chore: move all components into Components dir with pascal case (#934)
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
} from '@/components/Preferences/components';
|
||||
import { OfflineSubscription } from '@/components/Preferences/panes/account/offlineSubscription';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { Extensions } from '@/components/Preferences/panes/Extensions';
|
||||
import { ExtensionsLatestVersions } from '@/components/Preferences/panes/extensions-segments';
|
||||
import { AccordionItem } from '@/components/Shared/AccordionItem';
|
||||
|
||||
interface IProps {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
extensionsLatestVersions: ExtensionsLatestVersions;
|
||||
}
|
||||
|
||||
export const Advanced: FunctionalComponent<IProps> = observer(
|
||||
({ application, appState, extensionsLatestVersions }) => {
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<AccordionItem title={'Advanced Settings'}>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-grow flex flex-col">
|
||||
<OfflineSubscription
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
<Extensions
|
||||
className={'mt-3'}
|
||||
application={application}
|
||||
extensionsLatestVersions={extensionsLatestVersions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,60 @@
|
||||
import { AccountMenuPane } from '@/components/AccountMenu';
|
||||
import { Button } from '@/components/Button';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/components/Preferences/components';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { AccountIllustration } from '@standardnotes/stylekit';
|
||||
|
||||
export const Authentication: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ appState }) => {
|
||||
const clickSignIn = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setCurrentPane(AccountMenuPane.SignIn);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
|
||||
const clickRegister = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setCurrentPane(AccountMenuPane.Register);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-col items-center px-12">
|
||||
<AccountIllustration className="mb-3" />
|
||||
<Title>You're not signed in</Title>
|
||||
<Text className="text-center mb-3">
|
||||
Sign in to sync your notes and preferences across all your devices
|
||||
and enable end-to-end encryption.
|
||||
</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
label="Create free account"
|
||||
onClick={clickRegister}
|
||||
className="mb-3"
|
||||
/>
|
||||
<div className="text-input">
|
||||
Already have an account?{' '}
|
||||
<button
|
||||
className="border-0 p-0 bg-default color-info underline cursor-pointer"
|
||||
onClick={clickSignIn}
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Subtitle,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/components/Preferences/components';
|
||||
import { Button } from '@/components/Button';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { observer } from '@node_modules/mobx-react-lite';
|
||||
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||
import { dateToLocalizedString } from '@standardnotes/snjs';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
import { ChangeEmail } from '@/components/Preferences/panes/account/changeEmail';
|
||||
import { FunctionComponent, render } from 'preact';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { PasswordWizard } from '@/components/PasswordWizard';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
export const Credentials: FunctionComponent<Props> = observer(
|
||||
({ application }: Props) => {
|
||||
const [isChangeEmailDialogOpen, setIsChangeEmailDialogOpen] =
|
||||
useState(false);
|
||||
|
||||
const user = application.getUser();
|
||||
|
||||
const passwordCreatedAtTimestamp =
|
||||
application.getUserPasswordCreationDate() as Date;
|
||||
const passwordCreatedOn = dateToLocalizedString(passwordCreatedAtTimestamp);
|
||||
|
||||
const presentPasswordWizard = useCallback(() => {
|
||||
render(
|
||||
<PasswordWizard application={application} />,
|
||||
document.body.appendChild(document.createElement('div'))
|
||||
);
|
||||
}, [application]);
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Credentials</Title>
|
||||
<Subtitle>Email</Subtitle>
|
||||
<Text>
|
||||
You're signed in as <span className="font-bold">{user?.email}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className="min-w-20 mt-3"
|
||||
type="normal"
|
||||
label="Change email"
|
||||
onClick={() => {
|
||||
setIsChangeEmailDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
<HorizontalSeparator classes="mt-5 mb-3" />
|
||||
<Subtitle>Password</Subtitle>
|
||||
<Text>
|
||||
Current password was set on{' '}
|
||||
<span className="font-bold">{passwordCreatedOn}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className="min-w-20 mt-3"
|
||||
type="normal"
|
||||
label="Change password"
|
||||
onClick={presentPasswordWizard}
|
||||
/>
|
||||
{isChangeEmailDialogOpen && (
|
||||
<ChangeEmail
|
||||
onCloseDialog={() => setIsChangeEmailDialogOpen(false)}
|
||||
application={application}
|
||||
/>
|
||||
)}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Button } from '@/components/Button';
|
||||
import { OtherSessionsSignOutContainer } from '@/components/OtherSessionsSignOut';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Subtitle,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/components/Preferences/components';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
const SignOutView: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ application, appState }) => {
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Sign out</Title>
|
||||
<Subtitle>Other devices</Subtitle>
|
||||
<Text>Want to sign out on all devices except this one?</Text>
|
||||
<div className="min-h-3" />
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
className="mr-3"
|
||||
type="normal"
|
||||
label="Sign out other sessions"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setOtherSessionsSignOut(true);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="normal"
|
||||
label="Manage sessions"
|
||||
onClick={() => appState.openSessionsModal()}
|
||||
/>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
<Subtitle>This device</Subtitle>
|
||||
<Text>This will delete all local items and preferences.</Text>
|
||||
<div className="min-h-3" />
|
||||
<Button
|
||||
type="danger"
|
||||
label="Sign out and clear local data"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setSigningOut(true);
|
||||
}}
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
<OtherSessionsSignOutContainer
|
||||
appState={appState}
|
||||
application={application}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const ClearSessionDataView: FunctionComponent<{
|
||||
appState: AppState;
|
||||
}> = observer(({ appState }) => {
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Clear session data</Title>
|
||||
<Text>This will delete all local items and preferences.</Text>
|
||||
<div className="min-h-3" />
|
||||
<Button
|
||||
type="danger"
|
||||
label="Clear Session Data"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setSigningOut(true);
|
||||
}}
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
|
||||
export const SignOutWrapper: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ application, appState }) => {
|
||||
if (!application.hasAccount()) {
|
||||
return <ClearSessionDataView appState={appState} />;
|
||||
}
|
||||
return <SignOutView appState={appState} application={application} />;
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/components/Preferences/components';
|
||||
import { Button } from '@/components/Button';
|
||||
import { SyncQueueStrategy, dateToLocalizedString } from '@standardnotes/snjs';
|
||||
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
|
||||
import { useState } from '@node_modules/preact/hooks';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||
return dateToLocalizedString(lastUpdatedDate);
|
||||
};
|
||||
|
||||
export const Sync: FunctionComponent<Props> = observer(
|
||||
({ application }: Props) => {
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||
const [lastSyncDate, setLastSyncDate] = useState(
|
||||
formatLastSyncDate(application.sync.getLastSyncDate() as Date)
|
||||
);
|
||||
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true);
|
||||
|
||||
const response = await application.sync.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true,
|
||||
});
|
||||
setIsSyncingInProgress(false);
|
||||
if (response && (response as any).error) {
|
||||
application.alertService.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
} else {
|
||||
setLastSyncDate(
|
||||
formatLastSyncDate(application.sync.getLastSyncDate() as Date)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-grow flex flex-col">
|
||||
<Title>Sync</Title>
|
||||
<Text>
|
||||
Last synced <span className="font-bold">on {lastSyncDate}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className="min-w-20 mt-3"
|
||||
type="normal"
|
||||
label="Sync now"
|
||||
disabled={isSyncingInProgress}
|
||||
onClick={doSynchronization}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
import { StateUpdater } from 'preact/hooks';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
type Props = {
|
||||
setNewEmail: StateUpdater<string>;
|
||||
setCurrentPassword: StateUpdater<string>;
|
||||
};
|
||||
|
||||
const labelClassName = `block mb-1`;
|
||||
|
||||
const inputClassName = 'sk-input contrast';
|
||||
|
||||
export const ChangeEmailForm: FunctionalComponent<Props> = ({
|
||||
setNewEmail,
|
||||
setCurrentPassword,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-full flex flex-col">
|
||||
<div className="mt-2 mb-3">
|
||||
<label className={labelClassName} htmlFor="change-email-email-input">
|
||||
New Email:
|
||||
</label>
|
||||
<input
|
||||
id="change-email-email-input"
|
||||
className={inputClassName}
|
||||
type="email"
|
||||
onChange={({ target }) => {
|
||||
setNewEmail((target as HTMLInputElement).value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className={labelClassName} htmlFor="change-email-password-input">
|
||||
Current Password:
|
||||
</label>
|
||||
<input
|
||||
id="change-email-password-input"
|
||||
className={inputClassName}
|
||||
type="password"
|
||||
onChange={({ target }) => {
|
||||
setCurrentPassword((target as HTMLInputElement).value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
export const ChangeEmailSuccess: FunctionalComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className={'sk-label sk-bold info mt-2'}>
|
||||
Your email has been successfully changed.
|
||||
</div>
|
||||
<p className={'sk-p'}>
|
||||
Please ensure you are running the latest version of Standard Notes on
|
||||
all platforms to ensure maximum compatibility.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
import { useState } from '@node_modules/preact/hooks';
|
||||
import {
|
||||
ModalDialog,
|
||||
ModalDialogButtons,
|
||||
ModalDialogDescription,
|
||||
ModalDialogLabel,
|
||||
} from '@/components/Shared/ModalDialog';
|
||||
import { Button } from '@/components/Button';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { useBeforeUnload } from '@/hooks/useBeforeUnload';
|
||||
import { ChangeEmailForm } from './ChangeEmailForm';
|
||||
import { ChangeEmailSuccess } from './ChangeEmailSuccess';
|
||||
import { isEmailValid } from '@/utils';
|
||||
|
||||
enum SubmitButtonTitles {
|
||||
Default = 'Continue',
|
||||
GeneratingKeys = 'Generating Keys...',
|
||||
Finish = 'Finish',
|
||||
}
|
||||
|
||||
enum Steps {
|
||||
InitialStep,
|
||||
FinishStep,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onCloseDialog: () => void;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const ChangeEmail: FunctionalComponent<Props> = ({
|
||||
onCloseDialog,
|
||||
application,
|
||||
}) => {
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newEmail, setNewEmail] = useState('');
|
||||
const [isContinuing, setIsContinuing] = useState(false);
|
||||
const [lockContinue, setLockContinue] = useState(false);
|
||||
const [submitButtonTitle, setSubmitButtonTitle] = useState(
|
||||
SubmitButtonTitles.Default
|
||||
);
|
||||
const [currentStep, setCurrentStep] = useState(Steps.InitialStep);
|
||||
|
||||
useBeforeUnload();
|
||||
|
||||
const applicationAlertService = application.alertService;
|
||||
|
||||
const validateCurrentPassword = async () => {
|
||||
if (!currentPassword || currentPassword.length === 0) {
|
||||
applicationAlertService.alert('Please enter your current password.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const success = await application.validateAccountPassword(currentPassword);
|
||||
if (!success) {
|
||||
applicationAlertService.alert(
|
||||
'The current password you entered is not correct. Please try again.'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
const validateNewEmail = async () => {
|
||||
if (!isEmailValid(newEmail)) {
|
||||
applicationAlertService.alert(
|
||||
'The email you entered has an invalid format. Please review your input and try again.'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const resetProgressState = () => {
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Default);
|
||||
setIsContinuing(false);
|
||||
};
|
||||
|
||||
const processEmailChange = async () => {
|
||||
await application.downloadBackup();
|
||||
|
||||
setLockContinue(true);
|
||||
|
||||
const response = await application.changeEmail(newEmail, currentPassword);
|
||||
|
||||
const success = !response.error;
|
||||
|
||||
setLockContinue(false);
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
const dismiss = () => {
|
||||
if (lockContinue) {
|
||||
applicationAlertService.alert(
|
||||
'Cannot close window until pending tasks are complete.'
|
||||
);
|
||||
} else {
|
||||
onCloseDialog();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (lockContinue || isContinuing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep === Steps.FinishStep) {
|
||||
dismiss();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsContinuing(true);
|
||||
setSubmitButtonTitle(SubmitButtonTitles.GeneratingKeys);
|
||||
|
||||
const valid =
|
||||
(await validateCurrentPassword()) && (await validateNewEmail());
|
||||
|
||||
if (!valid) {
|
||||
resetProgressState();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await processEmailChange();
|
||||
if (!success) {
|
||||
resetProgressState();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsContinuing(false);
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Finish);
|
||||
setCurrentStep(Steps.FinishStep);
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
if (lockContinue) {
|
||||
applicationAlertService.alert(
|
||||
'Cannot close window until pending tasks are complete.'
|
||||
);
|
||||
} else {
|
||||
onCloseDialog();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={handleDialogClose}>
|
||||
Change Email
|
||||
</ModalDialogLabel>
|
||||
<ModalDialogDescription className="px-4.5">
|
||||
{currentStep === Steps.InitialStep && (
|
||||
<ChangeEmailForm
|
||||
setNewEmail={setNewEmail}
|
||||
setCurrentPassword={setCurrentPassword}
|
||||
/>
|
||||
)}
|
||||
{currentStep === Steps.FinishStep && <ChangeEmailSuccess />}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons className="px-4.5">
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="primary"
|
||||
label={submitButtonTitle}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export { Subscription } from './subscription/Subscription';
|
||||
export { Sync } from './Sync';
|
||||
export { Credentials } from './Credentials';
|
||||
export { SignOutWrapper } from './SignOutView';
|
||||
export { Authentication } from './Authentication';
|
||||
export { Advanced } from './Advanced';
|
||||
@@ -0,0 +1,146 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { Subtitle } from '@/components/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, ClientDisplayableError } from '@standardnotes/snjs';
|
||||
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||
|
||||
interface IProps {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export const OfflineSubscription: FunctionalComponent<IProps> = observer(
|
||||
({ application }) => {
|
||||
const [activationCode, setActivationCode] = useState('');
|
||||
const [isSuccessfullyActivated, setIsSuccessfullyActivated] =
|
||||
useState(false);
|
||||
const [isSuccessfullyRemoved, setIsSuccessfullyRemoved] = useState(false);
|
||||
const [hasUserPreviouslyStoredCode, setHasUserPreviouslyStoredCode] =
|
||||
useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (application.features.hasOfflineRepo()) {
|
||||
setHasUserPreviouslyStoredCode(true);
|
||||
}
|
||||
}, [application]);
|
||||
|
||||
const shouldShowOfflineSubscription = () => {
|
||||
return (
|
||||
!application.hasAccount() ||
|
||||
application.isThirdPartyHostUsed() ||
|
||||
hasUserPreviouslyStoredCode
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubscriptionCodeSubmit = async (
|
||||
event: TargetedEvent<HTMLFormElement, Event>
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
const result = await application.features.setOfflineFeaturesCode(
|
||||
activationCode
|
||||
);
|
||||
|
||||
if (result instanceof ClientDisplayableError) {
|
||||
await application.alertService.alert(result.text);
|
||||
} else {
|
||||
setIsSuccessfullyActivated(true);
|
||||
setHasUserPreviouslyStoredCode(true);
|
||||
setIsSuccessfullyRemoved(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveOfflineKey = async () => {
|
||||
await application.features.deleteOfflineFeatureRepo();
|
||||
|
||||
setIsSuccessfullyActivated(false);
|
||||
setHasUserPreviouslyStoredCode(false);
|
||||
setActivationCode('');
|
||||
setIsSuccessfullyRemoved(true);
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
if (!shouldShowOfflineSubscription()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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'}>
|
||||
Your offline subscription code has been 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>
|
||||
<HorizontalSeparator classes="mt-8 mb-5" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { LinkButton, Text } from '@/components/Preferences/components';
|
||||
import { Button } from '@/components/Button';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { loadPurchaseFlowUrl } from '@/components/PurchaseFlow/PurchaseFlowWrapper';
|
||||
|
||||
export const NoSubscription: FunctionalComponent<{
|
||||
application: WebApplication;
|
||||
}> = ({ application }) => {
|
||||
const [isLoadingPurchaseFlow, setIsLoadingPurchaseFlow] = useState(false);
|
||||
const [purchaseFlowError, setPurchaseFlowError] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
||||
const onPurchaseClick = async () => {
|
||||
const errorMessage =
|
||||
'There was an error when attempting to redirect you to the subscription page.';
|
||||
setIsLoadingPurchaseFlow(true);
|
||||
try {
|
||||
if (!(await loadPurchaseFlowUrl(application))) {
|
||||
setPurchaseFlowError(errorMessage);
|
||||
}
|
||||
} catch (e) {
|
||||
setPurchaseFlowError(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingPurchaseFlow(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>You don't have a Standard Notes subscription yet.</Text>
|
||||
{isLoadingPurchaseFlow && (
|
||||
<Text>Redirecting you to the subscription page...</Text>
|
||||
)}
|
||||
{purchaseFlowError && (
|
||||
<Text className="color-danger">{purchaseFlowError}</Text>
|
||||
)}
|
||||
<div className="flex">
|
||||
<LinkButton
|
||||
className="min-w-20 mt-3 mr-3"
|
||||
label="Learn More"
|
||||
link={window.plansUrl as string}
|
||||
/>
|
||||
{application.hasAccount() && (
|
||||
<Button
|
||||
className="min-w-20 mt-3"
|
||||
type="primary"
|
||||
label="Subscribe"
|
||||
onClick={onPurchaseClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Title,
|
||||
} from '@/components/Preferences/components';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { SubscriptionInformation } from './SubscriptionInformation';
|
||||
import { NoSubscription } from './NoSubscription';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
export const Subscription: FunctionComponent<Props> = observer(
|
||||
({ application, appState }: Props) => {
|
||||
const subscriptionState = appState.subscription;
|
||||
const { userSubscription } = subscriptionState;
|
||||
|
||||
const now = new Date().getTime();
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-grow flex flex-col">
|
||||
<Title>Subscription</Title>
|
||||
{userSubscription && userSubscription.endsAt > now ? (
|
||||
<SubscriptionInformation
|
||||
subscriptionState={subscriptionState}
|
||||
application={application}
|
||||
/>
|
||||
) : (
|
||||
<NoSubscription application={application} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,91 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { SubscriptionState } from '../../../../../ui_models/app_state/subscription_state';
|
||||
import { Text } from '@/components/Preferences/components';
|
||||
import { Button } from '@/components/Button';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { openSubscriptionDashboard } from '@/hooks/manageSubscription';
|
||||
|
||||
type Props = {
|
||||
subscriptionState: SubscriptionState;
|
||||
application?: WebApplication;
|
||||
};
|
||||
|
||||
const StatusText = observer(({ subscriptionState }: Props) => {
|
||||
const {
|
||||
userSubscriptionName,
|
||||
userSubscriptionExpirationDate,
|
||||
isUserSubscriptionExpired,
|
||||
isUserSubscriptionCanceled,
|
||||
} = subscriptionState;
|
||||
const expirationDateString = userSubscriptionExpirationDate?.toLocaleString();
|
||||
|
||||
if (isUserSubscriptionCanceled) {
|
||||
return (
|
||||
<Text className="mt-1">
|
||||
Your{' '}
|
||||
<span className="font-bold">
|
||||
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||
{userSubscriptionName}
|
||||
</span>{' '}
|
||||
subscription has been canceled{' '}
|
||||
{isUserSubscriptionExpired ? (
|
||||
<span className="font-bold">
|
||||
and expired on {expirationDateString}
|
||||
</span>
|
||||
) : (
|
||||
<span className="font-bold">
|
||||
but will remain valid until {expirationDateString}
|
||||
</span>
|
||||
)}
|
||||
. You may resubscribe below if you wish.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (isUserSubscriptionExpired) {
|
||||
return (
|
||||
<Text className="mt-1">
|
||||
Your{' '}
|
||||
<span className="font-bold">
|
||||
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||
{userSubscriptionName}
|
||||
</span>{' '}
|
||||
subscription{' '}
|
||||
<span className="font-bold">expired on {expirationDateString}</span>.
|
||||
You may resubscribe below if you wish.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text className="mt-1">
|
||||
Your{' '}
|
||||
<span className="font-bold">
|
||||
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||
{userSubscriptionName}
|
||||
</span>{' '}
|
||||
subscription will be{' '}
|
||||
<span className="font-bold">renewed on {expirationDateString}</span>.
|
||||
</Text>
|
||||
);
|
||||
});
|
||||
|
||||
export const SubscriptionInformation = observer(
|
||||
({ subscriptionState, application }: Props) => {
|
||||
const manageSubscription = async () => {
|
||||
openSubscriptionDashboard(application!);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusText subscriptionState={subscriptionState} />
|
||||
<Button
|
||||
className="min-w-20 mt-3 mr-3"
|
||||
type="normal"
|
||||
label="Manage subscription"
|
||||
onClick={manageSubscription}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user