feat: implement credentials information on Prefs -> Account pane (#632)

* feat: implement prefs -> credentials section UI (w/o backend integration)

* feat: implement credentials information on Prefs -> Account pane

- implement email changing UI (w/o backend integration)
- implement password changing UI and reuse existing change password logic
- replace 2FA dialog with shared one
- implement React hook for preventing window refresh

* fix: provide correct types

* refactor: reuse styles from stylekit, rename components and create enum for input types

* refactor: update default exports to named ones, correct texts

* chore: remove unnecessary depenedency

* chore: yarn.lock without unnecessary packages

* Revert "chore: yarn.lock without unnecessary packages"

This reverts commit 64aa75e8408b06884d6e7383180292a4a9a3e8ad.
This commit is contained in:
Vardan Hakobyan
2021-09-09 19:23:21 +04:00
committed by GitHub
parent 7b1499d75e
commit b0ed19d6a3
23 changed files with 551 additions and 108 deletions

View File

@@ -1,11 +1,14 @@
import { FunctionalComponent, ComponentChild } from 'preact';
import { HtmlInputTypes } from '@/enums';
interface Props {
type?: HtmlInputTypes;
className?: string;
disabled?: boolean;
left?: ComponentChild[];
right?: ComponentChild[];
text?: string;
placeholder?: string;
onChange?: (text: string) => void;
}
@@ -13,11 +16,13 @@ interface Props {
* Input that can be decorated on the left and right side
*/
export const DecoratedInput: FunctionalComponent<Props> = ({
type = 'text',
className = '',
disabled = false,
left,
right,
text,
placeholder = '',
onChange,
}) => {
const base =
@@ -32,10 +37,11 @@ export const DecoratedInput: FunctionalComponent<Props> = ({
{left}
<div className="flex-grow">
<input
type="text"
type={type}
className="w-full no-border color-black focus:shadow-none"
disabled={disabled}
value={text}
placeholder={placeholder}
onChange={(e) =>
onChange && onChange((e.target as HTMLInputElement).value)
}

View File

@@ -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}`} />;
};

View File

@@ -1,19 +1,9 @@
import { ComponentChildren, FunctionComponent } from 'preact';
import { IconButton } from '../../../components/IconButton';
import {
AlertDialog,
AlertDialogDescription,
AlertDialogLabel,
} from '@reach/alert-dialog';
import { useRef } from 'preact/hooks';
import { FunctionComponent } from 'preact';
import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@node_modules/@reach/alert-dialog';
import { useRef } from '@node_modules/preact/hooks';
import { IconButton } from '@/components/IconButton';
/**
* TwoFactorDialog is AlertDialog styled for 2FA
* Can be generalized but more use cases are needed
*/
export const TwoFactorDialog: FunctionComponent<{
children: ComponentChildren;
}> = ({ children }) => {
export const ModalDialog: FunctionComponent = ({ children }) => {
const ldRef = useRef<HTMLButtonElement>();
return (
@@ -34,12 +24,14 @@ export const TwoFactorDialog: FunctionComponent<{
);
};
export const TwoFactorDialogLabel: FunctionComponent<{
export const ModalDialogLabel: FunctionComponent<{
closeDialog: () => void;
}> = ({ children, closeDialog }) => (
<AlertDialogLabel className="">
<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
className="color-grey-1 h-5 w-5"
icon="close"
@@ -50,15 +42,17 @@ export const TwoFactorDialogLabel: FunctionComponent<{
</AlertDialogLabel>
);
export const TwoFactorDialogDescription: FunctionComponent = ({ children }) => (
export const ModalDialogDescription: FunctionComponent = ({ children }) => (
<AlertDialogDescription className="px-4 py-4">
{children}
</AlertDialogDescription>
);
export const TwoFactorDialogButtons: FunctionComponent = ({ children }) => (
export const ModalDialogButtons: FunctionComponent = ({ children }) => (
<>
<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>
</>
);
export default ModalDialog;

View 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'
}

View File

@@ -0,0 +1,11 @@
import { useEffect } from '@node_modules/preact/hooks';
export const useBeforeUnload = (): void => {
useEffect(() => {
window.onbeforeunload = () => true;
return () => {
window.onbeforeunload = null;
};
}, []);
};

View File

@@ -1,11 +1,12 @@
import { FunctionComponent } from 'preact';
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({
index,
length,
}) =>
index < length - 1 ? (
<hr className="h-1px w-full bg-border no-border" />
<HorizontalSeparator />
) : null;
export const PreferencesSegment: FunctionComponent = ({ children }) => (
@@ -30,7 +31,7 @@ export const PreferencesGroup: 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="w-125 max-w-125 flex flex-col gap-3">{children}</div>
</div>

View File

@@ -1,11 +1,15 @@
import { Sync } from '@/preferences/panes/account';
import { Credentials, Sync } from '@/preferences/panes/account';
import { PreferencesPane } from '@/preferences/components';
import { observer } from 'mobx-react-lite';
import { WebApplication } from '@/ui_models/application';
export const AccountPreferences = observer(({application}: {application: WebApplication}) => {
type Props = {
application: WebApplication;
}
export const AccountPreferences = observer(({application}: Props) => {
return (
<PreferencesPane>
<Credentials application={application} />
<Sync application={application} />
</PreferencesPane>
);

View File

@@ -0,0 +1,79 @@
import {
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel
} from '@/components/shared/ModalDialog';
import { FunctionalComponent } from 'preact';
import { DecoratedInput } from '@/components/DecoratedInput';
import { Button } from '@/components/Button';
import { useState } from 'preact/hooks';
import { SNAlertService } from '@node_modules/@standardnotes/snjs';
import { HtmlInputTypes } from '@/enums';
import { isEmailValid } from '@/utils';
type Props = {
onCloseDialog: () => void;
snAlert: SNAlertService['alert']
};
export const ChangeEmail: FunctionalComponent<Props> = ({ onCloseDialog, snAlert }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = () => {
let errorMessage = '';
if (email.trim() === '' || password.trim() === '') {
errorMessage = 'Some fields have not been filled out. Please fill out all fields and try again.';
} else if (!isEmailValid(email)) {
errorMessage = 'The email you entered has an invalid format. Please review your input and try again.';
}
if (errorMessage) {
snAlert(errorMessage);
return;
}
};
return (
<div>
<ModalDialog>
<ModalDialogLabel closeDialog={onCloseDialog}>
Change Email
</ModalDialogLabel>
<ModalDialogDescription>
<div className={'mt-2 mb-3'}>
<DecoratedInput
onChange={(newEmail) => {
setEmail(newEmail);
}}
text={email}
placeholder={'New Email'}
/>
</div>
<div className={'mt-2 mb-3'}>
<DecoratedInput
type={HtmlInputTypes.Password}
placeholder={'Password'}
onChange={password => setPassword(password)}
/>
</div>
</ModalDialogDescription>
<ModalDialogButtons>
<Button
className="min-w-20"
type="normal"
label="Cancel"
onClick={onCloseDialog}
/>
<Button
className="min-w-20"
type="primary"
label="Submit"
onClick={handleSubmit}
/>
</ModalDialogButtons>
</ModalDialog>
</div>
);
};

View File

@@ -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)}
snAlert={application.alertService.alert}
/>
)}
{
isChangePasswordDialogOpen && (
<ChangePassword
onCloseDialog={() => setIsChangePasswordDialogOpen(false)}
application={application}
/>
)}
</PreferencesSegment>
</PreferencesGroup>
);
});

View File

@@ -11,7 +11,7 @@ type Props = {
application: WebApplication;
};
const Sync = observer(({ application }: Props) => {
export const Sync = observer(({ application }: Props) => {
const formatLastSyncDate = (lastUpdatedDate: Date) => {
return dateToLocalizedString(lastUpdatedDate);
};
@@ -56,5 +56,3 @@ const Sync = observer(({ application }: Props) => {
</PreferencesGroup>
);
});
export default Sync;

View File

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

View File

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

View File

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

View File

@@ -1 +1,2 @@
export { default as Sync } from './Sync';
export { Sync } from './Sync';
export { Credentials } from './Credentials';

View File

@@ -6,11 +6,11 @@ import { FunctionComponent } from 'preact';
import { downloadSecretKey } from './download-secret-key';
import { TwoFactorActivation } from './TwoFactorActivation';
import {
TwoFactorDialog,
TwoFactorDialogLabel,
TwoFactorDialogDescription,
TwoFactorDialogButtons,
} from './TwoFactorDialog';
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel
} from '@/components/shared/ModalDialog';
export const SaveSecretKey: FunctionComponent<{
activation: TwoFactorActivation;
@@ -32,15 +32,15 @@ export const SaveSecretKey: FunctionComponent<{
/>
);
return (
<TwoFactorDialog>
<TwoFactorDialogLabel
<ModalDialog>
<ModalDialogLabel
closeDialog={() => {
act.cancelActivation();
}}
>
Step 2 of 3 - Save secret key
</TwoFactorDialogLabel>
<TwoFactorDialogDescription>
</ModalDialogLabel>
<ModalDialogDescription>
<div className="flex-grow flex flex-col gap-2">
<div className="flex flex-row items-center gap-1">
<div className="text-sm">
@@ -70,8 +70,8 @@ export const SaveSecretKey: FunctionComponent<{
</a>
</div>
</div>
</TwoFactorDialogDescription>
<TwoFactorDialogButtons>
</ModalDialogDescription>
<ModalDialogButtons>
<Button
className="min-w-20"
type="normal"
@@ -84,7 +84,7 @@ export const SaveSecretKey: FunctionComponent<{
label="Next"
onClick={() => act.openVerification()}
/>
</TwoFactorDialogButtons>
</TwoFactorDialog>
</ModalDialogButtons>
</ModalDialog>
);
});

View File

@@ -3,17 +3,17 @@ import { observer } from 'mobx-react-lite';
import QRCode from 'qrcode.react';
import { DecoratedInput } from '../../../components/DecoratedInput';
import { IconButton } from '../../../components/IconButton';
import { DecoratedInput } from '@/components/DecoratedInput';
import { IconButton } from '@/components/IconButton';
import { Button } from '@/components/Button';
import { TwoFactorActivation } from './TwoFactorActivation';
import {
TwoFactorDialog,
TwoFactorDialogLabel,
TwoFactorDialogDescription,
TwoFactorDialogButtons,
} from './TwoFactorDialog';
import { AuthAppInfoTooltip } from './AuthAppInfoPopup';
import {
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel
} from '@/components/shared/ModalDialog';
export const ScanQRCode: FunctionComponent<{
activation: TwoFactorActivation;
@@ -27,15 +27,15 @@ export const ScanQRCode: FunctionComponent<{
/>
);
return (
<TwoFactorDialog>
<TwoFactorDialogLabel
<ModalDialog>
<ModalDialogLabel
closeDialog={() => {
act.cancelActivation();
}}
>
Step 1 of 3 - Scan QR code
</TwoFactorDialogLabel>
<TwoFactorDialogDescription>
</ModalDialogLabel>
<ModalDialogDescription>
<div className="flex flex-row gap-3 items-center">
<div className="w-25 h-25 flex items-center justify-center bg-info">
<QRCode value={act.qrCode} size={100} />
@@ -61,8 +61,8 @@ export const ScanQRCode: FunctionComponent<{
</div>
</div>
</div>
</TwoFactorDialogDescription>
<TwoFactorDialogButtons>
</ModalDialogDescription>
<ModalDialogButtons>
<Button
className="min-w-20"
type="normal"
@@ -75,7 +75,7 @@ export const ScanQRCode: FunctionComponent<{
label="Next"
onClick={() => act.openSaveSecretKey()}
/>
</TwoFactorDialogButtons>
</TwoFactorDialog>
</ModalDialogButtons>
</ModalDialog>
);
});

View File

@@ -4,11 +4,11 @@ import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { TwoFactorActivation } from './TwoFactorActivation';
import {
TwoFactorDialog,
TwoFactorDialogLabel,
TwoFactorDialogDescription,
TwoFactorDialogButtons,
} from './TwoFactorDialog';
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel
} from '@/components/shared/ModalDialog';
export const Verification: FunctionComponent<{
activation: TwoFactorActivation;
@@ -16,11 +16,11 @@ export const Verification: FunctionComponent<{
const borderInv =
act.verificationStatus === 'invalid' ? 'border-dark-red' : '';
return (
<TwoFactorDialog>
<TwoFactorDialogLabel closeDialog={act.cancelActivation}>
<ModalDialog>
<ModalDialogLabel closeDialog={act.cancelActivation}>
Step 3 of 3 - Verification
</TwoFactorDialogLabel>
<TwoFactorDialogDescription>
</ModalDialogLabel>
<ModalDialogDescription>
<div className="flex-grow flex flex-col gap-1">
<div className="flex flex-row items-center gap-2">
<div className="text-sm">
@@ -42,8 +42,8 @@ export const Verification: FunctionComponent<{
/>
</div>
</div>
</TwoFactorDialogDescription>
<TwoFactorDialogButtons>
</ModalDialogDescription>
<ModalDialogButtons>
{act.verificationStatus === 'invalid' && (
<div className="text-sm color-danger">
Incorrect credentials, please try again.
@@ -61,7 +61,7 @@ export const Verification: FunctionComponent<{
label="Next"
onClick={act.enable2FA}
/>
</TwoFactorDialogButtons>
</TwoFactorDialog>
</ModalDialogButtons>
</ModalDialog>
);
});

View File

@@ -45,7 +45,7 @@ export class AutolockService extends ApplicationService {
async getAutoLockInterval() {
const interval = await this.application!.getValue(
STORAGE_KEY_AUTOLOCK_INTERVAL
);
) as number;
if (interval) {
return interval;
} else {

View File

@@ -148,7 +148,7 @@ export class ThemeManager extends ApplicationService {
const cachedThemes = await this.application!.getValue(
CACHED_THEMES_KEY,
StorageValueModes.Nonwrapped
);
) as SNTheme[];
if (cachedThemes) {
const themes = [];
for (const cachedTheme of cachedThemes) {

View File

@@ -1,5 +1,6 @@
import { Platform, platformFromString } from '@standardnotes/snjs';
import { IsDesktopPlatform, IsWebPlatform } from '@/version';
import { EMAIL_REGEX } from '@Views/constants';
declare const process: {
env: {
@@ -170,3 +171,7 @@ if (!IsWebPlatform && !IsDesktopPlatform) {
export function isDesktopApplication() {
return IsDesktopPlatform;
}
export const isEmailValid = (email: string): boolean => {
return EMAIL_REGEX.test(email);
};

View File

@@ -1,2 +1,4 @@
export const PANEL_NAME_NOTES = 'notes';
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])?)$/;

View File

@@ -71,8 +71,7 @@
"@reach/checkbox": "^0.13.2",
"@reach/dialog": "^0.13.0",
"@standardnotes/sncrypto-web": "1.5.2",
"@standardnotes/features": "1.6.1",
"@standardnotes/snjs": "2.12.3",
"@standardnotes/snjs": "2.13.0",
"mobx": "^6.3.2",
"mobx-react-lite": "^3.2.0",
"preact": "^10.5.12",

View File

@@ -2011,41 +2011,29 @@
prop-types "^15.7.2"
tslib "^2.1.0"
"@standardnotes/auth@3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.1.1.tgz#834701c2e14d31eb204bff90457fa05e9183464a"
integrity sha512-E9zDYZ1gJkVZBEzd7a1L2haQ4GYeH1lUrY87UmDH1AMYUHW+c0SqZ71af1fBNqGzrx3EZSXk+Qzr7RyOa6N1Mw==
"@standardnotes/auth@^3.2.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.7.0.tgz#1193f0521bd3b1c3655685c27aff894b98c7f582"
integrity sha512-lHMRyVOxF9g11MH04wcbhpErPysL721Hgrzbi6slRDqIuA+2VpECIkbAhI5WpgP5K8RjO/kZB4l0tzcRI1Mg/g==
"@standardnotes/auth@3.7.1", "@standardnotes/auth@^3.7.0":
version "3.7.1"
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.7.1.tgz#d0b1eb63f605e04ecb077fdb5ef83e3fe6db33f9"
integrity sha512-xtjAvtikLW3Xv75X/kYA1KTm8FJVPPlXvl+ofnrf/ijkIaRkbUW/3TUhMES+G5CMiG2TZv6uVn32GqJipqgQQQ==
dependencies:
"@standardnotes/common" "^1.0.0"
"@standardnotes/common" "^1.1.0"
"@standardnotes/common@1.1.0", "@standardnotes/common@^1.0.0", "@standardnotes/common@^1.1.0":
"@standardnotes/common@1.1.0", "@standardnotes/common@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.1.0.tgz#5ffb0a50f9947471e236bb66d097f153ad9a148f"
integrity sha512-Nm2IFWbMSfZDD7cnKtN+Gjic0f+PhPq/da/o4eOoUKg21VeOaQkTn+jlQKraKIs6Lmf+w9mmPNAgMc5o4hj7Lg==
"@standardnotes/domain-events@2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.0.0.tgz#6e537485054c6b32cb1d9408fab9cd132ed0fb65"
integrity sha512-4rMCGw2Gy1CLfi6SkpsH7QzbT/80g5gVGeicEHhHvLhMolAEsHXCGOjpw5/nA3YKl8B/U8s6wvaee2vIDejFLA==
"@standardnotes/domain-events@2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.1.0.tgz#a5c4379983a728a738f145aa9e76f7640c7283a2"
integrity sha512-8bCQk2V2fyWKalVWC9L8cuj2kuKLe+bTTp0xBVTDpDhWrGFzXfsI79AzWbOl/CLHJU/PWrXf1lvUgQwPwT+RlA==
dependencies:
"@standardnotes/auth" "^3.2.0"
"@standardnotes/auth" "^3.7.0"
"@standardnotes/features@1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.0.tgz#91317255bbad376670fd81fa445abc2d14fd43d9"
integrity sha512-0gN1UKpX0LZxOk7HlnLxbl7N8Drvyw36zRErFn3pSetUNQpr92Pd6qTTNNflBBtY2pwzqoP4XVB/2kZRJFAo8w==
dependencies:
"@standardnotes/common" "^1.1.0"
"@standardnotes/features@1.6.1":
version "1.6.1"
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.1.tgz#bfa227bd231dc1b54449936663731f5132b08e23"
integrity sha512-IC6fEotUqs23JdZx96JnEgARxwYzjmPz3UwU/uVn8hHjxPev/W0nyZFRiSlj4v+dod0jSa6FTR8iLLsOQ6M4Ug==
"@standardnotes/features@1.6.2":
version "1.6.2"
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.2.tgz#98c5998426d9f93e06c2846c5bc7b6aef8d31063"
integrity sha512-s/rqRyG7mrrgxJOzckPSYlB68wsRpM9jlFwDE+7zQO5/xKh+37ueWfy3RoqOgkKLey6lMpnTurofIJCvqLM3dQ==
dependencies:
"@standardnotes/common" "^1.1.0"
@@ -2067,15 +2055,15 @@
"@standardnotes/sncrypto-common" "^1.5.2"
libsodium-wrappers "^0.7.8"
"@standardnotes/snjs@2.12.1":
version "2.12.1"
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.12.1.tgz#4c2cac0e9f87d55d7d24a30f0fbe90d878869c7b"
integrity sha512-4ehV9Nviko0yLs8NUToSXQvOTcQs4XlU2e1sfSkvZZ+Z+iQZo/gR/Xc9gXhr/PvJpahecJIAnbcLpPlJ/DU9MQ==
"@standardnotes/snjs@2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.13.0.tgz#fd80dfafab839916aa8055c60909695e01752561"
integrity sha512-tm7KRFNmY0aMU0eMhBL4Rx6Q1PU9Z0JdqZv8+fYaZWXejlw/36IlW0tmUxgLonINDxweMCGqDrOX98D+Njav9Q==
dependencies:
"@standardnotes/auth" "3.1.1"
"@standardnotes/auth" "3.7.1"
"@standardnotes/common" "1.1.0"
"@standardnotes/domain-events" "2.0.0"
"@standardnotes/features" "1.6.0"
"@standardnotes/domain-events" "2.1.0"
"@standardnotes/features" "1.6.2"
"@standardnotes/settings" "1.2.0"
"@standardnotes/sncrypto-common" "1.5.2"