refactor: mobile modals (#2173)

This commit is contained in:
Aman Harwara
2023-01-24 19:26:20 +05:30
committed by GitHub
parent 6af95ddfeb
commit 42db3592b6
55 changed files with 1582 additions and 1033 deletions

View File

@@ -1,13 +1,9 @@
import ModalDialog from '@/Components/Shared/ModalDialog'
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
import Button from '@/Components/Button/Button'
import { FunctionComponent, useState } from 'react'
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
import { WebApplication } from '@/Application/Application'
import { useBeforeUnload } from '@/Hooks/useBeforeUnload'
import ChangeEmailForm from './ChangeEmailForm'
import ChangeEmailSuccess from './ChangeEmailSuccess'
import Modal, { ModalAction } from '@/Components/Shared/Modal'
enum SubmitButtonTitles {
Default = 'Continue',
@@ -37,7 +33,7 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
const applicationAlertService = application.alertService
const validateCurrentPassword = async () => {
const validateCurrentPassword = useCallback(async () => {
if (!currentPassword || currentPassword.length === 0) {
applicationAlertService.alert('Please enter your current password.').catch(console.error)
@@ -54,14 +50,14 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
}
return success
}
}, [application, applicationAlertService, currentPassword])
const resetProgressState = () => {
setSubmitButtonTitle(SubmitButtonTitles.Default)
setIsContinuing(false)
}
const processEmailChange = async () => {
const processEmailChange = useCallback(async () => {
await application.downloadBackup()
setLockContinue(true)
@@ -73,17 +69,17 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
setLockContinue(false)
return success
}
}, [application, currentPassword, newEmail])
const dismiss = () => {
const dismiss = useCallback(() => {
if (lockContinue) {
applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
} else {
onCloseDialog()
}
}
}, [applicationAlertService, lockContinue, onCloseDialog])
const handleSubmit = async () => {
const handleSubmit = useCallback(async () => {
if (lockContinue || isContinuing) {
return
}
@@ -115,31 +111,43 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
setIsContinuing(false)
setSubmitButtonTitle(SubmitButtonTitles.Finish)
setCurrentStep(Steps.FinishStep)
}
}, [currentStep, dismiss, isContinuing, lockContinue, processEmailChange, validateCurrentPassword])
const handleDialogClose = () => {
const handleDialogClose = useCallback(() => {
if (lockContinue) {
applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
} else {
onCloseDialog()
}
}
}, [applicationAlertService, lockContinue, onCloseDialog])
const modalActions = useMemo(
(): ModalAction[] => [
{
label: 'Cancel',
onClick: handleDialogClose,
type: 'cancel',
mobileSlot: 'left',
},
{
label: submitButtonTitle,
onClick: handleSubmit,
type: 'primary',
mobileSlot: 'right',
},
],
[handleDialogClose, handleSubmit, submitButtonTitle],
)
return (
<div>
<ModalDialog>
<ModalDialogLabel closeDialog={handleDialogClose}>Change Email</ModalDialogLabel>
<ModalDialogDescription className="flex flex-row items-center 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" primary label={submitButtonTitle} onClick={handleSubmit} />
</ModalDialogButtons>
</ModalDialog>
</div>
<Modal title="Change Email" close={handleDialogClose} actions={modalActions}>
<div className="px-4.5 py-4">
{currentStep === Steps.InitialStep && (
<ChangeEmailForm setNewEmail={setNewEmail} setCurrentPassword={setCurrentPassword} />
)}
{currentStep === Steps.FinishStep && <ChangeEmailSuccess />}
</div>
</Modal>
)
}

View File

@@ -10,6 +10,7 @@ import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import PasswordWizard from '@/Components/PasswordWizard/PasswordWizard'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import ModalOverlay from '@/Components/Shared/ModalOverlay'
type Props = {
application: WebApplication
@@ -33,6 +34,8 @@ const Credentials: FunctionComponent<Props> = ({ application }: Props) => {
setShouldShowPasswordWizard(false)
}, [])
const closeChangeEmailDialog = () => setIsChangeEmailDialogOpen(false)
return (
<>
<PreferencesGroup>
@@ -55,14 +58,14 @@ const Credentials: FunctionComponent<Props> = ({ application }: Props) => {
Current password was set on <span className="font-bold">{passwordCreatedOn}</span>
</Text>
<Button className="mt-3 min-w-20" label="Change password" onClick={presentPasswordWizard} />
{isChangeEmailDialogOpen && (
<ChangeEmail onCloseDialog={() => setIsChangeEmailDialogOpen(false)} application={application} />
)}
<ModalOverlay isOpen={isChangeEmailDialogOpen} onDismiss={closeChangeEmailDialog}>
<ChangeEmail onCloseDialog={closeChangeEmailDialog} application={application} />
</ModalOverlay>
</PreferencesSegment>
</PreferencesGroup>
{shouldShowPasswordWizard ? (
<ModalOverlay isOpen={shouldShowPasswordWizard} onDismiss={dismissPasswordWizard}>
<PasswordWizard application={application} dismissModal={dismissPasswordWizard} />
) : null}
</ModalOverlay>
</>
)
}

View File

@@ -1,19 +1,15 @@
import { FunctionComponent, useState } from 'react'
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
import ModalDialog from '@/Components/Shared/ModalDialog'
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
import Button from '@/Components/Button/Button'
import { WebApplication } from '@/Application/Application'
import { isEmailValid } from '@/Utils'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import InviteForm from './InviteForm'
import InviteSuccess from './InviteSuccess'
import Modal, { ModalAction } from '@/Components/Shared/Modal'
enum SubmitButtonTitles {
Default = 'Send Invite',
Default = 'Invite',
Sending = 'Sending...',
Finish = 'Finish',
}
@@ -36,7 +32,7 @@ const Invite: FunctionComponent<Props> = ({ onCloseDialog, application, subscrip
const [lockContinue, setLockContinue] = useState(false)
const [currentStep, setCurrentStep] = useState(Steps.InitialStep)
const validateInviteeEmail = async () => {
const validateInviteeEmail = useCallback(async () => {
if (!isEmailValid(inviteeEmail)) {
application.alertService
.alert('The email you entered has an invalid format. Please review your input and try again.')
@@ -46,22 +42,22 @@ const Invite: FunctionComponent<Props> = ({ onCloseDialog, application, subscrip
}
return true
}
}, [application.alertService, inviteeEmail])
const handleDialogClose = () => {
const handleDialogClose = useCallback(() => {
if (lockContinue) {
application.alertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
} else {
onCloseDialog()
}
}
}, [application.alertService, lockContinue, onCloseDialog])
const resetProgressState = () => {
setSubmitButtonTitle(SubmitButtonTitles.Default)
setIsContinuing(false)
}
const processInvite = async () => {
const processInvite = useCallback(async () => {
setLockContinue(true)
const success = await subscriptionState.sendSubscriptionInvitation(inviteeEmail)
@@ -69,9 +65,9 @@ const Invite: FunctionComponent<Props> = ({ onCloseDialog, application, subscrip
setLockContinue(false)
return success
}
}, [inviteeEmail, subscriptionState])
const handleSubmit = async () => {
const handleSubmit = useCallback(async () => {
if (lockContinue || isContinuing) {
return
}
@@ -107,21 +103,43 @@ const Invite: FunctionComponent<Props> = ({ onCloseDialog, application, subscrip
setIsContinuing(false)
setSubmitButtonTitle(SubmitButtonTitles.Finish)
setCurrentStep(Steps.FinishStep)
}
}, [
application.alertService,
currentStep,
handleDialogClose,
isContinuing,
lockContinue,
processInvite,
validateInviteeEmail,
])
const modalActions = useMemo(
(): ModalAction[] => [
{
label: submitButtonTitle,
onClick: handleSubmit,
type: 'primary',
mobileSlot: 'right',
disabled: lockContinue,
},
{
label: 'Cancel',
onClick: handleDialogClose,
type: 'cancel',
mobileSlot: 'left',
hidden: currentStep === Steps.FinishStep,
},
],
[currentStep, handleDialogClose, handleSubmit, lockContinue, submitButtonTitle],
)
return (
<div>
<ModalDialog>
<ModalDialogLabel closeDialog={handleDialogClose}>Share your Subscription</ModalDialogLabel>
<ModalDialogDescription className="flex flex-row items-center px-4.5">
{currentStep === Steps.InitialStep && <InviteForm setInviteeEmail={setInviteeEmail} />}
{currentStep === Steps.FinishStep && <InviteSuccess />}
</ModalDialogDescription>
<ModalDialogButtons className="px-4.5">
<Button className="min-w-20" primary label={submitButtonTitle} onClick={handleSubmit} />
</ModalDialogButtons>
</ModalDialog>
</div>
<Modal title="Share your Subscription" close={handleDialogClose} actions={modalActions}>
<div className="px-4.5 py-4">
{currentStep === Steps.InitialStep && <InviteForm setInviteeEmail={setInviteeEmail} />}
{currentStep === Steps.FinishStep && <InviteSuccess />}
</div>
</Modal>
)
}

View File

@@ -14,6 +14,7 @@ import InvitationsList from './InvitationsList'
import Invite from './Invite/Invite'
import Button from '@/Components/Button/Button'
import SharingStatusText from './SharingStatusText'
import ModalOverlay from '@/Components/Shared/ModalOverlay'
type Props = {
application: WebApplication
@@ -28,6 +29,8 @@ const SubscriptionSharing: FunctionComponent<Props> = ({ application, viewContro
const isSubscriptionSharingFeatureAvailable =
application.features.getFeatureStatus(FeatureIdentifier.SubscriptionSharing) === FeatureStatus.Entitled
const closeInviteDialog = () => setIsInviteDialogOpen(false)
return (
<PreferencesGroup>
<PreferencesSegment>
@@ -42,13 +45,13 @@ const SubscriptionSharing: FunctionComponent<Props> = ({ application, viewContro
{!subscriptionState.allInvitationsUsed && (
<Button className="min-w-20" label="Invite" onClick={() => setIsInviteDialogOpen(true)} />
)}
{isInviteDialogOpen && (
<ModalOverlay isOpen={isInviteDialogOpen} onDismiss={closeInviteDialog}>
<Invite
onCloseDialog={() => setIsInviteDialogOpen(false)}
onCloseDialog={closeInviteDialog}
application={application}
subscriptionState={subscriptionState}
/>
)}
</ModalOverlay>
</div>
) : (
<NoProSubscription application={application} />

View File

@@ -1,15 +1,11 @@
import Button from '@/Components/Button/Button'
import Icon from '@/Components/Icon/Icon'
import IconPicker from '@/Components/Icon/IconPicker'
import Popover from '@/Components/Popover/Popover'
import ModalDialog from '@/Components/Shared/ModalDialog'
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
import Modal, { ModalAction } from '@/Components/Shared/Modal'
import Spinner from '@/Components/Spinner/Spinner'
import { Platform, SmartViewDefaultIconName, VectorIconNameOrEmoji } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { EditSmartViewModalController } from './EditSmartViewModalController'
type Props = {
@@ -63,15 +59,40 @@ const EditSmartViewModal = ({ controller, platform }: Props) => {
}
}, [isPredicateJsonValid])
const modalActions = useMemo(
(): ModalAction[] => [
{
label: 'Delete',
onClick: deleteView,
disabled: isSaving,
type: 'destructive',
},
{
label: 'Cancel',
onClick: closeDialog,
disabled: isSaving,
type: 'cancel',
mobileSlot: 'left',
},
{
label: isSaving ? <Spinner className="h-4.5 w-4.5" /> : 'Save',
onClick: saveSmartView,
disabled: isSaving,
type: 'primary',
mobileSlot: 'right',
},
],
[closeDialog, deleteView, isSaving, saveSmartView],
)
if (!view) {
return null
}
return (
<ModalDialog>
<ModalDialogLabel closeDialog={closeDialog}>Edit Smart View "{view.title}"</ModalDialogLabel>
<ModalDialogDescription>
<div className="flex flex-col gap-4">
<Modal title={`Edit Smart View "${view.title}"`} close={closeDialog} actions={modalActions}>
<div className="px-4 py-4">
<div className="flex h-full flex-col gap-4">
<div className="flex items-center gap-2.5">
<div className="text-sm font-semibold">Title:</div>
<input
@@ -115,9 +136,9 @@ const EditSmartViewModal = ({ controller, platform }: Props) => {
</div>
</Popover>
</div>
<div className="flex flex-col gap-2.5">
<div className="flex flex-grow flex-col gap-2.5">
<div className="text-sm font-semibold">Predicate:</div>
<div className="flex flex-col overflow-hidden rounded-md border border-border">
<div className="flex flex-grow flex-col overflow-hidden rounded-md border border-border">
<textarea
className="h-full min-h-[10rem] w-full flex-grow resize-none bg-default py-1.5 px-2.5 font-mono text-sm"
value={predicateJson}
@@ -136,19 +157,8 @@ const EditSmartViewModal = ({ controller, platform }: Props) => {
</div>
</div>
</div>
</ModalDialogDescription>
<ModalDialogButtons>
<Button className="mr-auto" disabled={isSaving} onClick={deleteView} colorStyle="danger">
Delete
</Button>
<Button disabled={isSaving} onClick={saveSmartView} primary colorStyle="info">
{isSaving ? <Spinner className="h-4.5 w-4.5" /> : 'Save'}
</Button>
<Button disabled={isSaving} onClick={closeDialog}>
Cancel
</Button>
</ModalDialogButtons>
</ModalDialog>
</div>
</Modal>
)
}

View File

@@ -14,8 +14,8 @@ const SmartViewItem = ({ view, onEdit, onDelete }: Props) => {
return (
<div className="flex items-center gap-2 py-1.5">
<Icon type={view.iconString} size="custom" className="h-5.5 w-5.5" />
<span className="mr-auto text-sm">{view.title}</span>
<Icon type={view.iconString} size="custom" className="h-5.5 w-5.5 flex-shrink-0" />
<span className="mr-auto overflow-hidden text-ellipsis text-sm">{view.title}</span>
<Button small onClick={onEdit}>
Edit
</Button>

View File

@@ -15,6 +15,7 @@ import NoSubscriptionBanner from '@/Components/NoSubscriptionBanner/NoSubscripti
import { EditSmartViewModalController } from './EditSmartViewModalController'
import { STRING_DELETE_TAG } from '@/Constants/Strings'
import { confirmDialog } from '@standardnotes/ui-services'
import ModalOverlay from '@/Components/Shared/ModalOverlay'
type NewType = {
application: WebApplication
@@ -88,12 +89,15 @@ const SmartViews = ({ application, featuresController }: Props) => {
)}
</PreferencesSegment>
</PreferencesGroup>
{!!editSmartViewModalController.view && (
<ModalOverlay isOpen={!!editSmartViewModalController.view} onDismiss={editSmartViewModalController.closeDialog}>
<EditSmartViewModal controller={editSmartViewModalController} platform={application.platform} />
)}
{addSmartViewModalController.isAddingSmartView && (
</ModalOverlay>
<ModalOverlay
isOpen={addSmartViewModalController.isAddingSmartView}
onDismiss={addSmartViewModalController.closeModal}
>
<AddSmartViewModal controller={addSmartViewModalController} platform={application.platform} />
)}
</ModalOverlay>
</>
)
}

View File

@@ -1,4 +1,3 @@
import Button from '@/Components/Button/Button'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import IconButton from '@/Components/Button/IconButton'
import { observer } from 'mobx-react-lite'
@@ -7,10 +6,6 @@ import CopyButton from './CopyButton'
import Bullet from './Bullet'
import { downloadSecretKey } from './download-secret-key'
import { TwoFactorActivation } from './TwoFactorActivation'
import ModalDialog from '@/Components/Shared/ModalDialog'
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
import Icon from '@/Components/Icon/Icon'
type Props = {
@@ -18,72 +13,59 @@ type Props = {
}
const SaveSecretKey: FunctionComponent<Props> = ({ activation: act }) => {
const download = (
<IconButton
focusable={false}
title="Download"
icon="download"
className="p-0"
onClick={() => {
downloadSecretKey(act.secretKey)
}}
/>
)
return (
<ModalDialog>
<ModalDialogLabel
closeDialog={() => {
act.cancelActivation()
}}
>
Step 2 of 3 - Save secret key
</ModalDialogLabel>
<ModalDialogDescription className="h-33 flex flex-row items-center">
<div className="flex flex-grow flex-col">
<div className="flex flex-row flex-wrap items-center gap-1">
<Bullet />
<div className="text-sm">
<b>Save your secret key</b>{' '}
<a
target="_blank"
href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key"
>
somewhere safe
</a>
:
</div>
<DecoratedInput
disabled={true}
right={[<CopyButton copyValue={act.secretKey} />, download]}
value={act.secretKey}
className={{ container: 'ml-2' }}
/>
<div className="h-33 flex flex-row items-center px-4 py-4">
<div className="flex flex-grow flex-col">
<div className="flex flex-row flex-wrap items-center gap-1">
<Bullet />
<div className="text-sm">
<b>Save your secret key</b>{' '}
<a
target="_blank"
href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key"
>
somewhere safe
</a>
:
</div>
<div className="h-2" />
<div className="flex flex-row items-center">
<Bullet />
<div className="min-w-1" />
<div className="text-sm">
You can use this key to generate codes if you lose access to your authenticator app.
<br />
<a
target="_blank"
rel="noreferrer noopener"
className="underline hover:no-underline"
href="https://standardnotes.com/help/22/what-happens-if-i-lose-my-2fa-device-and-my-secret-key"
>
Learn more
<Icon className="ml-1 inline" type="open-in" size="small" />
</a>
</div>
<DecoratedInput
disabled={true}
right={[
<CopyButton copyValue={act.secretKey} />,
<IconButton
focusable={false}
title="Download"
icon="download"
className="p-0"
onClick={() => {
downloadSecretKey(act.secretKey)
}}
/>,
]}
value={act.secretKey}
className={{ container: 'ml-2' }}
/>
</div>
<div className="h-2" />
<div className="flex flex-row items-center">
<Bullet />
<div className="min-w-1" />
<div className="text-sm">
You can use this key to generate codes if you lose access to your authenticator app.
<br />
<a
target="_blank"
rel="noreferrer noopener"
className="underline hover:no-underline"
href="https://standardnotes.com/help/22/what-happens-if-i-lose-my-2fa-device-and-my-secret-key"
>
Learn more
<Icon className="ml-1 inline" type="open-in" size="small" />
</a>
</div>
</div>
</ModalDialogDescription>
<ModalDialogButtons>
<Button className="min-w-20" label="Back" onClick={() => act.openScanQRCode()} />
<Button className="min-w-20" primary label="Next" onClick={() => act.openVerification()} />
</ModalDialogButtons>
</ModalDialog>
</div>
</div>
)
}

View File

@@ -2,58 +2,53 @@ import { FunctionComponent } from 'react'
import { observer } from 'mobx-react-lite'
import QRCode from 'qrcode.react'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import Button from '@/Components/Button/Button'
import { TwoFactorActivation } from './TwoFactorActivation'
import AuthAppInfoTooltip from './AuthAppInfoPopup'
import ModalDialog from '@/Components/Shared/ModalDialog'
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
import CopyButton from './CopyButton'
import Bullet from './Bullet'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
type Props = {
activation: TwoFactorActivation
}
const ScanQRCode: FunctionComponent<Props> = ({ activation: act }) => {
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
return (
<ModalDialog>
<ModalDialogLabel closeDialog={act.cancelActivation}>Step 1 of 3 - Scan QR code</ModalDialogLabel>
<ModalDialogDescription className="h-33 flex flex-col items-center gap-5 md:flex-row">
<div className="w-25 h-25 flex items-center justify-center bg-info">
<QRCode className="border-2 border-solid border-neutral-contrast" value={act.qrCode} size={100} />
</div>
<div className="flex flex-grow flex-col gap-2">
<div className="flex flex-row items-center">
<Bullet />
<div className="min-w-1" />
<div className="text-sm">
Open your <b>authenticator app</b>.
</div>
<div className="min-w-2" />
<AuthAppInfoTooltip />
<div className="h-33 flex flex-col items-center gap-5 px-4 py-4 md:flex-row">
<div className="flex items-center justify-center bg-info">
<QRCode
className="border-2 border-solid border-neutral-contrast"
value={act.qrCode}
size={isMobileScreen ? 200 : 150}
/>
</div>
<div className="flex flex-grow flex-col gap-2">
<div className="flex flex-row items-center">
<Bullet />
<div className="min-w-1" />
<div className="text-sm">
Open your <b>authenticator app</b>.
</div>
<div className="flex flex-row items-center">
<Bullet className="mt-2 self-start" />
<div className="min-w-1" />
<div className="flex-grow text-sm">
<b>Scan this QR code</b> or <b>add this secret key</b>:
</div>
</div>
<DecoratedInput
className={{ container: 'w-92 ml-4' }}
disabled={true}
value={act.secretKey}
right={[<CopyButton copyValue={act.secretKey} />]}
/>
<div className="min-w-2" />
<AuthAppInfoTooltip />
</div>
</ModalDialogDescription>
<ModalDialogButtons>
<Button className="min-w-20" label="Cancel" onClick={() => act.cancelActivation()} />
<Button className="min-w-20" primary label="Next" onClick={() => act.openSaveSecretKey()} />
</ModalDialogButtons>
</ModalDialog>
<div className="flex flex-row items-center">
<Bullet className="mt-2 self-start" />
<div className="min-w-1" />
<div className="flex-grow text-sm">
<b>Scan this QR code</b> or <b>add this secret key</b>:
</div>
</div>
<DecoratedInput
className={{ container: 'w-92 ml-4' }}
disabled={true}
value={act.secretKey}
right={[<CopyButton copyValue={act.secretKey} />]}
/>
</div>
</div>
)
}

View File

@@ -10,6 +10,8 @@ import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/Pre
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
import { WebApplication } from '@/Application/Application'
import RecoveryCodeBanner from '@/Components/RecoveryCodeBanner/RecoveryCodeBanner'
import Modal, { ModalAction } from '@/Components/Shared/Modal'
import ModalOverlay from '@/Components/Shared/ModalOverlay'
type Props = {
auth: TwoFactorAuth
@@ -17,6 +19,74 @@ type Props = {
}
const TwoFactorAuthView: FunctionComponent<Props> = ({ auth, application }) => {
const shouldShowActivationModal = auth.status !== 'fetching' && is2FAActivation(auth.status)
const activationModalTitle = shouldShowActivationModal
? auth.status.activationStep === 'scan-qr-code'
? 'Step 1 of 3 - Scan QR code'
: auth.status.activationStep === 'save-secret-key'
? 'Step 2 of 3 - Save secret key'
: auth.status.activationStep === 'verification'
? 'Step 3 of 3 - Verification'
: auth.status.activationStep === 'success'
? 'Successfully Enabled'
: ''
: ''
const closeActivationModal = () => {
if (auth.status === 'fetching') {
return
}
if (!is2FAActivation(auth.status)) {
return
}
if (auth.status.activationStep === 'success') {
auth.status.finishActivation()
}
auth.status.cancelActivation()
}
const activationModalActions: ModalAction[] = shouldShowActivationModal
? [
{
label: 'Cancel',
onClick: auth.status.cancelActivation,
type: 'cancel',
mobileSlot: 'left',
hidden: auth.status.activationStep !== 'scan-qr-code',
},
{
label: 'Back',
onClick:
auth.status.activationStep === 'save-secret-key'
? auth.status.openScanQRCode
: auth.status.openSaveSecretKey,
type: 'cancel',
mobileSlot: 'left',
hidden: auth.status.activationStep !== 'save-secret-key' && auth.status.activationStep !== 'verification',
},
{
label: 'Next',
onClick:
auth.status.activationStep === 'scan-qr-code'
? auth.status.openSaveSecretKey
: auth.status.activationStep === 'save-secret-key'
? auth.status.openVerification
: auth.status.enable2FA,
type: 'primary',
mobileSlot: 'right',
hidden: auth.status.activationStep === 'success',
},
{
label: 'Finish',
onClick: auth.status.finishActivation,
type: 'primary',
mobileSlot: 'right',
hidden: auth.status.activationStep !== 'success',
},
]
: []
return (
<>
<PreferencesGroup>
@@ -45,9 +115,11 @@ const TwoFactorAuthView: FunctionComponent<Props> = ({ auth, application }) => {
</PreferencesSegment>
)}
</PreferencesGroup>
{auth.status !== 'fetching' && is2FAActivation(auth.status) && (
<TwoFactorActivationView activation={auth.status} />
)}
<ModalOverlay isOpen={shouldShowActivationModal} onDismiss={closeActivationModal}>
<Modal title={activationModalTitle} close={closeActivationModal} actions={activationModalActions}>
{shouldShowActivationModal && <TwoFactorActivationView activation={auth.status} />}
</Modal>
</ModalOverlay>
</>
)
}

View File

@@ -1,8 +1,3 @@
import Button from '@/Components/Button/Button'
import ModalDialog from '@/Components/Shared/ModalDialog'
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
import { Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react'
@@ -12,18 +7,12 @@ type Props = {
activation: TwoFactorActivation
}
const TwoFactorSuccess: FunctionComponent<Props> = ({ activation: act }) => (
<ModalDialog>
<ModalDialogLabel closeDialog={act.finishActivation}>Successfully Enabled</ModalDialogLabel>
<ModalDialogDescription className="flex flex-row items-center">
<div className="flex flex-row items-center justify-center pt-2">
<Subtitle>Two-factor authentication has been successfully enabled for your account.</Subtitle>
</div>
</ModalDialogDescription>
<ModalDialogButtons>
<Button className="min-w-20" primary label="Finish" onClick={act.finishActivation} />
</ModalDialogButtons>
</ModalDialog>
const TwoFactorSuccess: FunctionComponent<Props> = () => (
<div className="flex flex-row items-center px-4 py-4">
<div className="flex flex-row items-center justify-center pt-2">
<Subtitle>Two-factor authentication has been successfully enabled for your account.</Subtitle>
</div>
</div>
)
export default observer(TwoFactorSuccess)

View File

@@ -1,13 +1,8 @@
import Button from '@/Components/Button/Button'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react'
import Bullet from './Bullet'
import { TwoFactorActivation } from './TwoFactorActivation'
import ModalDialog from '@/Components/Shared/ModalDialog'
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
type Props = {
activation: TwoFactorActivation
@@ -17,47 +12,40 @@ const Verification: FunctionComponent<Props> = ({ activation: act }) => {
const secretKeyClass = act.verificationStatus === 'invalid-secret' ? 'border-danger' : ''
const authTokenClass = act.verificationStatus === 'invalid-auth-code' ? 'border-danger' : ''
return (
<ModalDialog>
<ModalDialogLabel closeDialog={act.cancelActivation}>Step 3 of 3 - Verification</ModalDialogLabel>
<ModalDialogDescription className="h-33 flex flex-row items-center">
<div className="flex flex-grow flex-col gap-4">
<div className="flex flex-row flex-wrap items-center gap-1">
<div className="text-sm">
<Bullet className="align-middle" />
<span className="align-middle">
Enter your <b>secret key</b>:
</span>
</div>
<DecoratedInput
className={{ container: `ml-2 w-full md:w-96 ${secretKeyClass}` }}
onChange={act.setInputSecretKey}
/>
</div>
<div className="flex flex-row flex-wrap items-center gap-1">
<div className="text-sm">
<Bullet className="align-middle" />
<span className="align-middle">
Verify the <b>authentication code</b> generated by your authenticator app:
</span>
</div>
<DecoratedInput
className={{ container: `ml-2 w-full md:w-30 ${authTokenClass}` }}
onChange={act.setInputOtpToken}
/>
<div className="h-33 flex flex-row items-center px-4 py-4">
<div className="flex flex-grow flex-col gap-4">
<div className="flex flex-row flex-wrap items-center gap-1">
<div className="text-sm">
<Bullet className="align-middle" />
<span className="align-middle">
Enter your <b>secret key</b>:
</span>
</div>
<DecoratedInput
className={{ container: `ml-2 w-full md:w-96 ${secretKeyClass}` }}
onChange={act.setInputSecretKey}
/>
</div>
<div className="flex flex-row flex-wrap items-center gap-1">
<div className="text-sm">
<Bullet className="align-middle" />
<span className="align-middle">
Verify the <b>authentication code</b> generated by your authenticator app:
</span>
</div>
<DecoratedInput
className={{ container: `ml-2 w-full md:w-30 ${authTokenClass}` }}
onChange={act.setInputOtpToken}
/>
</div>
</ModalDialogDescription>
<ModalDialogButtons>
{act.verificationStatus === 'invalid-auth-code' && (
<div className="flex-grow text-sm text-danger">Incorrect authentication code, please try again.</div>
)}
{act.verificationStatus === 'invalid-secret' && (
<div className="flex-grow text-sm text-danger">Incorrect secret key, please try again.</div>
)}
<Button className="min-w-20" label="Back" onClick={act.openSaveSecretKey} />
<Button className="min-w-20" primary label="Next" onClick={act.enable2FA} />
</ModalDialogButtons>
</ModalDialog>
</div>
</div>
)
}

View File

@@ -7,6 +7,8 @@ import PreferencesMenuItem from './PreferencesComponents/MenuItem'
import { PreferencesMenu } from './PreferencesMenu'
import { PreferenceId } from '@standardnotes/ui-services'
import { useApplication } from '../ApplicationProvider'
import { classNames } from '@standardnotes/snjs'
import { isIOS } from '@/Utils'
type Props = {
menu: PreferencesMenu
@@ -53,7 +55,12 @@ const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
}, [application])
return (
<div className="border-t border-border bg-default px-5 pt-2 md:border-0 md:bg-contrast md:px-0 md:py-0">
<div
className={classNames(
'border-t border-border bg-default px-5 pt-2 md:border-0 md:bg-contrast md:px-0 md:py-0',
isIOS() ? 'pb-safe-bottom' : 'pb-2 md:pb-0',
)}
>
<div className="hidden min-w-55 flex-col overflow-y-auto px-3 py-6 md:flex">
{menuItems.map((pref) => (
<PreferencesMenuItem
@@ -81,6 +88,11 @@ const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
onChange={(paneId) => {
selectPane(paneId as PreferenceId)
}}
classNameOverride={{
wrapper: 'relative',
popover: 'bottom-full w-full max-h-max',
}}
portal={false}
/>
</div>
</div>

View File

@@ -4,12 +4,13 @@ import { observer } from 'mobx-react-lite'
import { PreferencesMenu } from './PreferencesMenu'
import PreferencesCanvas from './PreferencesCanvas'
import { PreferencesProps } from './PreferencesProps'
import { isIOS } from '@/Utils'
import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile'
import { classNames } from '@standardnotes/utils'
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
import { ESCAPE_COMMAND } from '@standardnotes/ui-services'
import Modal from '../Shared/Modal'
import { AlertDialogLabel } from '@reach/alert-dialog'
import { classNames } from '@standardnotes/snjs'
import { isIOS } from '@/Utils'
const PreferencesView: FunctionComponent<PreferencesProps> = ({
application,
@@ -18,8 +19,6 @@ const PreferencesView: FunctionComponent<PreferencesProps> = ({
userProvider,
mfaProvider,
}) => {
const isDesktopScreen = useMediaQuery(MediaQueryBreakpoints.md)
const menu = useMemo(
() => new PreferencesMenu(application, viewControllerManager.enableUnfinishedFeatures),
[viewControllerManager.enableUnfinishedFeatures, application],
@@ -56,26 +55,32 @@ const PreferencesView: FunctionComponent<PreferencesProps> = ({
}, [addAndroidBackHandler, closePreferences])
return (
<div
className={classNames(
'absolute top-0 left-0 z-preferences flex h-full w-full flex-col bg-default pt-safe-top',
isIOS() ? 'pb-safe-bottom' : 'pb-2 md:pb-0',
)}
style={{
top: !isDesktopScreen ? `${document.documentElement.scrollTop}px` : '',
<Modal
close={closePreferences}
title="Preferences"
className={{
content: 'md:h-full md:!max-h-full md:!w-full',
description: 'flex flex-col',
}}
customHeader={
<AlertDialogLabel
className={classNames(
'flex w-full flex-row items-center justify-between border-b border-solid border-border bg-default px-3 pb-2 md:p-3',
isIOS() ? 'pt-safe-top' : 'pt-2',
)}
>
<div className="hidden h-8 w-8 md:block" />
<h1 className="text-base font-bold md:text-lg">Your preferences for Standard Notes</h1>
<RoundIconButton
onClick={() => {
closePreferences()
}}
icon="close"
label="Close preferences"
/>
</AlertDialogLabel>
}
>
<div className="flex w-full flex-row items-center justify-between border-b border-solid border-border bg-default px-3 py-2 md:p-3">
<div className="hidden h-8 w-8 md:block" />
<h1 className="text-base font-bold md:text-lg">Your preferences for Standard Notes</h1>
<RoundIconButton
onClick={() => {
closePreferences()
}}
icon="close"
label="Close preferences"
/>
</div>
<PreferencesCanvas
menu={menu}
application={application}
@@ -84,7 +89,7 @@ const PreferencesView: FunctionComponent<PreferencesProps> = ({
userProvider={userProvider}
mfaProvider={mfaProvider}
/>
</div>
</Modal>
)
}

View File

@@ -4,6 +4,7 @@ import PreferencesView from './PreferencesView'
import { PreferencesViewWrapperProps } from './PreferencesViewWrapperProps'
import { useCommandService } from '../CommandProvider'
import { OPEN_PREFERENCES_COMMAND } from '@standardnotes/ui-services'
import ModalOverlay from '../Shared/ModalOverlay'
const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperProps> = ({
viewControllerManager,
@@ -18,18 +19,16 @@ const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperProps> = (
})
}, [commandService, viewControllerManager])
if (!viewControllerManager.preferencesController?.isOpen) {
return null
}
return (
<PreferencesView
closePreferences={() => viewControllerManager.preferencesController.closePreferences()}
application={application}
viewControllerManager={viewControllerManager}
mfaProvider={application}
userProvider={application}
/>
<ModalOverlay isOpen={viewControllerManager.preferencesController?.isOpen} className="p-0">
<PreferencesView
closePreferences={() => viewControllerManager.preferencesController.closePreferences()}
application={application}
viewControllerManager={viewControllerManager}
mfaProvider={application}
userProvider={application}
/>
</ModalOverlay>
)
}