{Array.isArray(children)
? children
- .filter((child) => child != undefined && child !== '')
+ .filter(
+ (child) => child != undefined && child !== '' && child !== false
+ )
.map((child, i, arr) => (
<>
{child}
diff --git a/app/assets/javascripts/preferences/panes/Security.tsx b/app/assets/javascripts/preferences/panes/Security.tsx
index f5b0d1ecc..a0269a269 100644
--- a/app/assets/javascripts/preferences/panes/Security.tsx
+++ b/app/assets/javascripts/preferences/panes/Security.tsx
@@ -3,10 +3,11 @@ import { PreferencesPane } from '../components';
import { TwoFactorAuthWrapper } from './two-factor-auth';
import { MfaProps } from './two-factor-auth/MfaProps';
-interface SecurityProps extends MfaProps {}
-
-export const Security: FunctionComponent
= (props) => (
+export const Security: FunctionComponent = (props) => (
-
+
);
diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts
index 79e89298b..eb8b840b8 100644
--- a/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts
+++ b/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts
@@ -1,17 +1,6 @@
-export interface MfaGateway {
- getUser(): { uuid: string; email: string } | undefined;
-
- isMfaActivated(): Promise;
-
- generateMfaSecret(): Promise;
-
- getOtpToken(secret: string): Promise;
-
- enableMfa(secret: string, otpToken: string): Promise;
-
- disableMfa(): Promise;
-}
+import { MfaProvider, UserProvider } from '../../providers';
export interface MfaProps {
- mfaGateway: MfaGateway;
+ userProvider: UserProvider;
+ mfaProvider: MfaProvider;
}
diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts
index 1f756162a..44d6891f2 100644
--- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts
+++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts
@@ -1,5 +1,5 @@
-import { action, makeAutoObservable, observable, untracked } from 'mobx';
-import { MfaGateway } from './MfaProps';
+import { MfaProvider, UserProvider } from '../../providers';
+import { action, makeAutoObservable, observable } from 'mobx';
type ActivationStep = 'scan-qr-code' | 'save-secret-key' | 'verification';
type VerificationStatus = 'none' | 'invalid' | 'valid';
@@ -15,7 +15,8 @@ export class TwoFactorActivation {
private inputOtpToken = '';
constructor(
- private mfaGateway: MfaGateway,
+ private mfaProvider: MfaProvider,
+ private userProvider: UserProvider,
private readonly _secretKey: string,
private _cancelActivation: () => void,
private _enabled2FA: () => void
@@ -59,7 +60,7 @@ export class TwoFactorActivation {
}
get qrCode(): string {
- const email = this.mfaGateway.getUser()!.email;
+ const email = this.userProvider.getUser()!.email;
return `otpauth://totp/2FA?secret=${this._secretKey}&issuer=Standard%20Notes&label=${email}`;
}
@@ -101,7 +102,7 @@ export class TwoFactorActivation {
enable2FA(): void {
if (this.inputSecretKey === this._secretKey) {
- this.mfaGateway
+ this.mfaProvider
.enableMfa(this.inputSecretKey, this.inputOtpToken)
.then(
action(() => {
diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts
index 44aeb360f..629f75159 100644
--- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts
+++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts
@@ -1,5 +1,5 @@
+import { MfaProvider, UserProvider } from '@/preferences/providers';
import { action, makeAutoObservable, observable } from 'mobx';
-import { MfaGateway } from './MfaProps';
import { TwoFactorActivation } from './TwoFactorActivation';
type TwoFactorStatus =
@@ -7,20 +7,23 @@ type TwoFactorStatus =
| TwoFactorActivation
| 'two-factor-disabled';
-export const is2FADisabled = (s: TwoFactorStatus): s is 'two-factor-disabled' =>
- s === 'two-factor-disabled';
+export const is2FADisabled = (status: TwoFactorStatus): status is 'two-factor-disabled' =>
+ status === 'two-factor-disabled';
-export const is2FAActivation = (s: TwoFactorStatus): s is TwoFactorActivation =>
- (s as any).type === 'two-factor-activation';
+export const is2FAActivation = (status: TwoFactorStatus): status is TwoFactorActivation =>
+ (status as TwoFactorActivation)?.type === 'two-factor-activation';
-export const is2FAEnabled = (s: TwoFactorStatus): s is 'two-factor-enabled' =>
- s === 'two-factor-enabled';
+export const is2FAEnabled = (status: TwoFactorStatus): status is 'two-factor-enabled' =>
+ status === 'two-factor-enabled';
export class TwoFactorAuth {
private _status: TwoFactorStatus | 'fetching' = 'fetching';
private _errorMessage: string | null;
- constructor(private readonly mfaGateway: MfaGateway) {
+ constructor(
+ private readonly mfaProvider: MfaProvider,
+ private readonly userProvider: UserProvider
+ ) {
this._errorMessage = null;
makeAutoObservable<
@@ -37,12 +40,13 @@ export class TwoFactorAuth {
private startActivation(): void {
const setDisabled = action(() => (this._status = 'two-factor-disabled'));
const setEnabled = action(() => (this._status = 'two-factor-enabled'));
- this.mfaGateway
+ this.mfaProvider
.generateMfaSecret()
.then(
action((secret) => {
this._status = new TwoFactorActivation(
- this.mfaGateway,
+ this.mfaProvider,
+ this.userProvider,
secret,
setDisabled,
setEnabled
@@ -57,7 +61,7 @@ export class TwoFactorAuth {
}
private deactivate2FA(): void {
- this.mfaGateway
+ this.mfaProvider
.disableMfa()
.then(
action(() => {
@@ -72,18 +76,21 @@ export class TwoFactorAuth {
}
private get isLoggedIn(): boolean {
- return this.mfaGateway.getUser() != undefined;
+ return this.userProvider.getUser() != undefined;
}
fetchStatus(): void {
this._status = 'fetching';
if (!this.isLoggedIn) {
- this.setError('To enable 2FA, sign in or register for an account.');
return;
}
- this.mfaGateway
+ if (!this.isMfaFeatureAvailable) {
+ return;
+ }
+
+ this.mfaProvider
.isMfaActivated()
.then(
action((active) => {
@@ -99,7 +106,7 @@ export class TwoFactorAuth {
);
}
- setError(errorMessage: string | null): void {
+ private setError(errorMessage: string | null): void {
this._errorMessage = errorMessage;
}
@@ -108,6 +115,10 @@ export class TwoFactorAuth {
return;
}
+ if (!this.isMfaFeatureAvailable) {
+ return;
+ }
+
if (this._status === 'two-factor-disabled') {
return this.startActivation();
}
@@ -118,6 +129,12 @@ export class TwoFactorAuth {
}
get errorMessage(): string | null {
+ if (!this.isLoggedIn) {
+ return 'Two-factor authentication not available / Sign in or register for an account to configure 2FA';
+ }
+ if (!this.isMfaFeatureAvailable) {
+ return 'Two-factor authentication not available / A paid subscription plan is required to enable 2FA.';
+ }
return this._errorMessage;
}
@@ -127,4 +144,8 @@ export class TwoFactorAuth {
}
return this._status;
}
+
+ private get isMfaFeatureAvailable(): boolean {
+ return this.mfaProvider.isMfaFeatureAvailable();
+ }
}
diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx
index d23c7ed77..0207efb7c 100644
--- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx
+++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx
@@ -7,45 +7,38 @@ import {
} from '../../components';
import { Switch } from '../../../components/Switch';
import { observer } from 'mobx-react-lite';
-import {
- is2FAActivation,
- is2FADisabled,
- is2FAEnabled,
- TwoFactorAuth,
-} from './TwoFactorAuth';
-import { TwoFactorDisabledView } from './TwoFactorDisabledView';
+import { is2FAActivation, is2FADisabled, TwoFactorAuth } from './TwoFactorAuth';
import { TwoFactorActivationView } from './TwoFactorActivationView';
export const TwoFactorAuthView: FunctionComponent<{
auth: TwoFactorAuth;
-}> = observer(({ auth }) => (
-
-
-
-
-
Two-factor authentication
-
- An extra layer of security when logging in to your account.
-
- {auth.errorMessage != null && (
- {auth.errorMessage}
- )}
-
-
auth.toggle2FA()}
- />
-
-
-
- {is2FAActivation(auth.status) ? (
-
- ) : null}
-
- {!is2FAEnabled(auth.status) ? (
+}> = observer(({ auth }) => {
+ return (
+
-
+
+
+
Two-factor authentication
+
+ An extra layer of security when logging in to your account.
+
+
+
+
- ) : null}
-
-));
+
+ {is2FAActivation(auth.status) && (
+
+ )}
+
+ {auth.errorMessage != null && (
+
+ {auth.errorMessage}
+
+ )}
+
+ );
+});
diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDisabledView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDisabledView.tsx
deleted file mode 100644
index 327614485..000000000
--- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDisabledView.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Text } from '../../components';
-import { FunctionComponent } from 'preact';
-
-export const TwoFactorDisabledView: FunctionComponent = () => (
-
- Enabling two-factor authentication will sign you out of all other sessions.{' '}
-
- Learn more
-
-
-);
diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx
index 2d0494ad3..4d5cad0ab 100644
--- a/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx
+++ b/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx
@@ -4,10 +4,10 @@ import { MfaProps } from './MfaProps';
import { TwoFactorAuth } from './TwoFactorAuth';
import { TwoFactorAuthView } from './TwoFactorAuthView';
-export const TwoFactorAuthWrapper: FunctionComponent = ({
- mfaGateway,
-}) => {
- const [auth] = useState(() => new TwoFactorAuth(mfaGateway));
+export const TwoFactorAuthWrapper: FunctionComponent = (props) => {
+ const [auth] = useState(
+ () => new TwoFactorAuth(props.mfaProvider, props.userProvider)
+ );
auth.fetchStatus();
return ;
};
diff --git a/app/assets/javascripts/preferences/providers/MfaProvider.ts b/app/assets/javascripts/preferences/providers/MfaProvider.ts
new file mode 100644
index 000000000..338ff3131
--- /dev/null
+++ b/app/assets/javascripts/preferences/providers/MfaProvider.ts
@@ -0,0 +1,13 @@
+export interface MfaProvider {
+ isMfaActivated(): Promise;
+
+ generateMfaSecret(): Promise;
+
+ getOtpToken(secret: string): Promise;
+
+ enableMfa(secret: string, otpToken: string): Promise;
+
+ disableMfa(): Promise;
+
+ isMfaFeatureAvailable(): boolean;
+}
diff --git a/app/assets/javascripts/preferences/providers/UserProvider.ts b/app/assets/javascripts/preferences/providers/UserProvider.ts
new file mode 100644
index 000000000..7fad1d76b
--- /dev/null
+++ b/app/assets/javascripts/preferences/providers/UserProvider.ts
@@ -0,0 +1,3 @@
+export interface UserProvider {
+ getUser(): { uuid: string; email: string } | undefined;
+}
diff --git a/app/assets/javascripts/preferences/providers/index.ts b/app/assets/javascripts/preferences/providers/index.ts
new file mode 100644
index 000000000..db2179e81
--- /dev/null
+++ b/app/assets/javascripts/preferences/providers/index.ts
@@ -0,0 +1,2 @@
+export * from './MfaProvider';
+export * from './UserProvider';
diff --git a/package.json b/package.json
index 9d0b23d53..99ce6f3ad 100644
--- a/package.json
+++ b/package.json
@@ -71,7 +71,8 @@
"@reach/checkbox": "^0.13.2",
"@reach/dialog": "^0.13.0",
"@standardnotes/sncrypto-web": "1.5.2",
- "@standardnotes/snjs": "2.12.1",
+ "@standardnotes/features": "1.6.1",
+ "@standardnotes/snjs": "2.12.3",
"mobx": "^6.3.2",
"mobx-react-lite": "^3.2.0",
"preact": "^10.5.12",
diff --git a/yarn.lock b/yarn.lock
index 7da495c7c..4dc1f340d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2042,6 +2042,13 @@
dependencies:
"@standardnotes/common" "^1.1.0"
+"@standardnotes/features@1.6.1":
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.1.tgz#bfa227bd231dc1b54449936663731f5132b08e23"
+ integrity sha512-IC6fEotUqs23JdZx96JnEgARxwYzjmPz3UwU/uVn8hHjxPev/W0nyZFRiSlj4v+dod0jSa6FTR8iLLsOQ6M4Ug==
+ dependencies:
+ "@standardnotes/common" "^1.1.0"
+
"@standardnotes/settings@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.2.0.tgz#d7936c788138265b0720085ca9e63358d3092459"