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 {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import * as Dialog from '#/components/Dialog'
|
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 {useDialogControl} from '#/components/Dialog'
|
||||||
|
export {ScreenID, type Screen} from '#/components/dialogs/EmailDialog/types'
|
||||||
|
|
||||||
export function EmailDialog({
|
export function EmailDialog({
|
||||||
control,
|
control,
|
||||||
|
initialScreen,
|
||||||
}: {
|
}: {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
|
initialScreen: Screen
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
@@ -17,19 +25,28 @@ export function EmailDialog({
|
|||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<Dialog.ScrollableInner label={_(msg`Make adjustments to your account email settings`)}>
|
<Dialog.ScrollableInner label={_(msg`Make adjustments to your account email settings`)} style={[{maxWidth: 400}]}>
|
||||||
<Inner control={control} />
|
<Inner control={control} initialScreen={initialScreen} />
|
||||||
|
<Dialog.Close />
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Inner({
|
function Inner({
|
||||||
control,
|
initialScreen,
|
||||||
}: {
|
}: {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
|
initialScreen: Screen
|
||||||
}) {
|
}) {
|
||||||
return (
|
switch (initialScreen.id) {
|
||||||
<Text>Hello</Text>
|
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 EmailDialogInnerProps = EmailDialogProps & {}
|
||||||
|
|
||||||
export type Screen = {
|
export type Screen = {
|
||||||
id: ScreenID.ChangeEmail
|
id: ScreenID.Update
|
||||||
} | {
|
} | {
|
||||||
id: ScreenID.EnterCode
|
id: ScreenID.EnterCode
|
||||||
/**
|
/**
|
||||||
@@ -29,7 +29,7 @@ export type Screen = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum ScreenID {
|
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
|
EnterCode = 'EnterCode', // if user elects to enter a code
|
||||||
VerifyEmail = 'VerifyEmail', // as a separate step, instructs to click link first
|
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 {ChangeHandleDialog} from './components/ChangeHandleDialog'
|
||||||
import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
|
import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
|
||||||
import {ExportCarDialog} from './components/ExportCarDialog'
|
import {ExportCarDialog} from './components/ExportCarDialog'
|
||||||
|
import {EmailDialog, ScreenID} from '#/components/dialogs/EmailDialog'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'>
|
||||||
export function AccountSettingsScreen({}: Props) {
|
export function AccountSettingsScreen({}: Props) {
|
||||||
@@ -167,10 +168,16 @@ export function AccountSettingsScreen({}: Props) {
|
|||||||
</SettingsList.Container>
|
</SettingsList.Container>
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
|
|
||||||
|
<EmailDialog
|
||||||
|
control={changeEmailControl}
|
||||||
|
initialScreen={{id: ScreenID.Update}}
|
||||||
|
/>
|
||||||
|
{/*
|
||||||
<ChangeEmailDialog
|
<ChangeEmailDialog
|
||||||
control={changeEmailControl}
|
control={changeEmailControl}
|
||||||
verifyEmailControl={verifyEmailControl}
|
verifyEmailControl={verifyEmailControl}
|
||||||
/>
|
/>
|
||||||
|
*/}
|
||||||
<VerifyEmailDialog
|
<VerifyEmailDialog
|
||||||
control={verifyEmailControl}
|
control={verifyEmailControl}
|
||||||
changeEmailControl={changeEmailControl}
|
changeEmailControl={changeEmailControl}
|
||||||
|
|||||||
Reference in New Issue
Block a user