diff --git a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx index faf149cbd..7c3db4272 100644 --- a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx +++ b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx @@ -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( {appState.enableUnfinishedFeatures && } + ); } @@ -34,6 +36,7 @@ export const AccountPreferences = observer( {appState.enableUnfinishedFeatures && } + ); } diff --git a/app/assets/javascripts/preferences/panes/account/Advanced.tsx b/app/assets/javascripts/preferences/panes/account/Advanced.tsx new file mode 100644 index 000000000..7b0193c33 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/Advanced.tsx @@ -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 = observer(({ application, appState }) => { + return ( + + + + + Advanced Settings + + + + + + ); +}); diff --git a/app/assets/javascripts/preferences/panes/account/index.ts b/app/assets/javascripts/preferences/panes/account/index.ts index eb9c2bc51..85800edb5 100644 --- a/app/assets/javascripts/preferences/panes/account/index.ts +++ b/app/assets/javascripts/preferences/panes/account/index.ts @@ -3,3 +3,4 @@ export { Sync } from './Sync'; export { Credentials } from './Credentials'; export { SignOutWrapper } from './SignOutView'; export { Authentication } from './Authentication'; +export { Advanced } from './Advanced'; diff --git a/app/assets/javascripts/preferences/panes/account/offlineSubscription.tsx b/app/assets/javascripts/preferences/panes/account/offlineSubscription.tsx new file mode 100644 index 000000000..360aa40d0 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/offlineSubscription.tsx @@ -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 = 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) => { + 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 ( + + + {!hasUserPreviouslyStoredCode && 'Activate'} Offline Subscription + + + {!hasUserPreviouslyStoredCode && ( + setActivationCode(code)} + placeholder={'Offline Subscription Code'} + text={activationCode} + disabled={isSuccessfullyActivated} + className={'mb-3'} + /> + )} + + {(isSuccessfullyActivated || isSuccessfullyRemoved) && ( + + Successfully {isSuccessfullyActivated ? 'Activated' : 'Removed'}! + + )} + {hasUserPreviouslyStoredCode && ( + { + handleRemoveClick(); + }} + /> + )} + {!hasUserPreviouslyStoredCode && !isSuccessfullyActivated && ( + + handleSubscriptionCodeSubmit(event as TargetedEvent) + } + /> + )} + + + + ); +}); diff --git a/app/assets/javascripts/strings.ts b/app/assets/javascripts/strings.ts index dab4117e3..15d69e72c 100644 --- a/app/assets/javascripts/strings.ts +++ b/app/assets/javascripts/strings.ts @@ -111,6 +111,9 @@ export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT = 'Security Upgrade page.'; 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.', diff --git a/package.json b/package.json index f41b2ffb3..b9174c599 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 24ed3414f..31f234a0a 100644 --- a/yarn.lock +++ b/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"