Verify step, integrate stateful control
This commit is contained in:
@@ -48,7 +48,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
)
|
||||
}
|
||||
|
||||
function useStatefulDialogControl<T>(initialValue?: T): StatefulControl<T> {
|
||||
export function useStatefulDialogControl<T>(
|
||||
initialValue?: T,
|
||||
): StatefulControl<T> {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const control = Dialog.useDialogControl()
|
||||
return useMemo(
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {useState} from 'react'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {wait} from '#/lib/async/wait'
|
||||
import {atoms as a, type TextStyleProp,useTheme} from '#/alf'
|
||||
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {createStaticClick,InlineLinkText} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Span,Text} from '#/components/Typography'
|
||||
|
||||
export function ResendEmailText({
|
||||
onPress,
|
||||
style,
|
||||
}: TextStyleProp & {
|
||||
onPress: () => Promise<any>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const [status, setStatus] = useState<'sending' | 'success' | null>(null)
|
||||
|
||||
const handleOnPress = async () => {
|
||||
setStatus('sending')
|
||||
try {
|
||||
await wait(1000, onPress())
|
||||
setStatus('success')
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setStatus(null)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={[a.italic, a.leading_snug, t.atoms.text_contrast_medium, style]}>
|
||||
<Trans>
|
||||
Don't see an email?{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Resend`)}
|
||||
{...createStaticClick(() => {
|
||||
handleOnPress()
|
||||
})}>
|
||||
Click here to resend.
|
||||
</InlineLinkText>
|
||||
</Trans>{' '}
|
||||
<Span style={{top: 1}}>
|
||||
{status === 'sending' ? (
|
||||
<Loader size="xs" />
|
||||
) : status === 'success' ? (
|
||||
<Check size="xs" fill={t.palette.positive_500} />
|
||||
) : null}
|
||||
</Span>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {useCallback} from 'react'
|
||||
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
export function useRefreshSession() {
|
||||
const agent = useAgent()
|
||||
|
||||
return useCallback(() => {
|
||||
return agent.resumeSession(agent.session!).catch(() => {})
|
||||
}, [agent])
|
||||
}
|
||||
@@ -1,49 +1,65 @@
|
||||
import {useCallback} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {ScreenID, Screen} from '#/components/dialogs/EmailDialog/types'
|
||||
|
||||
import {
|
||||
type StatefulControl,
|
||||
useStatefulDialogControl,
|
||||
} from '#/components/dialogs/Context'
|
||||
import {useRefreshSession} from '#/components/dialogs/EmailDialog/data/useRefreshSession'
|
||||
/*
|
||||
* Steps
|
||||
*/
|
||||
import {Update} from '#/components/dialogs/EmailDialog/screens/Update'
|
||||
import {Verify} from '#/components/dialogs/EmailDialog/screens/Verify'
|
||||
import {type Screen,ScreenID} from '#/components/dialogs/EmailDialog/types'
|
||||
|
||||
export {useDialogControl} from '#/components/Dialog'
|
||||
export {ScreenID, type Screen} from '#/components/dialogs/EmailDialog/types'
|
||||
export {
|
||||
ScreenID as EmailDialogScreenID,
|
||||
type Screen,
|
||||
} from '#/components/dialogs/EmailDialog/types'
|
||||
|
||||
export function EmailDialog({
|
||||
control,
|
||||
initialScreen,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
initialScreen: Screen
|
||||
}) {
|
||||
export function useEmailDialogControl() {
|
||||
return useStatefulDialogControl<Screen>()
|
||||
}
|
||||
|
||||
export function EmailDialog({control}: {control: StatefulControl<Screen>}) {
|
||||
const {_} = useLingui()
|
||||
const refreshSession = useRefreshSession()
|
||||
const onClose = useCallback(() => {
|
||||
/**
|
||||
* If link in any verification email is clicked, it will open a new tab.
|
||||
* When the user returns to this tab, we'll refresh their account state
|
||||
* when the dialog closes.
|
||||
*/
|
||||
refreshSession()
|
||||
}, [refreshSession])
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Outer control={control.control} onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner label={_(msg`Make adjustments to your account email settings`)} style={[{maxWidth: 400}]}>
|
||||
<Inner control={control} initialScreen={initialScreen} />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Make adjustments to your account email settings`)}
|
||||
style={[{maxWidth: 400}]}>
|
||||
<Inner control={control} />
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function Inner({
|
||||
initialScreen,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
initialScreen: Screen
|
||||
}) {
|
||||
switch (initialScreen.id) {
|
||||
function Inner({control}: {control: StatefulControl<Screen>}) {
|
||||
if (!control.value) return null
|
||||
|
||||
switch (control.value.id) {
|
||||
case ScreenID.Update: {
|
||||
return (
|
||||
<Update />
|
||||
)
|
||||
return <Update config={control.value} />
|
||||
}
|
||||
case ScreenID.Verify: {
|
||||
return <Verify config={control.value} />
|
||||
}
|
||||
default: {
|
||||
return null
|
||||
|
||||
@@ -3,26 +3,28 @@ import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
|
||||
import {Text, Span} from '#/components/Typography'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {logger} from '#/logger'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {useSession} from '#/state/session'
|
||||
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {InlineLinkText, createStaticClick} from '#/components/Link'
|
||||
import {wait} from '#/lib/async/wait'
|
||||
|
||||
import {useUpdateEmail} from '#/components/dialogs/EmailDialog/data/useUpdateEmail'
|
||||
import {logger} from '#/logger'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {ResendEmailText} from '#/components/dialogs/EmailDialog/components/ResendEmailText'
|
||||
import {TokenField} from '#/components/dialogs/EmailDialog/components/TokenField'
|
||||
import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'
|
||||
import {useRequestEmailVerification} from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification'
|
||||
import {TokenField} from '#/components/dialogs/EmailDialog/components/TokenField'
|
||||
import {useUpdateEmail} from '#/components/dialogs/EmailDialog/data/useUpdateEmail'
|
||||
import {type Screen} from '#/components/dialogs/EmailDialog/types'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
|
||||
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function Update() {
|
||||
export function Update(_props: {
|
||||
config: Exclude<Screen, {id: 'Verify' | 'EnterCode'}>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
@@ -34,28 +36,10 @@ export function Update() {
|
||||
const [tokenRequired, setTokenRequired] = useState(false)
|
||||
|
||||
const [updateStatus, setUpdateStatus] = useState<'sending' | null>(null)
|
||||
const {mutateAsync: updateEmail, isPending: isUpdateEmailPending} =
|
||||
useUpdateEmail()
|
||||
|
||||
const [resendStatus, setResendStatus] = useState<
|
||||
'sending' | 'success' | null
|
||||
>(null)
|
||||
const {mutateAsync: updateEmail} = useUpdateEmail()
|
||||
const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate()
|
||||
|
||||
const {mutateAsync: requestEmailVerification} = useRequestEmailVerification()
|
||||
|
||||
const handleResendRequestEmailUpdate = async () => {
|
||||
setResendStatus('sending')
|
||||
try {
|
||||
await wait(1000, requestEmailUpdate())
|
||||
setResendStatus('success')
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setResendStatus(null)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEmailChange = (email: string) => {
|
||||
setEmail(email)
|
||||
|
||||
@@ -144,31 +128,10 @@ export function Update() {
|
||||
onSubmitEditing={handleUpdateEmail}
|
||||
/>
|
||||
{!success && (
|
||||
<Text
|
||||
style={[
|
||||
a.italic,
|
||||
a.pt_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
Don't see an email?{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Resend`)}
|
||||
{...createStaticClick(() => {
|
||||
handleResendRequestEmailUpdate()
|
||||
})}>
|
||||
Click here to resend.
|
||||
</InlineLinkText>
|
||||
</Trans>{' '}
|
||||
<Span style={{top: 1}}>
|
||||
{resendStatus === 'sending' ? (
|
||||
<Loader size="xs" />
|
||||
) : resendStatus === 'success' ? (
|
||||
<Check size="xs" fill={t.palette.positive_500} />
|
||||
) : null}
|
||||
</Span>
|
||||
</Text>
|
||||
<ResendEmailText
|
||||
onPress={requestEmailUpdate}
|
||||
style={[a.pt_sm]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
@@ -207,7 +170,7 @@ export function Update() {
|
||||
disabled={
|
||||
!email ||
|
||||
(tokenRequired && !token) ||
|
||||
isUpdateEmailPending ||
|
||||
updateStatus === 'sending' ||
|
||||
success
|
||||
}>
|
||||
<ButtonText>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {wait} from '#/lib/async/wait'
|
||||
import {logger} from '#/logger'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {ResendEmailText} from '#/components/dialogs/EmailDialog/components/ResendEmailText'
|
||||
import {useRequestEmailVerification} from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification'
|
||||
import {type Screen} from '#/components/dialogs/EmailDialog/types'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
|
||||
import {createStaticClick,InlineLinkText} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Span,Text} from '#/components/Typography'
|
||||
|
||||
export function Verify(_props: {
|
||||
config: Exclude<Screen, {id: 'Update' | 'EnterCode'}>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const [sentEmail, setSentEmail] = useState(false)
|
||||
const [sendingStatus, setSendingStatus] = useState<'sending' | null>(null)
|
||||
const {mutateAsync: requestEmailVerification} = useRequestEmailVerification()
|
||||
|
||||
const handleRequestEmailVerification = async () => {
|
||||
setError('')
|
||||
setSendingStatus('sending')
|
||||
|
||||
try {
|
||||
await wait(1000, requestEmailVerification())
|
||||
setSentEmail(true)
|
||||
} catch (e) {
|
||||
logger.error('EmailDialog: sending verification email failed', {
|
||||
safeMessage: e,
|
||||
})
|
||||
setError(_(msg`Failed to send email, please try again.`))
|
||||
} finally {
|
||||
setSendingStatus(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.gap_lg]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_xl, a.font_heavy]}>
|
||||
{sentEmail ? (
|
||||
<>
|
||||
<Span style={{top: 1}}>
|
||||
<Check size="sm" fill={t.palette.positive_500} />
|
||||
</Span>{' '}
|
||||
<Trans>Email sent!</Trans>
|
||||
</>
|
||||
) : (
|
||||
<Trans>Verify email</Trans>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
{sentEmail ? (
|
||||
<Trans>
|
||||
We sent an email to{' '}
|
||||
<Span style={[a.font_bold, t.atoms.text]}>
|
||||
{currentAccount!.email}
|
||||
</Span>{' '}
|
||||
containing a link. Click on it to complete the email verification
|
||||
process.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
We'll send you an email to{' '}
|
||||
<Span style={[a.font_bold, t.atoms.text]}>
|
||||
{currentAccount!.email}
|
||||
</Span>{' '}
|
||||
containing a link. Click on it to complete the email verification
|
||||
process.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{sentEmail && <ResendEmailText onPress={requestEmailVerification} />}
|
||||
</View>
|
||||
|
||||
{!sentEmail && (
|
||||
<>
|
||||
{error && <Admonition type="error"> {error} </Admonition>}
|
||||
|
||||
<Button
|
||||
label={_(msg`Send verification email`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color={'primary'}
|
||||
onPress={handleRequestEmailVerification}
|
||||
disabled={sendingStatus === 'sending'}>
|
||||
<ButtonText>
|
||||
<Trans>Send email</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon
|
||||
icon={sendingStatus === 'sending' ? Loader : Envelope}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Have a code?{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Enter code`)}
|
||||
{...createStaticClick(() => {})}>
|
||||
Click here.
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -8,28 +8,31 @@ export type EmailDialogProps = {
|
||||
|
||||
export type EmailDialogInnerProps = EmailDialogProps & {}
|
||||
|
||||
export type Screen = {
|
||||
id: ScreenID.Update
|
||||
} | {
|
||||
id: ScreenID.EnterCode
|
||||
/**
|
||||
* - email was sent earlier
|
||||
* - email was _just_ sent
|
||||
*/
|
||||
instructions: ReactNode[]
|
||||
} | {
|
||||
id: ScreenID.VerifyEmail
|
||||
/**
|
||||
* - default flow
|
||||
* - new user blockers (x5)
|
||||
*/
|
||||
instructions: ReactNode[]
|
||||
// onCloseWithoutVerifying,
|
||||
// onCloseAfterVerifying,
|
||||
}
|
||||
export type Screen =
|
||||
| {
|
||||
id: ScreenID.Update
|
||||
}
|
||||
| {
|
||||
id: ScreenID.EnterCode
|
||||
/**
|
||||
* - email was sent earlier
|
||||
* - email was _just_ sent
|
||||
*/
|
||||
instructions: ReactNode[]
|
||||
}
|
||||
| {
|
||||
id: ScreenID.Verify
|
||||
/**
|
||||
* - default flow
|
||||
* - new user blockers (x5)
|
||||
*/
|
||||
instructions?: ReactNode[]
|
||||
// onCloseWithoutVerifying,
|
||||
// onCloseAfterVerifying,
|
||||
}
|
||||
|
||||
export enum ScreenID {
|
||||
Update = 'Update', // normal, instructs to click link first
|
||||
EnterCode = 'EnterCode', // if user elects to enter a code
|
||||
VerifyEmail = 'VerifyEmail', // as a separate step, instructs to click link first
|
||||
Verify = 'Verify', // as a separate step, instructs to click link first
|
||||
}
|
||||
|
||||
@@ -9,8 +9,11 @@ import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
||||
import {ChangeEmailDialog} from '#/components/dialogs/ChangeEmailDialog'
|
||||
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||
import {
|
||||
EmailDialog,
|
||||
EmailDialogScreenID,
|
||||
useEmailDialogControl,
|
||||
} from '#/components/dialogs/EmailDialog'
|
||||
import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At'
|
||||
import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake'
|
||||
import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car'
|
||||
@@ -24,7 +27,6 @@ import * as Layout from '#/components/Layout'
|
||||
import {ChangeHandleDialog} from './components/ChangeHandleDialog'
|
||||
import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
|
||||
import {ExportCarDialog} from './components/ExportCarDialog'
|
||||
import {EmailDialog, ScreenID} from '#/components/dialogs/EmailDialog'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'>
|
||||
export function AccountSettingsScreen({}: Props) {
|
||||
@@ -32,8 +34,7 @@ export function AccountSettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const {openModal} = useModalControls()
|
||||
const verifyEmailControl = useDialogControl()
|
||||
const changeEmailControl = useDialogControl()
|
||||
const emailDialogControl = useEmailDialogControl()
|
||||
const birthdayControl = useDialogControl()
|
||||
const changeHandleControl = useDialogControl()
|
||||
const exportCarControl = useDialogControl()
|
||||
@@ -76,7 +77,11 @@ export function AccountSettingsScreen({}: Props) {
|
||||
{currentAccount && !currentAccount.emailConfirmed && (
|
||||
<SettingsList.PressableItem
|
||||
label={_(msg`Verify your email`)}
|
||||
onPress={() => verifyEmailControl.open()}
|
||||
onPress={() =>
|
||||
emailDialogControl.open({
|
||||
id: EmailDialogScreenID.Verify,
|
||||
})
|
||||
}
|
||||
style={[
|
||||
a.my_xs,
|
||||
a.mx_lg,
|
||||
@@ -98,7 +103,11 @@ export function AccountSettingsScreen({}: Props) {
|
||||
)}
|
||||
<SettingsList.PressableItem
|
||||
label={_(msg`Change email`)}
|
||||
onPress={() => changeEmailControl.open()}>
|
||||
onPress={() =>
|
||||
emailDialogControl.open({
|
||||
id: EmailDialogScreenID.Update,
|
||||
})
|
||||
}>
|
||||
<SettingsList.ItemIcon icon={PencilIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Change email</Trans>
|
||||
@@ -168,20 +177,7 @@ export function AccountSettingsScreen({}: Props) {
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
|
||||
<EmailDialog
|
||||
control={changeEmailControl}
|
||||
initialScreen={{id: ScreenID.Update}}
|
||||
/>
|
||||
{/*
|
||||
<ChangeEmailDialog
|
||||
control={changeEmailControl}
|
||||
verifyEmailControl={verifyEmailControl}
|
||||
/>
|
||||
*/}
|
||||
<VerifyEmailDialog
|
||||
control={verifyEmailControl}
|
||||
changeEmailControl={changeEmailControl}
|
||||
/>
|
||||
<EmailDialog control={emailDialogControl} />
|
||||
<BirthDateSettingsDialog control={birthdayControl} />
|
||||
<ChangeHandleDialog control={changeHandleControl} />
|
||||
<ExportCarDialog control={exportCarControl} />
|
||||
|
||||
Reference in New Issue
Block a user