Merge branch 'develop' into feature/subscription-info-in-preferences
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
import { FunctionalComponent, ComponentChild } from 'preact';
|
import { FunctionalComponent, ComponentChild } from 'preact';
|
||||||
|
import { HtmlInputTypes } from '@/enums';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
type?: HtmlInputTypes;
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
left?: ComponentChild[];
|
left?: ComponentChild[];
|
||||||
right?: ComponentChild[];
|
right?: ComponentChild[];
|
||||||
text?: string;
|
text?: string;
|
||||||
|
placeholder?: string;
|
||||||
onChange?: (text: string) => void;
|
onChange?: (text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,11 +16,13 @@ interface Props {
|
|||||||
* Input that can be decorated on the left and right side
|
* Input that can be decorated on the left and right side
|
||||||
*/
|
*/
|
||||||
export const DecoratedInput: FunctionalComponent<Props> = ({
|
export const DecoratedInput: FunctionalComponent<Props> = ({
|
||||||
|
type = 'text',
|
||||||
className = '',
|
className = '',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
text,
|
text,
|
||||||
|
placeholder = '',
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const base =
|
const base =
|
||||||
@@ -32,10 +37,11 @@ export const DecoratedInput: FunctionalComponent<Props> = ({
|
|||||||
{left}
|
{left}
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type={type}
|
||||||
className="w-full no-border color-black focus:shadow-none"
|
className="w-full no-border color-black focus:shadow-none"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={text}
|
value={text}
|
||||||
|
placeholder={placeholder}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange && onChange((e.target as HTMLInputElement).value)
|
onChange && onChange((e.target as HTMLInputElement).value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { FunctionalComponent } from 'preact';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
classes?: string;
|
||||||
|
}
|
||||||
|
export const HorizontalSeparator: FunctionalComponent<Props> = ({
|
||||||
|
classes = ''
|
||||||
|
}) => {
|
||||||
|
return <hr className={`h-1px w-full bg-border no-border ${classes}`} />;
|
||||||
|
};
|
||||||
@@ -1,19 +1,9 @@
|
|||||||
import { ComponentChildren, FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { IconButton } from '../../../components/IconButton';
|
import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@node_modules/@reach/alert-dialog';
|
||||||
import {
|
import { useRef } from '@node_modules/preact/hooks';
|
||||||
AlertDialog,
|
import { IconButton } from '@/components/IconButton';
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogLabel,
|
|
||||||
} from '@reach/alert-dialog';
|
|
||||||
import { useRef } from 'preact/hooks';
|
|
||||||
|
|
||||||
/**
|
export const ModalDialog: FunctionComponent = ({ children }) => {
|
||||||
* TwoFactorDialog is AlertDialog styled for 2FA
|
|
||||||
* Can be generalized but more use cases are needed
|
|
||||||
*/
|
|
||||||
export const TwoFactorDialog: FunctionComponent<{
|
|
||||||
children: ComponentChildren;
|
|
||||||
}> = ({ children }) => {
|
|
||||||
const ldRef = useRef<HTMLButtonElement>();
|
const ldRef = useRef<HTMLButtonElement>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -34,12 +24,14 @@ export const TwoFactorDialog: FunctionComponent<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TwoFactorDialogLabel: FunctionComponent<{
|
export const ModalDialogLabel: FunctionComponent<{
|
||||||
closeDialog: () => void;
|
closeDialog: () => void;
|
||||||
}> = ({ children, closeDialog }) => (
|
}> = ({ children, closeDialog }) => (
|
||||||
<AlertDialogLabel className="">
|
<AlertDialogLabel className="">
|
||||||
<div className="px-4 pt-4 pb-3 flex flex-row">
|
<div className="px-4 pt-4 pb-3 flex flex-row">
|
||||||
<div className="flex-grow color-black text-lg font-bold">{children}</div>
|
<div className="flex-grow color-black text-lg font-bold">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
className="color-grey-1 h-5 w-5"
|
className="color-grey-1 h-5 w-5"
|
||||||
icon="close"
|
icon="close"
|
||||||
@@ -50,15 +42,17 @@ export const TwoFactorDialogLabel: FunctionComponent<{
|
|||||||
</AlertDialogLabel>
|
</AlertDialogLabel>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TwoFactorDialogDescription: FunctionComponent = ({ children }) => (
|
export const ModalDialogDescription: FunctionComponent = ({ children }) => (
|
||||||
<AlertDialogDescription className="px-4 py-4">
|
<AlertDialogDescription className="px-4 py-4">
|
||||||
{children}
|
{children}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TwoFactorDialogButtons: FunctionComponent = ({ children }) => (
|
export const ModalDialogButtons: FunctionComponent = ({ children }) => (
|
||||||
<>
|
<>
|
||||||
<hr className="h-1px bg-border no-border m-0" />
|
<hr className="h-1px bg-border no-border m-0" />
|
||||||
<div className="px-4 py-4 flex flex-row justify-end gap-3">{children}</div>
|
<div className="px-4 py-4 flex flex-row justify-end gap-3">{children}</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export default ModalDialog;
|
||||||
24
app/assets/javascripts/enums.ts
Normal file
24
app/assets/javascripts/enums.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export enum HtmlInputTypes {
|
||||||
|
Button = 'button',
|
||||||
|
Checkbox = 'checkbox',
|
||||||
|
Color = 'color',
|
||||||
|
Date = 'date',
|
||||||
|
DateTimeLocal = 'datetime-local',
|
||||||
|
Email = 'email',
|
||||||
|
File = 'file',
|
||||||
|
Hidden = 'hidden',
|
||||||
|
Image = 'image',
|
||||||
|
Month = 'month',
|
||||||
|
Number = 'number',
|
||||||
|
Password = 'password',
|
||||||
|
Radio = 'radio',
|
||||||
|
Range = 'range',
|
||||||
|
Reset = 'reset',
|
||||||
|
Search = 'search',
|
||||||
|
Submit = 'submit',
|
||||||
|
Tel = 'tel',
|
||||||
|
Text = 'text',
|
||||||
|
Time = 'time',
|
||||||
|
Url = 'url',
|
||||||
|
Week = 'week'
|
||||||
|
}
|
||||||
11
app/assets/javascripts/hooks/useBeforeUnload.tsx
Normal file
11
app/assets/javascripts/hooks/useBeforeUnload.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useEffect } from '@node_modules/preact/hooks';
|
||||||
|
|
||||||
|
export const useBeforeUnload = (): void => {
|
||||||
|
useEffect(() => {
|
||||||
|
window.onbeforeunload = () => true;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
||||||
|
|
||||||
const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({
|
const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({
|
||||||
index,
|
index,
|
||||||
length,
|
length,
|
||||||
}) =>
|
}) =>
|
||||||
index < length - 1 ? (
|
index < length - 1 ? (
|
||||||
<hr className="h-1px w-full bg-border no-border" />
|
<HorizontalSeparator />
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
export const PreferencesSegment: FunctionComponent = ({ children }) => (
|
export const PreferencesSegment: FunctionComponent = ({ children }) => (
|
||||||
@@ -30,7 +31,7 @@ export const PreferencesGroup: FunctionComponent = ({ children }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const PreferencesPane: FunctionComponent = ({ children }) => (
|
export const PreferencesPane: FunctionComponent = ({ children }) => (
|
||||||
<div className="preferences-pane flex-grow flex flex-row overflow-y-auto min-h-0">
|
<div className="color-black flex-grow flex flex-row overflow-y-auto min-h-0">
|
||||||
<div className="flex-grow flex flex-col py-6 items-center">
|
<div className="flex-grow flex flex-col py-6 items-center">
|
||||||
<div className="w-125 max-w-125 flex flex-col gap-3">{children}</div>
|
<div className="w-125 max-w-125 flex flex-col gap-3">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { Sync, SubscriptionWrapper } from '@/preferences/panes/account';
|
import { Sync, SubscriptionWrapper, Credentials } from '@/preferences/panes/account';
|
||||||
import { PreferencesPane } from '@/preferences/components';
|
import { PreferencesPane } from '@/preferences/components';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
|
||||||
export const AccountPreferences = observer(({application}: {application: WebApplication}) => {
|
type Props = {
|
||||||
|
application: WebApplication;
|
||||||
|
}
|
||||||
|
export const AccountPreferences = observer(({application}: Props) => {
|
||||||
return (
|
return (
|
||||||
<PreferencesPane>
|
<PreferencesPane>
|
||||||
|
<Credentials application={application} />
|
||||||
<Sync application={application} />
|
<Sync application={application} />
|
||||||
<SubscriptionWrapper application={application} />
|
<SubscriptionWrapper application={application} />
|
||||||
</PreferencesPane>
|
</PreferencesPane>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/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 '@/utils';
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
import { ChangeEmail } from '@/preferences/panes/account/changeEmail';
|
||||||
|
import { ChangePassword } from '@/preferences/panes/account/changePassword';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Credentials = observer(({ application }: Props) => {
|
||||||
|
const [isChangePasswordDialogOpen, setIsChangePasswordDialogOpen] = useState(false);
|
||||||
|
const [isChangeEmailDialogOpen, setIsChangeEmailDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const user = application.getUser();
|
||||||
|
|
||||||
|
const passwordCreatedAtTimestamp = application.getUserPasswordCreationDate() as Date;
|
||||||
|
const passwordCreatedOn = dateToLocalizedString(passwordCreatedAtTimestamp);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<Title>Credentials</Title>
|
||||||
|
<div className={'text-input mt-2'}>
|
||||||
|
Email
|
||||||
|
</div>
|
||||||
|
<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' />
|
||||||
|
<div className={'text-input mt-2'}>
|
||||||
|
Password
|
||||||
|
</div>
|
||||||
|
<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={() => {
|
||||||
|
setIsChangePasswordDialogOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isChangeEmailDialogOpen && (
|
||||||
|
<ChangeEmail
|
||||||
|
onCloseDialog={() => setIsChangeEmailDialogOpen(false)}
|
||||||
|
application={application}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
isChangePasswordDialogOpen && (
|
||||||
|
<ChangePassword
|
||||||
|
onCloseDialog={() => setIsChangePasswordDialogOpen(false)}
|
||||||
|
application={application}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PreferencesSegment>
|
||||||
|
</PreferencesGroup>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -11,7 +11,7 @@ type Props = {
|
|||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Sync = observer(({ application }: Props) => {
|
export const Sync = observer(({ application }: Props) => {
|
||||||
const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||||
return dateToLocalizedString(lastUpdatedDate);
|
return dateToLocalizedString(lastUpdatedDate);
|
||||||
};
|
};
|
||||||
@@ -56,5 +56,3 @@ const Sync = observer(({ application }: Props) => {
|
|||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Sync;
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||||
|
import { StateUpdater } from 'preact/hooks';
|
||||||
|
import { FunctionalComponent } from 'preact';
|
||||||
|
import { HtmlInputTypes } from '@/enums';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setNewEmail: StateUpdater<string>
|
||||||
|
setCurrentPassword: StateUpdater<string>
|
||||||
|
}
|
||||||
|
export const ChangeEmailForm: FunctionalComponent<Props> = ({
|
||||||
|
setNewEmail,
|
||||||
|
setCurrentPassword
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
<>
|
||||||
|
<div className={'mt-2 mb-3'}>
|
||||||
|
<DecoratedInput
|
||||||
|
onChange={(newEmail) => {
|
||||||
|
setNewEmail(newEmail);
|
||||||
|
}}
|
||||||
|
placeholder={'New Email'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={'mt-2 mb-3'}>
|
||||||
|
<DecoratedInput
|
||||||
|
type={HtmlInputTypes.Password}
|
||||||
|
onChange={(currentPassword) => {
|
||||||
|
setCurrentPassword(currentPassword);
|
||||||
|
}}
|
||||||
|
placeholder={'Current Password'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FunctionalComponent } from 'preact';
|
||||||
|
|
||||||
|
export const ChangeEmailSuccess: FunctionalComponent = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={'sk-label sk-bold info'}>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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
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>
|
||||||
|
{currentStep === Steps.InitialStep && (
|
||||||
|
<ChangeEmailForm
|
||||||
|
setNewEmail={setNewEmail}
|
||||||
|
setCurrentPassword={setCurrentPassword}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{currentStep === Steps.FinishStep && <ChangeEmailSuccess />}
|
||||||
|
</ModalDialogDescription>
|
||||||
|
<ModalDialogButtons>
|
||||||
|
{currentStep === Steps.InitialStep && (
|
||||||
|
<Button
|
||||||
|
className='min-w-20'
|
||||||
|
type='normal'
|
||||||
|
label='Cancel'
|
||||||
|
onClick={handleDialogClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className='min-w-20'
|
||||||
|
type='primary'
|
||||||
|
label={submitButtonTitle}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
/>
|
||||||
|
</ModalDialogButtons>
|
||||||
|
</ModalDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||||
|
import { StateUpdater } from 'preact/hooks';
|
||||||
|
import { FunctionalComponent } from 'preact';
|
||||||
|
import { HtmlInputTypes } from '@/enums';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setCurrentPassword: StateUpdater<string>
|
||||||
|
setNewPassword: StateUpdater<string>
|
||||||
|
setNewPasswordConfirmation: StateUpdater<string>
|
||||||
|
}
|
||||||
|
export const ChangePasswordForm: FunctionalComponent<Props> = ({
|
||||||
|
setCurrentPassword,
|
||||||
|
setNewPassword,
|
||||||
|
setNewPasswordConfirmation
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
<>
|
||||||
|
<div className={'mt-2 mb-3'}>
|
||||||
|
<DecoratedInput
|
||||||
|
type={HtmlInputTypes.Password}
|
||||||
|
onChange={(currentPassword) => {
|
||||||
|
setCurrentPassword(currentPassword);
|
||||||
|
}}
|
||||||
|
placeholder={'Current Password'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={'mt-2 mb-3'}>
|
||||||
|
<DecoratedInput
|
||||||
|
type={HtmlInputTypes.Password}
|
||||||
|
placeholder={'New Password'}
|
||||||
|
onChange={newPassword => setNewPassword(newPassword)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={'mt-2 mb-3'}>
|
||||||
|
<DecoratedInput
|
||||||
|
type={HtmlInputTypes.Password}
|
||||||
|
placeholder={'Confirm New Password'}
|
||||||
|
onChange={newPasswordConfirmation => setNewPasswordConfirmation(newPasswordConfirmation)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FunctionalComponent } from 'preact';
|
||||||
|
|
||||||
|
export const ChangePasswordSuccess: FunctionalComponent = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={'sk-label sk-bold info'}>Your password 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
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 { ChangePasswordSuccess } from '@/preferences/panes/account/changePassword/ChangePasswordSuccess';
|
||||||
|
import { ChangePasswordForm } from '@/preferences/panes/account/changePassword/ChangePasswordForm';
|
||||||
|
import { useBeforeUnload } from '@/hooks/useBeforeUnload';
|
||||||
|
|
||||||
|
enum SubmitButtonTitles {
|
||||||
|
Default = 'Continue',
|
||||||
|
GeneratingKeys = 'Generating Keys...',
|
||||||
|
Finish = 'Finish'
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Steps {
|
||||||
|
InitialStep,
|
||||||
|
FinishStep
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onCloseDialog: () => void;
|
||||||
|
application: WebApplication;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangePassword: FunctionalComponent<Props> = ({
|
||||||
|
onCloseDialog,
|
||||||
|
application
|
||||||
|
}) => {
|
||||||
|
const [currentPassword, setCurrentPassword] = useState('');
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [newPasswordConfirmation, setNewPasswordConfirmation] = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPassword || newPassword.length === 0) {
|
||||||
|
applicationAlertService.alert(
|
||||||
|
'Please enter a new password.'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (newPassword !== newPasswordConfirmation) {
|
||||||
|
applicationAlertService.alert(
|
||||||
|
'Your new password does not match its confirmation.'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!application.getUser()?.email) {
|
||||||
|
applicationAlertService.alert(
|
||||||
|
'We don\'t have your email stored. Please log out then log back in to fix this issue.'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validate current password */
|
||||||
|
const success = await application.validateAccountPassword(currentPassword);
|
||||||
|
if (!success) {
|
||||||
|
applicationAlertService.alert(
|
||||||
|
'The current password you entered is not correct. Please try again.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetProgressState = () => {
|
||||||
|
setSubmitButtonTitle(SubmitButtonTitles.Default);
|
||||||
|
setIsContinuing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processPasswordChange = async () => {
|
||||||
|
await application.downloadBackup();
|
||||||
|
|
||||||
|
setLockContinue(true);
|
||||||
|
|
||||||
|
const response = await application.changePassword(
|
||||||
|
currentPassword,
|
||||||
|
newPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
resetProgressState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await processPasswordChange();
|
||||||
|
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 Password
|
||||||
|
</ModalDialogLabel>
|
||||||
|
<ModalDialogDescription>
|
||||||
|
{currentStep === Steps.InitialStep && (
|
||||||
|
<ChangePasswordForm
|
||||||
|
setCurrentPassword={setCurrentPassword}
|
||||||
|
setNewPassword={setNewPassword}
|
||||||
|
setNewPasswordConfirmation={setNewPasswordConfirmation}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{currentStep === Steps.FinishStep && <ChangePasswordSuccess />}
|
||||||
|
</ModalDialogDescription>
|
||||||
|
<ModalDialogButtons>
|
||||||
|
{currentStep === Steps.InitialStep && (
|
||||||
|
<Button
|
||||||
|
className='min-w-20'
|
||||||
|
type='normal'
|
||||||
|
label='Cancel'
|
||||||
|
onClick={handleDialogClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className='min-w-20'
|
||||||
|
type='primary'
|
||||||
|
label={submitButtonTitle}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
/>
|
||||||
|
</ModalDialogButtons>
|
||||||
|
</ModalDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export { default as Sync } from './Sync';
|
|
||||||
export { SubscriptionWrapper } from './subscription/SubscriptionWrapper';
|
export { SubscriptionWrapper } from './subscription/SubscriptionWrapper';
|
||||||
|
export { Sync } from './Sync';
|
||||||
|
export { Credentials } from './Credentials';
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { FunctionComponent } from 'preact';
|
|||||||
import { downloadSecretKey } from './download-secret-key';
|
import { downloadSecretKey } from './download-secret-key';
|
||||||
import { TwoFactorActivation } from './TwoFactorActivation';
|
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||||
import {
|
import {
|
||||||
TwoFactorDialog,
|
ModalDialog,
|
||||||
TwoFactorDialogLabel,
|
ModalDialogButtons,
|
||||||
TwoFactorDialogDescription,
|
ModalDialogDescription,
|
||||||
TwoFactorDialogButtons,
|
ModalDialogLabel
|
||||||
} from './TwoFactorDialog';
|
} from '@/components/shared/ModalDialog';
|
||||||
|
|
||||||
export const SaveSecretKey: FunctionComponent<{
|
export const SaveSecretKey: FunctionComponent<{
|
||||||
activation: TwoFactorActivation;
|
activation: TwoFactorActivation;
|
||||||
@@ -32,15 +32,15 @@ export const SaveSecretKey: FunctionComponent<{
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<TwoFactorDialog>
|
<ModalDialog>
|
||||||
<TwoFactorDialogLabel
|
<ModalDialogLabel
|
||||||
closeDialog={() => {
|
closeDialog={() => {
|
||||||
act.cancelActivation();
|
act.cancelActivation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Step 2 of 3 - Save secret key
|
Step 2 of 3 - Save secret key
|
||||||
</TwoFactorDialogLabel>
|
</ModalDialogLabel>
|
||||||
<TwoFactorDialogDescription>
|
<ModalDialogDescription>
|
||||||
<div className="flex-grow flex flex-col gap-2">
|
<div className="flex-grow flex flex-col gap-2">
|
||||||
<div className="flex flex-row items-center gap-1">
|
<div className="flex flex-row items-center gap-1">
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
@@ -70,8 +70,8 @@ export const SaveSecretKey: FunctionComponent<{
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TwoFactorDialogDescription>
|
</ModalDialogDescription>
|
||||||
<TwoFactorDialogButtons>
|
<ModalDialogButtons>
|
||||||
<Button
|
<Button
|
||||||
className="min-w-20"
|
className="min-w-20"
|
||||||
type="normal"
|
type="normal"
|
||||||
@@ -84,7 +84,7 @@ export const SaveSecretKey: FunctionComponent<{
|
|||||||
label="Next"
|
label="Next"
|
||||||
onClick={() => act.openVerification()}
|
onClick={() => act.openVerification()}
|
||||||
/>
|
/>
|
||||||
</TwoFactorDialogButtons>
|
</ModalDialogButtons>
|
||||||
</TwoFactorDialog>
|
</ModalDialog>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import { observer } from 'mobx-react-lite';
|
|||||||
|
|
||||||
import QRCode from 'qrcode.react';
|
import QRCode from 'qrcode.react';
|
||||||
|
|
||||||
import { DecoratedInput } from '../../../components/DecoratedInput';
|
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||||
import { IconButton } from '../../../components/IconButton';
|
import { IconButton } from '@/components/IconButton';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
import { TwoFactorActivation } from './TwoFactorActivation';
|
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||||
import {
|
|
||||||
TwoFactorDialog,
|
|
||||||
TwoFactorDialogLabel,
|
|
||||||
TwoFactorDialogDescription,
|
|
||||||
TwoFactorDialogButtons,
|
|
||||||
} from './TwoFactorDialog';
|
|
||||||
import { AuthAppInfoTooltip } from './AuthAppInfoPopup';
|
import { AuthAppInfoTooltip } from './AuthAppInfoPopup';
|
||||||
|
import {
|
||||||
|
ModalDialog,
|
||||||
|
ModalDialogButtons,
|
||||||
|
ModalDialogDescription,
|
||||||
|
ModalDialogLabel
|
||||||
|
} from '@/components/shared/ModalDialog';
|
||||||
|
|
||||||
export const ScanQRCode: FunctionComponent<{
|
export const ScanQRCode: FunctionComponent<{
|
||||||
activation: TwoFactorActivation;
|
activation: TwoFactorActivation;
|
||||||
@@ -27,15 +27,15 @@ export const ScanQRCode: FunctionComponent<{
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<TwoFactorDialog>
|
<ModalDialog>
|
||||||
<TwoFactorDialogLabel
|
<ModalDialogLabel
|
||||||
closeDialog={() => {
|
closeDialog={() => {
|
||||||
act.cancelActivation();
|
act.cancelActivation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Step 1 of 3 - Scan QR code
|
Step 1 of 3 - Scan QR code
|
||||||
</TwoFactorDialogLabel>
|
</ModalDialogLabel>
|
||||||
<TwoFactorDialogDescription>
|
<ModalDialogDescription>
|
||||||
<div className="flex flex-row gap-3 items-center">
|
<div className="flex flex-row gap-3 items-center">
|
||||||
<div className="w-25 h-25 flex items-center justify-center bg-info">
|
<div className="w-25 h-25 flex items-center justify-center bg-info">
|
||||||
<QRCode value={act.qrCode} size={100} />
|
<QRCode value={act.qrCode} size={100} />
|
||||||
@@ -61,8 +61,8 @@ export const ScanQRCode: FunctionComponent<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TwoFactorDialogDescription>
|
</ModalDialogDescription>
|
||||||
<TwoFactorDialogButtons>
|
<ModalDialogButtons>
|
||||||
<Button
|
<Button
|
||||||
className="min-w-20"
|
className="min-w-20"
|
||||||
type="normal"
|
type="normal"
|
||||||
@@ -75,7 +75,7 @@ export const ScanQRCode: FunctionComponent<{
|
|||||||
label="Next"
|
label="Next"
|
||||||
onClick={() => act.openSaveSecretKey()}
|
onClick={() => act.openSaveSecretKey()}
|
||||||
/>
|
/>
|
||||||
</TwoFactorDialogButtons>
|
</ModalDialogButtons>
|
||||||
</TwoFactorDialog>
|
</ModalDialog>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { observer } from 'mobx-react-lite';
|
|||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { TwoFactorActivation } from './TwoFactorActivation';
|
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||||
import {
|
import {
|
||||||
TwoFactorDialog,
|
ModalDialog,
|
||||||
TwoFactorDialogLabel,
|
ModalDialogButtons,
|
||||||
TwoFactorDialogDescription,
|
ModalDialogDescription,
|
||||||
TwoFactorDialogButtons,
|
ModalDialogLabel
|
||||||
} from './TwoFactorDialog';
|
} from '@/components/shared/ModalDialog';
|
||||||
|
|
||||||
export const Verification: FunctionComponent<{
|
export const Verification: FunctionComponent<{
|
||||||
activation: TwoFactorActivation;
|
activation: TwoFactorActivation;
|
||||||
@@ -16,11 +16,11 @@ export const Verification: FunctionComponent<{
|
|||||||
const borderInv =
|
const borderInv =
|
||||||
act.verificationStatus === 'invalid' ? 'border-dark-red' : '';
|
act.verificationStatus === 'invalid' ? 'border-dark-red' : '';
|
||||||
return (
|
return (
|
||||||
<TwoFactorDialog>
|
<ModalDialog>
|
||||||
<TwoFactorDialogLabel closeDialog={act.cancelActivation}>
|
<ModalDialogLabel closeDialog={act.cancelActivation}>
|
||||||
Step 3 of 3 - Verification
|
Step 3 of 3 - Verification
|
||||||
</TwoFactorDialogLabel>
|
</ModalDialogLabel>
|
||||||
<TwoFactorDialogDescription>
|
<ModalDialogDescription>
|
||||||
<div className="flex-grow flex flex-col gap-1">
|
<div className="flex-grow flex flex-col gap-1">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
@@ -42,8 +42,8 @@ export const Verification: FunctionComponent<{
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TwoFactorDialogDescription>
|
</ModalDialogDescription>
|
||||||
<TwoFactorDialogButtons>
|
<ModalDialogButtons>
|
||||||
{act.verificationStatus === 'invalid' && (
|
{act.verificationStatus === 'invalid' && (
|
||||||
<div className="text-sm color-danger">
|
<div className="text-sm color-danger">
|
||||||
Incorrect credentials, please try again.
|
Incorrect credentials, please try again.
|
||||||
@@ -61,7 +61,7 @@ export const Verification: FunctionComponent<{
|
|||||||
label="Next"
|
label="Next"
|
||||||
onClick={act.enable2FA}
|
onClick={act.enable2FA}
|
||||||
/>
|
/>
|
||||||
</TwoFactorDialogButtons>
|
</ModalDialogButtons>
|
||||||
</TwoFactorDialog>
|
</ModalDialog>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export class AutolockService extends ApplicationService {
|
|||||||
async getAutoLockInterval() {
|
async getAutoLockInterval() {
|
||||||
const interval = await this.application!.getValue(
|
const interval = await this.application!.getValue(
|
||||||
STORAGE_KEY_AUTOLOCK_INTERVAL
|
STORAGE_KEY_AUTOLOCK_INTERVAL
|
||||||
);
|
) as number;
|
||||||
if (interval) {
|
if (interval) {
|
||||||
return interval;
|
return interval;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export class ThemeManager extends ApplicationService {
|
|||||||
const cachedThemes = await this.application!.getValue(
|
const cachedThemes = await this.application!.getValue(
|
||||||
CACHED_THEMES_KEY,
|
CACHED_THEMES_KEY,
|
||||||
StorageValueModes.Nonwrapped
|
StorageValueModes.Nonwrapped
|
||||||
);
|
) as SNTheme[];
|
||||||
if (cachedThemes) {
|
if (cachedThemes) {
|
||||||
const themes = [];
|
const themes = [];
|
||||||
for (const cachedTheme of cachedThemes) {
|
for (const cachedTheme of cachedThemes) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Platform, platformFromString } from '@standardnotes/snjs';
|
import { Platform, platformFromString } from '@standardnotes/snjs';
|
||||||
import { IsDesktopPlatform, IsWebPlatform } from '@/version';
|
import { IsDesktopPlatform, IsWebPlatform } from '@/version';
|
||||||
|
import { EMAIL_REGEX } from '@Views/constants';
|
||||||
|
|
||||||
declare const process: {
|
declare const process: {
|
||||||
env: {
|
env: {
|
||||||
@@ -170,3 +171,7 @@ if (!IsWebPlatform && !IsDesktopPlatform) {
|
|||||||
export function isDesktopApplication() {
|
export function isDesktopApplication() {
|
||||||
return IsDesktopPlatform;
|
return IsDesktopPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isEmailValid = (email: string): boolean => {
|
||||||
|
return EMAIL_REGEX.test(email);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export const PANEL_NAME_NOTES = 'notes';
|
export const PANEL_NAME_NOTES = 'notes';
|
||||||
export const PANEL_NAME_TAGS = 'tags';
|
export const PANEL_NAME_TAGS = 'tags';
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
export const EMAIL_REGEX = /^([a-zA-Z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)$/;
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@@ -2035,12 +2035,12 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.2.tgz#98c5998426d9f93e06c2846c5bc7b6aef8d31063"
|
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.2.tgz#98c5998426d9f93e06c2846c5bc7b6aef8d31063"
|
||||||
integrity sha512-s/rqRyG7mrrgxJOzckPSYlB68wsRpM9jlFwDE+7zQO5/xKh+37ueWfy3RoqOgkKLey6lMpnTurofIJCvqLM3dQ==
|
integrity sha512-s/rqRyG7mrrgxJOzckPSYlB68wsRpM9jlFwDE+7zQO5/xKh+37ueWfy3RoqOgkKLey6lMpnTurofIJCvqLM3dQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/common" "^1.1.0"
|
"@standardnotes/auth" "^3.7.0"
|
||||||
|
|
||||||
"@standardnotes/features@1.6.1":
|
"@standardnotes/features@1.6.2":
|
||||||
version "1.6.1"
|
version "1.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.1.tgz#bfa227bd231dc1b54449936663731f5132b08e23"
|
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.2.tgz#98c5998426d9f93e06c2846c5bc7b6aef8d31063"
|
||||||
integrity sha512-IC6fEotUqs23JdZx96JnEgARxwYzjmPz3UwU/uVn8hHjxPev/W0nyZFRiSlj4v+dod0jSa6FTR8iLLsOQ6M4Ug==
|
integrity sha512-s/rqRyG7mrrgxJOzckPSYlB68wsRpM9jlFwDE+7zQO5/xKh+37ueWfy3RoqOgkKLey6lMpnTurofIJCvqLM3dQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/common" "^1.1.0"
|
"@standardnotes/common" "^1.1.0"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user