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:
Vardan Hakobyan
2021-11-03 22:27:36 +04:00
committed by GitHub
parent bad87a4f2f
commit 04fab80adb
7 changed files with 171 additions and 14 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
});

View File

@@ -3,3 +3,4 @@ export { Sync } from './Sync';
export { Credentials } from './Credentials';
export { SignOutWrapper } from './SignOutView';
export { Authentication } from './Authentication';
export { Advanced } from './Advanced';

View File

@@ -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>
);
});

View File

@@ -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.',