Merge branch 'develop' into feature/subscription-info-in-preferences

This commit is contained in:
Antonella Sgarlatta
2021-09-13 12:05:23 -03:00
24 changed files with 691 additions and 77 deletions

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)}
application={application}
/>
)}
{
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,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>
</>
)
);
};

View File

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

View File

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

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,2 +1,3 @@
export { default as Sync } from './Sync';
export { SubscriptionWrapper } from './subscription/SubscriptionWrapper';
export { Sync } from './Sync';
export { Credentials } from './Credentials';