feat: fetch features and store locally for offline users (#706)
* feat: fetch features and store locally for offline users * feat: handle success and error cases * refactor: move offline activation code reading/validation to snjs * chore: update after renaming snjs function * fix: correct condition for checking offline users * feat: let users remove their previous offline keys (WIP) * refactor: handle setOfflineFeatures function response accordingly * feat: remove corresponding local data when removing offline key * fix: use snjs' confirm dialog instead of custom one * feat: show warning before installing extension from untrusted source * refactor: move functions for validating external feature url and checking if custom server host was used to snjs * chore: put correct snjs version * chore: make `eslint-plugin-react-hooks` in yarn.lock to match the `develop` branch * chore: deps update * chore: deps update
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
Credentials,
|
||||
SignOutWrapper,
|
||||
Authentication,
|
||||
Advanced
|
||||
} from '@/preferences/panes/account';
|
||||
import { PreferencesPane } from '@/preferences/components';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
@@ -24,6 +25,7 @@ export const AccountPreferences = observer(
|
||||
<Authentication application={application} appState={appState} />
|
||||
{appState.enableUnfinishedFeatures && <SubscriptionWrapper application={application} />}
|
||||
<SignOutWrapper application={application} appState={appState} />
|
||||
<Advanced application={application} appState={appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +36,7 @@ export const AccountPreferences = observer(
|
||||
<Sync application={application} />
|
||||
{appState.enableUnfinishedFeatures && <SubscriptionWrapper application={application} />}
|
||||
<SignOutWrapper application={application} appState={appState} />
|
||||
<Advanced application={application} appState={appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { PreferencesGroup, PreferencesSegment, Title } from '@/preferences/components';
|
||||
import { OfflineSubscription } from '@/preferences/panes/account/offlineSubscription';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
|
||||
interface IProps {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export const Advanced: FunctionalComponent<IProps> = observer(({ application, appState }) => {
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className='flex flex-row items-center'>
|
||||
<div className='flex-grow flex flex-col'>
|
||||
<Title>Advanced Settings</Title>
|
||||
<OfflineSubscription application={application} appState={appState} />
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
@@ -3,3 +3,4 @@ export { Sync } from './Sync';
|
||||
export { Credentials } from './Credentials';
|
||||
export { SignOutWrapper } from './SignOutView';
|
||||
export { Authentication } from './Authentication';
|
||||
export { Advanced } from './Advanced';
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { Subtitle } from '@/preferences/components';
|
||||
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||
import { Button } from '@/components/Button';
|
||||
import { JSXInternal } from '@node_modules/preact/src/jsx';
|
||||
import TargetedEvent = JSXInternal.TargetedEvent;
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { STRING_REMOVE_OFFLINE_KEY_CONFIRMATION } from '@/strings';
|
||||
import { ButtonType } from '@standardnotes/snjs';
|
||||
|
||||
interface IProps {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export const OfflineSubscription: FunctionalComponent<IProps> = observer(({ application, appState }) => {
|
||||
const [activationCode, setActivationCode] = useState('');
|
||||
const [isSuccessfullyActivated, setIsSuccessfullyActivated] = useState(false);
|
||||
const [isSuccessfullyRemoved, setIsSuccessfullyRemoved] = useState(false);
|
||||
const [hasUserPreviouslyStoredCode, setHasUserPreviouslyStoredCode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (application.getIsOfflineActivationCodeStoredPreviously()) {
|
||||
setHasUserPreviouslyStoredCode(true);
|
||||
}
|
||||
}, [application]);
|
||||
|
||||
const shouldShowOfflineSubscription = () => {
|
||||
return !application.hasAccount() || application.isCustomServerHostUsed();
|
||||
};
|
||||
|
||||
const handleSubscriptionCodeSubmit = async (event: TargetedEvent<HTMLFormElement, Event>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const result = await application.setOfflineFeatures(activationCode);
|
||||
|
||||
if (result?.error) {
|
||||
await application.alertService.alert(result.error);
|
||||
} else {
|
||||
setIsSuccessfullyActivated(true);
|
||||
setHasUserPreviouslyStoredCode(true);
|
||||
setIsSuccessfullyRemoved(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveOfflineKey = async () => {
|
||||
await application.removeOfflineActivationCode();
|
||||
|
||||
setIsSuccessfullyActivated(false);
|
||||
setHasUserPreviouslyStoredCode(false);
|
||||
setActivationCode('');
|
||||
setIsSuccessfullyRemoved(true);
|
||||
};
|
||||
|
||||
if (!shouldShowOfflineSubscription()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleRemoveClick = async () => {
|
||||
application.alertService.confirm(
|
||||
STRING_REMOVE_OFFLINE_KEY_CONFIRMATION,
|
||||
'Remove offline key?',
|
||||
'Remove Offline Key',
|
||||
ButtonType.Danger,
|
||||
'Cancel'
|
||||
)
|
||||
.then(async (shouldRemove: boolean) => {
|
||||
if (shouldRemove) {
|
||||
await handleRemoveOfflineKey();
|
||||
}
|
||||
})
|
||||
.catch((err: string) => {
|
||||
application.alertService.alert(err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex flex-col mt-3 w-full'>
|
||||
<Subtitle>{!hasUserPreviouslyStoredCode && 'Activate'} Offline Subscription</Subtitle>
|
||||
<form onSubmit={handleSubscriptionCodeSubmit}>
|
||||
<div className={'mt-2'}>
|
||||
{!hasUserPreviouslyStoredCode && (
|
||||
<DecoratedInput
|
||||
onChange={(code) => setActivationCode(code)}
|
||||
placeholder={'Offline Subscription Code'}
|
||||
text={activationCode}
|
||||
disabled={isSuccessfullyActivated}
|
||||
className={'mb-3'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{(isSuccessfullyActivated || isSuccessfullyRemoved) && (
|
||||
<div className={'mt-3 mb-3 info font-bold'}>
|
||||
Successfully {isSuccessfullyActivated ? 'Activated' : 'Removed'}!
|
||||
</div>
|
||||
)}
|
||||
{hasUserPreviouslyStoredCode && (
|
||||
<Button
|
||||
type='danger'
|
||||
label='Remove offline key'
|
||||
onClick={() => {
|
||||
handleRemoveClick();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!hasUserPreviouslyStoredCode && !isSuccessfullyActivated && (
|
||||
<Button
|
||||
label={'Submit'}
|
||||
type='primary'
|
||||
disabled={activationCode === ''}
|
||||
onClick={(event) =>
|
||||
handleSubscriptionCodeSubmit(event as TargetedEvent<HTMLFormElement>)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -111,6 +111,9 @@ export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT =
|
||||
'<a href="https://standardnotes.com/help/security" target="_blank">Security Upgrade page.</a>';
|
||||
export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade';
|
||||
|
||||
export const STRING_REMOVE_OFFLINE_KEY_CONFIRMATION =
|
||||
'This will delete the previously saved offline key.';
|
||||
|
||||
export const Strings = {
|
||||
protectingNoteWithoutProtectionSources:
|
||||
'Access to this note will not be restricted until you set up a passcode or account.',
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"@reach/listbox": "^0.16.2",
|
||||
"@standardnotes/features": "1.8.0",
|
||||
"@standardnotes/sncrypto-web": "1.5.3",
|
||||
"@standardnotes/snjs": "2.16.3",
|
||||
"@standardnotes/snjs": "2.17.1",
|
||||
"mobx": "^6.3.5",
|
||||
"mobx-react-lite": "^3.2.1",
|
||||
"preact": "^10.5.15",
|
||||
|
||||
26
yarn.lock
26
yarn.lock
@@ -2166,10 +2166,10 @@
|
||||
dependencies:
|
||||
"@standardnotes/auth" "^3.8.1"
|
||||
|
||||
"@standardnotes/features@1.7.3", "@standardnotes/features@^1.7.3":
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.7.3.tgz#4872c837fd11d069a8a41941bb3e5f294fb13d9c"
|
||||
integrity sha512-G9NACv8pfVOB9O9L1C+Yoh25vMWVFLfF0FKSK5jjm/THm/w3SiQ2K82BIGgoQGpVGGAPEPa3Ru+OCBs3w8u+Jg==
|
||||
"@standardnotes/features@1.8.0", "@standardnotes/features@^1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.8.0.tgz#1414350108714e376c57600b74437b9ae58cf343"
|
||||
integrity sha512-gkBM82kEeKj4tve25WFsvPMb7MLqK2C3HcjKxk4RfoGouLAGjnSAVqs4AKZsDqdOrDZr5cAv55Uychr5qZMJTw==
|
||||
dependencies:
|
||||
"@standardnotes/common" "^1.2.1"
|
||||
|
||||
@@ -2192,15 +2192,15 @@
|
||||
buffer "^6.0.3"
|
||||
libsodium-wrappers "^0.7.9"
|
||||
|
||||
"@standardnotes/snjs@2.16.2":
|
||||
version "2.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.16.2.tgz#2958ec0a2f1724343de82204f905311d7c3ffb65"
|
||||
integrity sha512-G9HNu1TsAnK0OeRo6IYvmIR/huKoNkB+qWDPuh2+p/pJjLrtO6SGrOD4cm4Mg/63t29g8wW8Za/6/tPJHZOFCg==
|
||||
"@standardnotes/snjs@2.17.1":
|
||||
version "2.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.17.1.tgz#bb2761bd3f6f750c2497deb9ead8b85f3723ec1d"
|
||||
integrity sha512-RPIZAG2cuxhwKhUA5bujw0Dzsb8k2xTrFRQtvTPdRAS+isqJid0nWjHyv3gfC8XsMmDp5J3Kurg+5sz77liQfQ==
|
||||
dependencies:
|
||||
"@standardnotes/auth" "^3.8.1"
|
||||
"@standardnotes/common" "^1.2.1"
|
||||
"@standardnotes/domain-events" "^2.5.1"
|
||||
"@standardnotes/features" "^1.7.3"
|
||||
"@standardnotes/features" "^1.8.0"
|
||||
"@standardnotes/settings" "^1.2.1"
|
||||
"@standardnotes/sncrypto-common" "^1.5.2"
|
||||
|
||||
@@ -4006,10 +4006,10 @@ eslint-config-prettier@^8.3.0:
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a"
|
||||
integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==
|
||||
|
||||
eslint-plugin-react-hooks@^4.2.1-alpha-4298ddbc5-20211023:
|
||||
version "4.2.1-alpha-4298ddbc5-20211023"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.1-alpha-4298ddbc5-20211023.tgz#b227d6f900b4c86772585ae8130f2df625f05703"
|
||||
integrity sha512-JK7BG4+zc4vpIMMxAiwgLQXQVrvHHKzrHmSNDFDwRwy4zVvKyp7mL9m8AsWvgFRQdWgmCeAACcrA4XqeNsxZ+w==
|
||||
eslint-plugin-react-hooks@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556"
|
||||
integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==
|
||||
|
||||
eslint-plugin-react@^7.26.1:
|
||||
version "7.26.1"
|
||||
|
||||
Reference in New Issue
Block a user