Update email
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import {View, TextInputProps} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
|
||||
|
||||
export function normalizeCode(value: string) {
|
||||
const normalized = value.toUpperCase().replace(/[^A-Z2-7]/g, '')
|
||||
if (normalized.length <= 5) return normalized
|
||||
return `${normalized.slice(0, 5)}-${normalized.slice(5, 10)}`
|
||||
}
|
||||
|
||||
export function TokenField({
|
||||
value,
|
||||
onChangeText,
|
||||
}: Pick<TextInputProps, 'value' | 'onChangeText' | 'onSubmitEditing'>) {
|
||||
const {_} = useLingui()
|
||||
|
||||
const handleOnChangeText = (v: string) => {
|
||||
onChangeText?.(normalizeCode(v))
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Shield} />
|
||||
<TextField.Input
|
||||
label={_(msg`Confirmation code`)}
|
||||
placeholder="XXXXX-XXXXX"
|
||||
value={value}
|
||||
onChangeText={handleOnChangeText}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
export function useRequestEmailUpdate() {
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
return (await agent.com.atproto.server.requestEmailUpdate()).data
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'
|
||||
|
||||
async function updateEmailAndRefreshSession(
|
||||
agent: ReturnType<typeof useAgent>,
|
||||
email: string,
|
||||
token?: string,
|
||||
) {
|
||||
await agent.com.atproto.server.updateEmail({email: email.trim(), token})
|
||||
await agent.resumeSession(agent.session!)
|
||||
}
|
||||
|
||||
export function useUpdateEmail() {
|
||||
const agent = useAgent()
|
||||
const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate()
|
||||
|
||||
return useMutation<
|
||||
{status: 'tokenRequired' | 'success'},
|
||||
Error,
|
||||
{email: string; token?: string}
|
||||
>({
|
||||
mutationFn: async ({email, token}: {email: string; token?: string}) => {
|
||||
if (token) {
|
||||
await updateEmailAndRefreshSession(agent, email, token)
|
||||
return {
|
||||
status: 'success',
|
||||
}
|
||||
} else {
|
||||
const {tokenRequired} = await requestEmailUpdate()
|
||||
if (tokenRequired) {
|
||||
return {
|
||||
status: 'tokenRequired',
|
||||
}
|
||||
} else {
|
||||
await updateEmailAndRefreshSession(agent, email, token)
|
||||
return {
|
||||
status: 'success',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ScreenID, Screen} from '#/components/dialogs/EmailDialog/types'
|
||||
|
||||
/*
|
||||
* Steps
|
||||
*/
|
||||
import {Update} from '#/components/dialogs/EmailDialog/screens/Update'
|
||||
|
||||
export {useDialogControl} from '#/components/Dialog'
|
||||
export {ScreenID, type Screen} from '#/components/dialogs/EmailDialog/types'
|
||||
|
||||
export function EmailDialog({
|
||||
control,
|
||||
initialScreen,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
initialScreen: Screen
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
|
||||
@@ -17,19 +25,28 @@ export function EmailDialog({
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner label={_(msg`Make adjustments to your account email settings`)}>
|
||||
<Inner control={control} />
|
||||
<Dialog.ScrollableInner label={_(msg`Make adjustments to your account email settings`)} style={[{maxWidth: 400}]}>
|
||||
<Inner control={control} initialScreen={initialScreen} />
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function Inner({
|
||||
control,
|
||||
initialScreen,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
initialScreen: Screen
|
||||
}) {
|
||||
return (
|
||||
<Text>Hello</Text>
|
||||
)
|
||||
switch (initialScreen.id) {
|
||||
case ScreenID.Update: {
|
||||
return (
|
||||
<Update />
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import {useState} from 'react'
|
||||
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 {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'
|
||||
import {TokenField} from '#/components/dialogs/EmailDialog/components/TokenField'
|
||||
|
||||
export function Update() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const [email, setEmail] = useState('')
|
||||
const [token, setToken] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [tip, setTip] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
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: requestEmailUpdate} = useRequestEmailUpdate()
|
||||
|
||||
const handleResendRequestEmailUpdate = async () => {
|
||||
setResendStatus('sending')
|
||||
try {
|
||||
await wait(1000, requestEmailUpdate())
|
||||
setResendStatus('success')
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setResendStatus(null)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEmailChange = (email: string) => {
|
||||
setEmail(email)
|
||||
|
||||
// reset if email is edited
|
||||
if (tokenRequired) {
|
||||
setToken('')
|
||||
setTokenRequired(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateEmail = async () => {
|
||||
setError('')
|
||||
setTip('')
|
||||
setUpdateStatus('sending')
|
||||
|
||||
if (email === currentAccount!.email) {
|
||||
setTip(_(msg`This email is already associated with your account.`))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const {status} = await wait(
|
||||
1000,
|
||||
updateEmail({
|
||||
email,
|
||||
token,
|
||||
}),
|
||||
)
|
||||
|
||||
if (status === 'tokenRequired') {
|
||||
setTokenRequired(true)
|
||||
} else if (status === 'success') {
|
||||
setSuccess(true)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('EmailDialog: update email failed', {safeMessage: e})
|
||||
setError(_(msg`Email updated failed, please try again.`))
|
||||
} finally {
|
||||
setUpdateStatus(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.gap_lg]}>
|
||||
<Text style={[a.text_xl, a.font_heavy]}>
|
||||
<Trans>Update email</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.gap_md]}>
|
||||
<View>
|
||||
<Text style={[a.pb_sm, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Enter your new email address below.</Trans>
|
||||
</Text>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={At} />
|
||||
<TextField.Input
|
||||
label={_(msg`New email address`)}
|
||||
placeholder={_(msg`alice@example.com`)}
|
||||
defaultValue={email}
|
||||
onChangeText={success ? undefined : handleEmailChange}
|
||||
keyboardType="email-address"
|
||||
autoComplete="email"
|
||||
onSubmitEditing={handleUpdateEmail}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
|
||||
{tokenRequired && (
|
||||
<>
|
||||
<Divider />
|
||||
<View>
|
||||
<Text style={[a.text_md, a.pb_sm, a.font_bold]}>
|
||||
<Trans>Security step required</Trans>
|
||||
</Text>
|
||||
<Text style={[a.pb_sm, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Check your email for a security code.</Trans>
|
||||
</Text>
|
||||
<TokenField
|
||||
value={token}
|
||||
onChangeText={success ? undefined : setToken}
|
||||
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>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && <Admonition type="error">{error}</Admonition>}
|
||||
|
||||
{tip && <Admonition type="tip">{tip}</Admonition>}
|
||||
</View>
|
||||
|
||||
{success ? (
|
||||
<>
|
||||
<Divider />
|
||||
<View style={[a.gap_sm]}>
|
||||
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
|
||||
<Check fill={t.palette.positive_500} size="xs" />
|
||||
<Text style={[a.text_md, a.font_heavy]}>
|
||||
<Trans>Success!</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[a.leading_snug]}>
|
||||
<Trans>
|
||||
Click on the link in the email we just sent you to verify your
|
||||
email.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
label={_(msg`Update email`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color={success ? 'secondary' : 'primary'}
|
||||
onPress={handleUpdateEmail}
|
||||
disabled={
|
||||
!email ||
|
||||
(tokenRequired && !token) ||
|
||||
isUpdateEmailPending ||
|
||||
success
|
||||
}>
|
||||
<ButtonText>
|
||||
<Trans>Update email</Trans>
|
||||
</ButtonText>
|
||||
{updateStatus === 'sending' && <ButtonIcon icon={Loader} />}
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export type EmailDialogProps = {
|
||||
export type EmailDialogInnerProps = EmailDialogProps & {}
|
||||
|
||||
export type Screen = {
|
||||
id: ScreenID.ChangeEmail
|
||||
id: ScreenID.Update
|
||||
} | {
|
||||
id: ScreenID.EnterCode
|
||||
/**
|
||||
@@ -29,7 +29,7 @@ export type Screen = {
|
||||
}
|
||||
|
||||
export enum ScreenID {
|
||||
ChangeEmail = 'ChangeEmail', // normal, instructs to click link first
|
||||
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
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ 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) {
|
||||
@@ -167,10 +168,16 @@ 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}
|
||||
|
||||
Reference in New Issue
Block a user