[APP-1158] Refactor email-related dialogs (#8296)
* WIP * Update email * Fire off confirmation email after change * Verify step, integrate stateful control * Remove tentative EnterCode step * Handle token step * Handle instructions and integrate into 2FA setting * Fix load state when reusing same email * Add new state * Add 2FA screens * Clean up state in Update step * Clean up verify state, handle normal callback * Normalize convetions * Add verification reminder screen * Improve session refresh * Handle verification requirements for composer and convo * Fix lint * Do better * Couple missing translations * Format * Use listeners for easier to grok logic * Clean errors * Move to global context * [APP-1158] Gate features by email verification state (#8305) * Use new hook in all locations * Format * Seems to work, not great duplication * Wrap all open composer calls * Remove unneeded spans * Missed one * Fix handler on Conversation * Gate new chat in header * Add comment * Remove whoopsie * Format * add hackfix for dialog not showing * add prompt to accept chat btn * navigation not necessary * send back one screen, rather than home * Update comment --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com> * Clear dialog state Co-authored-by: Samuel Newman <mozzius@protonmail.com> * Update icon * Check color * Add 2FA warning * Update instructions * Fix X button * Use an effect silly goose * Update copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/dialaUpdate copyogs/EmailDialog/screens/Update.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy * Update copy * Update copy * Update copy * Update copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy * Add link back to update email from verify email dialog * Handle token field validation --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
This commit is contained in:
@@ -195,7 +195,13 @@ export function Inner({
|
||||
onDismiss={close}
|
||||
style={{display: 'flex', flexDirection: 'column'}}>
|
||||
{header}
|
||||
<View style={[gtMobile ? a.p_2xl : a.p_xl, contentContainerStyle]}>
|
||||
<View
|
||||
style={[
|
||||
gtMobile ? a.p_2xl : a.p_xl,
|
||||
a.overflow_hidden,
|
||||
a.rounded_md,
|
||||
contentContainerStyle,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
</DismissableLayer.DismissableLayer>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack'
|
||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||
import {useEmail} from '#/lib/hooks/useEmail'
|
||||
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
|
||||
@@ -30,7 +30,6 @@ import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {atoms as a, ios, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
||||
import {Loader} from '#/components/Loader'
|
||||
@@ -197,9 +196,7 @@ function Empty() {
|
||||
const confirmDialogControl = useDialogControl()
|
||||
const followersDialogControl = useDialogControl()
|
||||
const errorDialogControl = useDialogControl()
|
||||
|
||||
const {needsEmailVerification} = useEmail()
|
||||
const verifyEmailControl = useDialogControl()
|
||||
const requireEmailVerification = useRequireEmailVerification()
|
||||
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
|
||||
@@ -230,6 +227,27 @@ function Empty() {
|
||||
generateStarterPack()
|
||||
}
|
||||
|
||||
const openConfirmDialog = useCallback(() => {
|
||||
confirmDialogControl.open()
|
||||
}, [confirmDialogControl])
|
||||
const wrappedOpenConfirmDialog = requireEmailVerification(openConfirmDialog, {
|
||||
instructions: [
|
||||
<Trans key="confirm">
|
||||
Before creating a starter pack, you must first verify your email.
|
||||
</Trans>,
|
||||
],
|
||||
})
|
||||
const navToWizard = useCallback(() => {
|
||||
navigation.navigate('StarterPackWizard')
|
||||
}, [navigation])
|
||||
const wrappedNavToWizard = requireEmailVerification(navToWizard, {
|
||||
instructions: [
|
||||
<Trans key="nav">
|
||||
Before creating a starter pack, you must first verify your email.
|
||||
</Trans>,
|
||||
],
|
||||
})
|
||||
|
||||
return (
|
||||
<LinearGradientBackground
|
||||
style={[
|
||||
@@ -258,13 +276,7 @@ function Empty() {
|
||||
color="primary"
|
||||
size="small"
|
||||
disabled={isGenerating}
|
||||
onPress={() => {
|
||||
if (needsEmailVerification) {
|
||||
verifyEmailControl.open()
|
||||
} else {
|
||||
confirmDialogControl.open()
|
||||
}
|
||||
}}
|
||||
onPress={wrappedOpenConfirmDialog}
|
||||
style={{backgroundColor: 'transparent'}}>
|
||||
<ButtonText style={{color: 'white'}}>
|
||||
<Trans>Make one for me</Trans>
|
||||
@@ -277,13 +289,7 @@ function Empty() {
|
||||
color="primary"
|
||||
size="small"
|
||||
disabled={isGenerating}
|
||||
onPress={() => {
|
||||
if (needsEmailVerification) {
|
||||
verifyEmailControl.open()
|
||||
} else {
|
||||
navigation.navigate('StarterPackWizard')
|
||||
}
|
||||
}}
|
||||
onPress={wrappedNavToWizard}
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
borderColor: 'white',
|
||||
@@ -339,12 +345,6 @@ function Empty() {
|
||||
onConfirm={generate}
|
||||
confirmButtonCta={_(msg`Retry`)}
|
||||
/>
|
||||
<VerifyEmailDialog
|
||||
reasonText={_(
|
||||
msg`Before creating a starter pack, you must first verify your email.`,
|
||||
)}
|
||||
control={verifyEmailControl}
|
||||
/>
|
||||
</LinearGradientBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {createContext, useContext, useMemo, useState} from 'react'
|
||||
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {type Screen} from '#/components/dialogs/EmailDialog/types'
|
||||
|
||||
type Control = Dialog.DialogControlProps
|
||||
|
||||
@@ -15,6 +16,7 @@ type ControlsContext = {
|
||||
mutedWordsDialogControl: Control
|
||||
signinDialogControl: Control
|
||||
inAppBrowserConsentControl: StatefulControl<string>
|
||||
emailDialogControl: StatefulControl<Screen>
|
||||
}
|
||||
|
||||
const ControlsContext = createContext<ControlsContext | null>(null)
|
||||
@@ -33,14 +35,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const mutedWordsDialogControl = Dialog.useDialogControl()
|
||||
const signinDialogControl = Dialog.useDialogControl()
|
||||
const inAppBrowserConsentControl = useStatefulDialogControl<string>()
|
||||
const emailDialogControl = useStatefulDialogControl<Screen>()
|
||||
|
||||
const ctx = useMemo<ControlsContext>(
|
||||
() => ({
|
||||
mutedWordsDialogControl,
|
||||
signinDialogControl,
|
||||
inAppBrowserConsentControl,
|
||||
emailDialogControl,
|
||||
}),
|
||||
[mutedWordsDialogControl, signinDialogControl, inAppBrowserConsentControl],
|
||||
[
|
||||
mutedWordsDialogControl,
|
||||
signinDialogControl,
|
||||
inAppBrowserConsentControl,
|
||||
emailDialogControl,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -48,7 +57,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,45 @@
|
||||
import {type TextInputProps, View} 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)}`
|
||||
}
|
||||
|
||||
export function isValidCode(value?: string) {
|
||||
return Boolean(value && /^[A-Z2-7]{5}-[A-Z2-7]{5}$/.test(value))
|
||||
}
|
||||
|
||||
export function TokenField({
|
||||
value,
|
||||
onChangeText,
|
||||
onSubmitEditing,
|
||||
}: Pick<TextInputProps, 'value' | 'onChangeText' | 'onSubmitEditing'>) {
|
||||
const {_} = useLingui()
|
||||
const isInvalid = Boolean(value && value.length > 10 && !isValidCode(value))
|
||||
|
||||
const handleOnChangeText = (v: string) => {
|
||||
onChangeText?.(normalizeCode(v))
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Shield} />
|
||||
<TextField.Input
|
||||
isInvalid={isInvalid}
|
||||
label={_(msg`Confirmation code`)}
|
||||
placeholder="XXXXX-XXXXX"
|
||||
value={value}
|
||||
onChangeText={handleOnChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {useCallback, useEffect, useState} from 'react'
|
||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent} from '#/state/session'
|
||||
import {emitEmailVerified} from '#/components/dialogs/EmailDialog/events'
|
||||
|
||||
export type AccountEmailState = {
|
||||
isEmailVerified: boolean
|
||||
email2FAEnabled: boolean
|
||||
}
|
||||
|
||||
export const accountEmailStateQueryKey = ['accountEmailState'] as const
|
||||
|
||||
export function useInvalidateAccountEmailState() {
|
||||
const qc = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
return qc.invalidateQueries({
|
||||
queryKey: accountEmailStateQueryKey,
|
||||
})
|
||||
}, [qc])
|
||||
}
|
||||
|
||||
export function useUpdateAccountEmailStateQueryCache() {
|
||||
const qc = useQueryClient()
|
||||
|
||||
return useCallback(
|
||||
(data: AccountEmailState) => {
|
||||
return qc.setQueriesData(
|
||||
{
|
||||
queryKey: accountEmailStateQueryKey,
|
||||
},
|
||||
data,
|
||||
)
|
||||
},
|
||||
[qc],
|
||||
)
|
||||
}
|
||||
|
||||
export function useAccountEmailState() {
|
||||
const agent = useAgent()
|
||||
const [prevIsEmailVerified, setPrevEmailIsVerified] = useState(
|
||||
!!agent.session?.emailConfirmed,
|
||||
)
|
||||
const fallbackData: AccountEmailState = {
|
||||
isEmailVerified: !!agent.session?.emailConfirmed,
|
||||
email2FAEnabled: !!agent.session?.emailAuthFactor,
|
||||
}
|
||||
const query = useQuery<AccountEmailState>({
|
||||
enabled: !!agent.session,
|
||||
refetchOnWindowFocus: true,
|
||||
queryKey: accountEmailStateQueryKey,
|
||||
queryFn: async () => {
|
||||
// will also trigger updates to `#/state/session` data
|
||||
const {data} = await agent.resumeSession(agent.session!)
|
||||
return {
|
||||
isEmailVerified: !!data.emailConfirmed,
|
||||
email2FAEnabled: !!data.emailAuthFactor,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const state = query.data ?? fallbackData
|
||||
|
||||
/*
|
||||
* This will emit `n` times for each instance of this hook. So the listeners
|
||||
* all use `once` to prevent multiple handlers firing.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (state.isEmailVerified && !prevIsEmailVerified) {
|
||||
setPrevEmailIsVerified(true)
|
||||
emitEmailVerified()
|
||||
} else if (!state.isEmailVerified && prevIsEmailVerified) {
|
||||
setPrevEmailIsVerified(false)
|
||||
}
|
||||
}, [state, prevIsEmailVerified])
|
||||
|
||||
return state
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useUpdateAccountEmailStateQueryCache} from '#/components/dialogs/EmailDialog/data/useAccountEmailState'
|
||||
|
||||
export function useConfirmEmail() {
|
||||
const agent = useAgent()
|
||||
const {currentAccount} = useSession()
|
||||
const updateAccountEmailStateQueryCache =
|
||||
useUpdateAccountEmailStateQueryCache()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({token}: {token: string}) => {
|
||||
if (!currentAccount?.email) {
|
||||
throw new Error('No email found for the current account')
|
||||
}
|
||||
|
||||
await agent.com.atproto.server.confirmEmail({
|
||||
email: currentAccount.email,
|
||||
token: token.trim(),
|
||||
})
|
||||
const {data} = await agent.resumeSession(agent.session!)
|
||||
updateAccountEmailStateQueryCache({
|
||||
isEmailVerified: !!data.emailConfirmed,
|
||||
email2FAEnabled: !!data.emailAuthFactor,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useUpdateAccountEmailStateQueryCache} from '#/components/dialogs/EmailDialog/data/useAccountEmailState'
|
||||
|
||||
export function useManageEmail2FA() {
|
||||
const agent = useAgent()
|
||||
const {currentAccount} = useSession()
|
||||
const updateAccountEmailStateQueryCache =
|
||||
useUpdateAccountEmailStateQueryCache()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
enabled,
|
||||
token,
|
||||
}:
|
||||
| {enabled: true; token?: undefined}
|
||||
| {enabled: false; token: string}) => {
|
||||
if (!currentAccount?.email) {
|
||||
throw new Error('No email found for the current account')
|
||||
}
|
||||
|
||||
await agent.com.atproto.server.updateEmail({
|
||||
email: currentAccount.email,
|
||||
emailAuthFactor: enabled,
|
||||
token,
|
||||
})
|
||||
const {data} = await agent.resumeSession(agent.session!)
|
||||
updateAccountEmailStateQueryCache({
|
||||
isEmailVerified: !!data.emailConfirmed,
|
||||
email2FAEnabled: !!data.emailAuthFactor,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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,13 @@
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
export function useRequestEmailVerification() {
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
await agent.com.atproto.server.requestEmailConfirmation()
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {useEffect} from 'react'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
|
||||
const events = new EventEmitter<{
|
||||
emailVerified: void
|
||||
}>()
|
||||
|
||||
export function emitEmailVerified() {
|
||||
events.emit('emailVerified')
|
||||
}
|
||||
|
||||
export function useOnEmailVerified(cb: () => void) {
|
||||
useEffect(() => {
|
||||
/*
|
||||
* N.B. Use `once` here, since the event can fire multiple times for each
|
||||
* instance of `useAccountEmailState`
|
||||
*/
|
||||
events.once('emailVerified', cb)
|
||||
return () => {
|
||||
events.off('emailVerified', cb)
|
||||
}
|
||||
}, [cb])
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import {useCallback, useState} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {type StatefulControl} from '#/components/dialogs/Context'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
import {useAccountEmailState} from '#/components/dialogs/EmailDialog/data/useAccountEmailState'
|
||||
import {Manage2FA} from '#/components/dialogs/EmailDialog/screens/Manage2FA'
|
||||
import {Update} from '#/components/dialogs/EmailDialog/screens/Update'
|
||||
import {VerificationReminder} from '#/components/dialogs/EmailDialog/screens/VerificationReminder'
|
||||
import {Verify} from '#/components/dialogs/EmailDialog/screens/Verify'
|
||||
import {type Screen, ScreenID} from '#/components/dialogs/EmailDialog/types'
|
||||
|
||||
export type {Screen} from '#/components/dialogs/EmailDialog/types'
|
||||
export {ScreenID as EmailDialogScreenID} from '#/components/dialogs/EmailDialog/types'
|
||||
|
||||
export function useEmailDialogControl() {
|
||||
return useGlobalDialogsControlContext().emailDialogControl
|
||||
}
|
||||
|
||||
export function EmailDialog() {
|
||||
const {_} = useLingui()
|
||||
const emailDialogControl = useEmailDialogControl()
|
||||
const {isEmailVerified} = useAccountEmailState()
|
||||
const onClose = useCallback(() => {
|
||||
if (!isEmailVerified) {
|
||||
if (emailDialogControl.value?.id === ScreenID.Verify) {
|
||||
emailDialogControl.value.onCloseWithoutVerifying?.()
|
||||
}
|
||||
}
|
||||
emailDialogControl.clear()
|
||||
}, [isEmailVerified, emailDialogControl])
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={emailDialogControl.control} onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Make adjustments to email settings for your account`)}
|
||||
style={[{maxWidth: 400}]}>
|
||||
<Inner control={emailDialogControl} />
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function Inner({control}: {control: StatefulControl<Screen>}) {
|
||||
const [screen, showScreen] = useState(() => control.value)
|
||||
|
||||
if (!screen) return null
|
||||
|
||||
switch (screen.id) {
|
||||
case ScreenID.Update: {
|
||||
return <Update config={screen} showScreen={showScreen} />
|
||||
}
|
||||
case ScreenID.Verify: {
|
||||
return <Verify config={screen} showScreen={showScreen} />
|
||||
}
|
||||
case ScreenID.VerificationReminder: {
|
||||
return <VerificationReminder config={screen} showScreen={showScreen} />
|
||||
}
|
||||
case ScreenID.Manage2FA: {
|
||||
return <Manage2FA config={screen} showScreen={showScreen} />
|
||||
}
|
||||
default: {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import {useReducer, 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 {useCleanError} from '#/lib/hooks/useCleanError'
|
||||
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 {useDialogContext} from '#/components/Dialog'
|
||||
import {ResendEmailText} from '#/components/dialogs/EmailDialog/components/ResendEmailText'
|
||||
import {
|
||||
isValidCode,
|
||||
TokenField,
|
||||
} from '#/components/dialogs/EmailDialog/components/TokenField'
|
||||
import {useManageEmail2FA} from '#/components/dialogs/EmailDialog/data/useManageEmail2FA'
|
||||
import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {Check_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'
|
||||
|
||||
type State = {
|
||||
error: string
|
||||
step: 'email' | 'token'
|
||||
emailStatus: 'pending' | 'success' | 'error' | 'default'
|
||||
tokenStatus: 'pending' | 'success' | 'error' | 'default'
|
||||
}
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: 'setError'
|
||||
error: string
|
||||
}
|
||||
| {
|
||||
type: 'setStep'
|
||||
step: 'email' | 'token'
|
||||
}
|
||||
| {
|
||||
type: 'setEmailStatus'
|
||||
status: State['emailStatus']
|
||||
}
|
||||
| {
|
||||
type: 'setTokenStatus'
|
||||
status: State['tokenStatus']
|
||||
}
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'setError': {
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
emailStatus: 'error',
|
||||
tokenStatus: 'error',
|
||||
}
|
||||
}
|
||||
case 'setStep': {
|
||||
return {
|
||||
...state,
|
||||
error: '',
|
||||
step: action.step,
|
||||
}
|
||||
}
|
||||
case 'setEmailStatus': {
|
||||
return {
|
||||
...state,
|
||||
error: '',
|
||||
emailStatus: action.status,
|
||||
}
|
||||
}
|
||||
case 'setTokenStatus': {
|
||||
return {
|
||||
...state,
|
||||
error: '',
|
||||
tokenStatus: action.status,
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function Disable() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const cleanError = useCleanError()
|
||||
const {currentAccount} = useSession()
|
||||
const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate()
|
||||
const {mutateAsync: manageEmail2FA} = useManageEmail2FA()
|
||||
const control = useDialogContext()
|
||||
|
||||
const [token, setToken] = useState('')
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
error: '',
|
||||
step: 'email',
|
||||
emailStatus: 'default',
|
||||
tokenStatus: 'default',
|
||||
})
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
dispatch({type: 'setEmailStatus', status: 'pending'})
|
||||
try {
|
||||
await wait(1000, requestEmailUpdate())
|
||||
dispatch({type: 'setEmailStatus', status: 'success'})
|
||||
setTimeout(() => {
|
||||
dispatch({type: 'setStep', step: 'token'})
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
logger.error('Manage2FA: email update code request failed', {
|
||||
safeMessage: e,
|
||||
})
|
||||
const {clean} = cleanError(e)
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: clean || _(msg`Failed to send email, please try again.`),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleManageEmail2FA = async () => {
|
||||
if (!isValidCode(token)) {
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: _(msg`Please enter a valid code.`),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({type: 'setTokenStatus', status: 'pending'})
|
||||
|
||||
try {
|
||||
await wait(1000, manageEmail2FA({enabled: false, token}))
|
||||
dispatch({type: 'setTokenStatus', status: 'success'})
|
||||
setTimeout(() => {
|
||||
control.close()
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
logger.error('Manage2FA: disable email 2FA failed', {safeMessage: e})
|
||||
const {clean} = cleanError(e)
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: clean || _(msg`Failed to update email 2FA settings`),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_xl, a.font_heavy, a.leading_snug]}>
|
||||
<Trans>Disable email 2FA</Trans>
|
||||
</Text>
|
||||
|
||||
{state.step === 'email' ? (
|
||||
<>
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
To disable your email 2FA method, please verify your access to{' '}
|
||||
<Span style={[a.font_bold]}>{currentAccount?.email}</Span>
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.gap_lg, a.pt_sm]}>
|
||||
{state.error && <Admonition type="error">{state.error}</Admonition>}
|
||||
|
||||
<Button
|
||||
label={_(msg`Send email`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onPress={handleSendEmail}
|
||||
disabled={state.emailStatus === 'pending'}>
|
||||
<ButtonText>
|
||||
<Trans>Send email</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon
|
||||
icon={
|
||||
state.emailStatus === 'pending'
|
||||
? Loader
|
||||
: state.emailStatus === 'success'
|
||||
? Check
|
||||
: 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(() => {
|
||||
dispatch({type: 'setStep', step: 'token'})
|
||||
})}>
|
||||
Click here.
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
To disable your email 2FA method, please verify your access to{' '}
|
||||
<Span style={[a.font_bold]}>{currentAccount?.email}</Span>
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.gap_sm, a.py_sm]}>
|
||||
<TokenField
|
||||
value={token}
|
||||
onChangeText={setToken}
|
||||
onSubmitEditing={handleManageEmail2FA}
|
||||
/>
|
||||
<ResendEmailText onPress={handleSendEmail} />
|
||||
</View>
|
||||
|
||||
{state.error && <Admonition type="error">{state.error}</Admonition>}
|
||||
|
||||
<Button
|
||||
label={_(msg`Disable 2FA`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onPress={handleManageEmail2FA}
|
||||
disabled={
|
||||
!token || token.length !== 11 || state.tokenStatus === 'pending'
|
||||
}>
|
||||
<ButtonText>
|
||||
<Trans>Disable 2FA</Trans>
|
||||
</ButtonText>
|
||||
{state.tokenStatus === 'pending' ? (
|
||||
<ButtonIcon icon={Loader} />
|
||||
) : state.tokenStatus === 'success' ? (
|
||||
<ButtonIcon icon={Check} />
|
||||
) : null}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import {useReducer} 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 {useCleanError} from '#/lib/hooks/useCleanError'
|
||||
import {logger} from '#/logger'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {useDialogContext} from '#/components/Dialog'
|
||||
import {useManageEmail2FA} from '#/components/dialogs/EmailDialog/data/useManageEmail2FA'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type State = {
|
||||
error: string
|
||||
status: 'pending' | 'success' | 'error' | 'default'
|
||||
}
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: 'setError'
|
||||
error: string
|
||||
}
|
||||
| {
|
||||
type: 'setStatus'
|
||||
status: State['status']
|
||||
}
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'setError': {
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
status: 'error',
|
||||
}
|
||||
}
|
||||
case 'setStatus': {
|
||||
return {
|
||||
...state,
|
||||
error: '',
|
||||
status: action.status,
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function Enable() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const cleanError = useCleanError()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const {mutateAsync: manageEmail2FA} = useManageEmail2FA()
|
||||
const control = useDialogContext()
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
error: '',
|
||||
status: 'default',
|
||||
})
|
||||
|
||||
const handleManageEmail2FA = async () => {
|
||||
dispatch({type: 'setStatus', status: 'pending'})
|
||||
|
||||
try {
|
||||
await wait(1000, manageEmail2FA({enabled: true}))
|
||||
dispatch({type: 'setStatus', status: 'success'})
|
||||
setTimeout(() => {
|
||||
control.close()
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
logger.error('Manage2FA: enable email 2FA failed', {safeMessage: e})
|
||||
const {clean} = cleanError(e)
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: clean || _(msg`Failed to update email 2FA settings`),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.gap_lg]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_xl, a.font_heavy, a.leading_snug]}>
|
||||
<Trans>Enable email 2FA</Trans>
|
||||
</Text>
|
||||
|
||||
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Require an email code to sign in to your account.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{state.error && <Admonition type="error">{state.error}</Admonition>}
|
||||
|
||||
<View style={[a.gap_sm, gtPhone && [a.flex_row_reverse]]}>
|
||||
<Button
|
||||
label={_(msg`Enable`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onPress={handleManageEmail2FA}
|
||||
disabled={state.status === 'pending'}>
|
||||
<ButtonText>
|
||||
<Trans>Enable</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon
|
||||
position="right"
|
||||
icon={
|
||||
state.status === 'pending'
|
||||
? Loader
|
||||
: state.status === 'success'
|
||||
? Check
|
||||
: ShieldIcon
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`Cancel`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {useAccountEmailState} from '#/components/dialogs/EmailDialog/data/useAccountEmailState'
|
||||
import {Disable} from '#/components/dialogs/EmailDialog/screens/Manage2FA/Disable'
|
||||
import {Enable} from '#/components/dialogs/EmailDialog/screens/Manage2FA/Enable'
|
||||
import {
|
||||
ScreenID,
|
||||
type ScreenProps,
|
||||
} from '#/components/dialogs/EmailDialog/types'
|
||||
|
||||
export function Manage2FA({showScreen}: ScreenProps<ScreenID.Manage2FA>) {
|
||||
const {isEmailVerified, email2FAEnabled} = useAccountEmailState()
|
||||
const [requestedAction, setRequestedAction] = useState<
|
||||
'enable' | 'disable' | null
|
||||
>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmailVerified) {
|
||||
showScreen({
|
||||
id: ScreenID.Verify,
|
||||
instructions: [
|
||||
<Trans key="2fa">
|
||||
You need to verify your email address before you can enable email
|
||||
2FA.
|
||||
</Trans>,
|
||||
],
|
||||
onVerify: () => {
|
||||
showScreen({
|
||||
id: ScreenID.Manage2FA,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [isEmailVerified, showScreen])
|
||||
|
||||
/*
|
||||
* Wacky state handling so that once 2FA settings change, we don't show the
|
||||
* wrong step of this form - esb
|
||||
*/
|
||||
|
||||
if (email2FAEnabled) {
|
||||
if (!requestedAction) {
|
||||
setRequestedAction('disable')
|
||||
return <Disable />
|
||||
}
|
||||
|
||||
if (requestedAction === 'disable') {
|
||||
return <Disable />
|
||||
}
|
||||
if (requestedAction === 'enable') {
|
||||
return <Enable />
|
||||
}
|
||||
} else {
|
||||
if (!requestedAction) {
|
||||
setRequestedAction('enable')
|
||||
return <Enable />
|
||||
}
|
||||
|
||||
if (requestedAction === 'disable') {
|
||||
return <Disable />
|
||||
}
|
||||
if (requestedAction === 'enable') {
|
||||
return <Enable />
|
||||
}
|
||||
}
|
||||
|
||||
// should never happen
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
import {useReducer} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {validate as validateEmail} from 'email-validator'
|
||||
|
||||
import {wait} from '#/lib/async/wait'
|
||||
import {useCleanError} from '#/lib/hooks/useCleanError'
|
||||
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 {
|
||||
isValidCode,
|
||||
TokenField,
|
||||
} from '#/components/dialogs/EmailDialog/components/TokenField'
|
||||
import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'
|
||||
import {useRequestEmailVerification} from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification'
|
||||
import {useUpdateEmail} from '#/components/dialogs/EmailDialog/data/useUpdateEmail'
|
||||
import {
|
||||
type ScreenID,
|
||||
type ScreenProps,
|
||||
} from '#/components/dialogs/EmailDialog/types'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type State = {
|
||||
step: 'email' | 'token'
|
||||
mutationStatus: 'pending' | 'success' | 'error' | 'default'
|
||||
error: string
|
||||
emailValid: boolean
|
||||
email: string
|
||||
token: string
|
||||
}
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: 'setStep'
|
||||
step: State['step']
|
||||
}
|
||||
| {
|
||||
type: 'setError'
|
||||
error: string
|
||||
}
|
||||
| {
|
||||
type: 'setMutationStatus'
|
||||
status: State['mutationStatus']
|
||||
}
|
||||
| {
|
||||
type: 'setEmail'
|
||||
value: string
|
||||
}
|
||||
| {
|
||||
type: 'setToken'
|
||||
value: string
|
||||
}
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'setStep': {
|
||||
return {
|
||||
...state,
|
||||
step: action.step,
|
||||
}
|
||||
}
|
||||
case 'setError': {
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
mutationStatus: 'error',
|
||||
}
|
||||
}
|
||||
case 'setMutationStatus': {
|
||||
return {
|
||||
...state,
|
||||
error: '',
|
||||
mutationStatus: action.status,
|
||||
}
|
||||
}
|
||||
case 'setEmail': {
|
||||
const emailValid = validateEmail(action.value)
|
||||
return {
|
||||
...state,
|
||||
step: 'email',
|
||||
token: '',
|
||||
email: action.value,
|
||||
emailValid,
|
||||
}
|
||||
}
|
||||
case 'setToken': {
|
||||
return {
|
||||
...state,
|
||||
error: '',
|
||||
token: action.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function Update(_props: ScreenProps<ScreenID.Update>) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const cleanError = useCleanError()
|
||||
const {currentAccount} = useSession()
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
step: 'email',
|
||||
mutationStatus: 'default',
|
||||
error: '',
|
||||
email: '',
|
||||
emailValid: true,
|
||||
token: '',
|
||||
})
|
||||
|
||||
const {mutateAsync: updateEmail} = useUpdateEmail()
|
||||
const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate()
|
||||
const {mutateAsync: requestEmailVerification} = useRequestEmailVerification()
|
||||
|
||||
const handleEmailChange = (email: string) => {
|
||||
dispatch({
|
||||
type: 'setEmail',
|
||||
value: email,
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdateEmail = async () => {
|
||||
if (state.step === 'token' && !isValidCode(state.token)) {
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: _(msg`Please enter a valid code.`),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'setMutationStatus',
|
||||
status: 'pending',
|
||||
})
|
||||
|
||||
if (state.emailValid === false) {
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: _(msg`Please enter a valid email address.`),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (state.email === currentAccount!.email) {
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: _(msg`This email is already associated with your account.`),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const {status} = await wait(
|
||||
1000,
|
||||
updateEmail({
|
||||
email: state.email,
|
||||
token: state.token,
|
||||
}),
|
||||
)
|
||||
|
||||
if (status === 'tokenRequired') {
|
||||
dispatch({
|
||||
type: 'setStep',
|
||||
step: 'token',
|
||||
})
|
||||
dispatch({
|
||||
type: 'setMutationStatus',
|
||||
status: 'default',
|
||||
})
|
||||
} else if (status === 'success') {
|
||||
dispatch({
|
||||
type: 'setMutationStatus',
|
||||
status: 'success',
|
||||
})
|
||||
|
||||
try {
|
||||
// fire off a confirmation email immediately
|
||||
await requestEmailVerification()
|
||||
} catch {}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('EmailDialog: update email failed', {safeMessage: e})
|
||||
const {clean} = cleanError(e)
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: clean || _(msg`Failed to update email, please try again.`),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.gap_lg]}>
|
||||
<Text style={[a.text_xl, a.font_heavy]}>
|
||||
<Trans>Update your email</Trans>
|
||||
</Text>
|
||||
|
||||
{currentAccount?.emailAuthFactor && (
|
||||
<Admonition type="warning">
|
||||
<Trans>
|
||||
If you update your email address, email 2FA will be disabled.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
)}
|
||||
|
||||
<View style={[a.gap_md]}>
|
||||
<View>
|
||||
<Text style={[a.pb_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Please enter your new email address.</Trans>
|
||||
</Text>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Envelope} />
|
||||
<TextField.Input
|
||||
label={_(msg`New email address`)}
|
||||
placeholder={_(msg`alice@example.com`)}
|
||||
defaultValue={state.email}
|
||||
onChangeText={
|
||||
state.mutationStatus === 'success'
|
||||
? undefined
|
||||
: handleEmailChange
|
||||
}
|
||||
keyboardType="email-address"
|
||||
autoComplete="email"
|
||||
autoCapitalize="none"
|
||||
onSubmitEditing={handleUpdateEmail}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
|
||||
{state.step === 'token' && (
|
||||
<>
|
||||
<Divider />
|
||||
<View>
|
||||
<Text style={[a.text_md, a.pb_sm, a.font_bold]}>
|
||||
<Trans>Security step required</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.pb_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Please enter the security code we sent to your previous email
|
||||
address.
|
||||
</Trans>
|
||||
</Text>
|
||||
<TokenField
|
||||
value={state.token}
|
||||
onChangeText={
|
||||
state.mutationStatus === 'success'
|
||||
? undefined
|
||||
: token => {
|
||||
dispatch({
|
||||
type: 'setToken',
|
||||
value: token,
|
||||
})
|
||||
}
|
||||
}
|
||||
onSubmitEditing={handleUpdateEmail}
|
||||
/>
|
||||
{state.mutationStatus !== 'success' && (
|
||||
<ResendEmailText
|
||||
onPress={requestEmailUpdate}
|
||||
style={[a.pt_sm]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state.error && <Admonition type="error">{state.error}</Admonition>}
|
||||
</View>
|
||||
|
||||
{state.mutationStatus === 'success' ? (
|
||||
<>
|
||||
<Divider />
|
||||
<View style={[a.gap_sm]}>
|
||||
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
|
||||
<Check fill={t.palette.positive_600} size="xs" />
|
||||
<Text style={[a.text_md, a.font_heavy]}>
|
||||
<Trans>Success!</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[a.leading_snug]}>
|
||||
<Trans>
|
||||
Please click on the link in the email we just sent you to verify
|
||||
your new email address. This is an important step to allow you
|
||||
to continue enjoying all the features of Bluesky.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
label={_(msg`Update email`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onPress={handleUpdateEmail}
|
||||
disabled={
|
||||
!state.email ||
|
||||
(state.step === 'token' &&
|
||||
(!state.token || state.token.length !== 11)) ||
|
||||
state.mutationStatus === 'pending'
|
||||
}>
|
||||
<ButtonText>
|
||||
<Trans>Update email</Trans>
|
||||
</ButtonText>
|
||||
{state.mutationStatus === 'pending' && <ButtonIcon icon={Loader} />}
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {useDialogContext} from '#/components/Dialog'
|
||||
import {
|
||||
ScreenID,
|
||||
type ScreenProps,
|
||||
} from '#/components/dialogs/EmailDialog/types'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {GradientFill} from '#/components/GradientFill'
|
||||
import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function VerificationReminder({
|
||||
showScreen,
|
||||
}: ScreenProps<ScreenID.VerificationReminder>) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {gtPhone, gtMobile} = useBreakpoints()
|
||||
const control = useDialogContext()
|
||||
|
||||
const dialogPadding = gtMobile ? a.p_2xl.padding : a.p_xl.padding
|
||||
|
||||
return (
|
||||
<View style={[a.gap_lg]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
{
|
||||
top: platform({web: dialogPadding, default: a.p_2xl.padding}) * -1,
|
||||
left: dialogPadding * -1,
|
||||
right: dialogPadding * -1,
|
||||
height: 150,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.overflow_hidden,
|
||||
a.pt_md,
|
||||
t.atoms.bg_contrast_100,
|
||||
]}>
|
||||
<GradientFill gradient={tokens.gradients.primary} />
|
||||
<ShieldIcon width={64} fill="white" style={[a.z_10]} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[a.mb_xs, {height: 150 - dialogPadding}]} />
|
||||
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_xl, a.font_heavy]}>
|
||||
<Trans>Please verify your email</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Your email has not yet been verified. Please verify your email in
|
||||
order to enjoy all the features of Bluesky.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View style={[a.gap_sm, gtPhone && [a.flex_row_reverse]]}>
|
||||
<Button
|
||||
label={_(msg`Get started`)}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
onPress={() =>
|
||||
showScreen({
|
||||
id: ScreenID.Verify,
|
||||
})
|
||||
}>
|
||||
<ButtonText>
|
||||
<Trans>Get started</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`Maybe later`)}
|
||||
accessibilityHint={_(msg`Snoozes the reminder`)}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="large"
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Maybe later</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
import {useReducer} 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 {useCleanError} from '#/lib/hooks/useCleanError'
|
||||
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 {
|
||||
isValidCode,
|
||||
TokenField,
|
||||
} from '#/components/dialogs/EmailDialog/components/TokenField'
|
||||
import {useConfirmEmail} from '#/components/dialogs/EmailDialog/data/useConfirmEmail'
|
||||
import {useRequestEmailVerification} from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification'
|
||||
import {useOnEmailVerified} from '#/components/dialogs/EmailDialog/events'
|
||||
import {
|
||||
ScreenID,
|
||||
type ScreenProps,
|
||||
} 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'
|
||||
|
||||
type State = {
|
||||
step: 'email' | 'token' | 'success'
|
||||
mutationStatus: 'pending' | 'success' | 'error' | 'default'
|
||||
error: string
|
||||
token: string
|
||||
}
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: 'setStep'
|
||||
step: State['step']
|
||||
}
|
||||
| {
|
||||
type: 'setError'
|
||||
error: string
|
||||
}
|
||||
| {
|
||||
type: 'setMutationStatus'
|
||||
status: State['mutationStatus']
|
||||
}
|
||||
| {
|
||||
type: 'setToken'
|
||||
value: string
|
||||
}
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'setStep': {
|
||||
return {
|
||||
...state,
|
||||
error: '',
|
||||
mutationStatus: 'default',
|
||||
step: action.step,
|
||||
}
|
||||
}
|
||||
case 'setError': {
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
mutationStatus: 'error',
|
||||
}
|
||||
}
|
||||
case 'setMutationStatus': {
|
||||
return {
|
||||
...state,
|
||||
error: '',
|
||||
mutationStatus: action.status,
|
||||
}
|
||||
}
|
||||
case 'setToken': {
|
||||
return {
|
||||
...state,
|
||||
error: '',
|
||||
token: action.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function Verify({config, showScreen}: ScreenProps<ScreenID.Verify>) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const cleanError = useCleanError()
|
||||
const {currentAccount} = useSession()
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
step: 'email',
|
||||
mutationStatus: 'default',
|
||||
error: '',
|
||||
token: '',
|
||||
})
|
||||
|
||||
const {mutateAsync: requestEmailVerification} = useRequestEmailVerification()
|
||||
const {mutateAsync: confirmEmail} = useConfirmEmail()
|
||||
|
||||
useOnEmailVerified(() => {
|
||||
if (config.onVerify) {
|
||||
config.onVerify()
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'setStep',
|
||||
step: 'success',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const handleRequestEmailVerification = async () => {
|
||||
dispatch({
|
||||
type: 'setMutationStatus',
|
||||
status: 'pending',
|
||||
})
|
||||
|
||||
try {
|
||||
await wait(1000, requestEmailVerification())
|
||||
dispatch({
|
||||
type: 'setMutationStatus',
|
||||
status: 'success',
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error('EmailDialog: sending verification email failed', {
|
||||
safeMessage: e,
|
||||
})
|
||||
const {clean} = cleanError(e)
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: clean || _(msg`Failed to send email, please try again.`),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmEmail = async () => {
|
||||
if (!isValidCode(state.token)) {
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: _(msg`Please enter a valid code.`),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'setMutationStatus',
|
||||
status: 'pending',
|
||||
})
|
||||
|
||||
try {
|
||||
await wait(1000, confirmEmail({token: state.token}))
|
||||
dispatch({
|
||||
type: 'setStep',
|
||||
step: 'success',
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error('EmailDialog: confirming email failed', {
|
||||
safeMessage: e,
|
||||
})
|
||||
const {clean} = cleanError(e)
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: clean || _(msg`Failed to verify email, please try again.`),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (state.step === 'success') {
|
||||
return (
|
||||
<View style={[a.gap_lg]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_xl, a.font_heavy]}>
|
||||
<Span style={{top: 1}}>
|
||||
<Check size="sm" fill={t.palette.positive_600} />
|
||||
</Span>
|
||||
{' '}
|
||||
<Trans>Email verification complete!</Trans>
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
You have successfully verified your email address. You can close
|
||||
this dialog.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.gap_lg]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_xl, a.font_heavy]}>
|
||||
{state.step === 'email' ? (
|
||||
state.mutationStatus === 'success' ? (
|
||||
<>
|
||||
<Span style={{top: 1}}>
|
||||
<Check size="sm" fill={t.palette.positive_600} />
|
||||
</Span>
|
||||
{' '}
|
||||
<Trans>Email sent!</Trans>
|
||||
</>
|
||||
) : (
|
||||
<Trans>Verify your email</Trans>
|
||||
)
|
||||
) : (
|
||||
<Trans>Verify email code</Trans>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{state.step === 'email' && state.mutationStatus !== 'success' && (
|
||||
<>
|
||||
{config.instructions?.map((int, i) => (
|
||||
<Text
|
||||
key={i}
|
||||
style={[
|
||||
a.italic,
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{int}
|
||||
</Text>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
{state.step === 'email' ? (
|
||||
state.mutationStatus === 'success' ? (
|
||||
<Trans>
|
||||
We sent an email to{' '}
|
||||
<Span style={[a.font_bold, t.atoms.text]}>
|
||||
{currentAccount!.email}
|
||||
</Span>{' '}
|
||||
containing a link. Please click on it to complete the email
|
||||
verification process.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
We'll send an email to{' '}
|
||||
<Span style={[a.font_bold, t.atoms.text]}>
|
||||
{currentAccount!.email}
|
||||
</Span>{' '}
|
||||
containing a link. Please click on it to complete the email
|
||||
verification process.
|
||||
</Trans>
|
||||
)
|
||||
) : (
|
||||
<Trans>
|
||||
Please enter the code we sent to{' '}
|
||||
<Span style={[a.font_bold, t.atoms.text]}>
|
||||
{currentAccount!.email}
|
||||
</Span>{' '}
|
||||
below.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{state.step === 'email' && state.mutationStatus !== 'success' && (
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
If you need to update your email,{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Click here to update your email`)}
|
||||
{...createStaticClick(() => {
|
||||
showScreen({id: ScreenID.Update})
|
||||
})}>
|
||||
click here
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{state.step === 'email' && state.mutationStatus === 'success' && (
|
||||
<ResendEmailText onPress={requestEmailVerification} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{state.step === 'email' && state.mutationStatus !== 'success' ? (
|
||||
<>
|
||||
{state.error && <Admonition type="error">{state.error}</Admonition>}
|
||||
<Button
|
||||
label={_(msg`Send verification email`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onPress={handleRequestEmailVerification}
|
||||
disabled={state.mutationStatus === 'pending'}>
|
||||
<ButtonText>
|
||||
<Trans>Send email</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon
|
||||
icon={state.mutationStatus === 'pending' ? Loader : Envelope}
|
||||
/>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{state.step === 'email' && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Have a code?{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Enter code`)}
|
||||
{...createStaticClick(() => {
|
||||
dispatch({
|
||||
type: 'setStep',
|
||||
step: 'token',
|
||||
})
|
||||
})}>
|
||||
Click here.
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state.step === 'token' ? (
|
||||
<>
|
||||
<TokenField
|
||||
value={state.token}
|
||||
onChangeText={token => {
|
||||
dispatch({
|
||||
type: 'setToken',
|
||||
value: token,
|
||||
})
|
||||
}}
|
||||
onSubmitEditing={handleConfirmEmail}
|
||||
/>
|
||||
|
||||
{state.error && <Admonition type="error">{state.error}</Admonition>}
|
||||
|
||||
<Button
|
||||
label={_(msg`Verify code`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onPress={handleConfirmEmail}
|
||||
disabled={
|
||||
!state.token ||
|
||||
state.token.length !== 11 ||
|
||||
state.mutationStatus === 'pending'
|
||||
}>
|
||||
<ButtonText>
|
||||
<Trans>Verify code</Trans>
|
||||
</ButtonText>
|
||||
{state.mutationStatus === 'pending' && <ButtonIcon icon={Loader} />}
|
||||
</Button>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Don't have a code or need a new one?{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Click here to restart the verification process.`)}
|
||||
{...createStaticClick(() => {
|
||||
dispatch({
|
||||
type: 'setStep',
|
||||
step: 'email',
|
||||
})
|
||||
})}>
|
||||
Click here.
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {type ReactNode} from 'react'
|
||||
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
|
||||
export type EmailDialogProps = {
|
||||
control: DialogControlProps
|
||||
}
|
||||
|
||||
export type EmailDialogInnerProps = EmailDialogProps & {}
|
||||
|
||||
export type Screen =
|
||||
| {
|
||||
id: ScreenID.Update
|
||||
}
|
||||
| {
|
||||
id: ScreenID.Verify
|
||||
instructions?: ReactNode[]
|
||||
onVerify?: () => void
|
||||
onCloseWithoutVerifying?: () => void
|
||||
}
|
||||
| {
|
||||
id: ScreenID.VerificationReminder
|
||||
}
|
||||
| {
|
||||
id: ScreenID.Manage2FA
|
||||
}
|
||||
|
||||
export enum ScreenID {
|
||||
Update = 'Update',
|
||||
Verify = 'Verify',
|
||||
VerificationReminder = 'VerificationReminder',
|
||||
Manage2FA = 'Manage2FA',
|
||||
}
|
||||
|
||||
export type ScreenProps<T extends ScreenID> = {
|
||||
config: Extract<Screen, {id: T}>
|
||||
showScreen: (screen: Screen) => void
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useEmail} from '#/lib/hooks/useEmail'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||
import {canBeMessaged} from '#/components/dms/util'
|
||||
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
|
||||
|
||||
@@ -26,8 +24,7 @@ export function MessageProfileButton({
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {needsEmailVerification} = useEmail()
|
||||
const verifyEmailControl = useDialogControl()
|
||||
const requireEmailVerification = useRequireEmailVerification()
|
||||
|
||||
const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did)
|
||||
const {mutate: initiateConvo} = useGetConvoForMembers({
|
||||
@@ -45,11 +42,6 @@ export function MessageProfileButton({
|
||||
return
|
||||
}
|
||||
|
||||
if (needsEmailVerification) {
|
||||
verifyEmailControl.open()
|
||||
return
|
||||
}
|
||||
|
||||
if (convoAvailability.convo) {
|
||||
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
||||
navigation.navigate('MessagesConversation', {
|
||||
@@ -59,14 +51,15 @@ export function MessageProfileButton({
|
||||
logEvent('chat:create', {logContext: 'ProfileHeader'})
|
||||
initiateConvo([profile.did])
|
||||
}
|
||||
}, [
|
||||
needsEmailVerification,
|
||||
verifyEmailControl,
|
||||
navigation,
|
||||
profile.did,
|
||||
initiateConvo,
|
||||
convoAvailability,
|
||||
])
|
||||
}, [navigation, profile.did, initiateConvo, convoAvailability])
|
||||
|
||||
const wrappedOnPress = requireEmailVerification(onPress, {
|
||||
instructions: [
|
||||
<Trans key="message">
|
||||
Before you can message another user, you must first verify your email.
|
||||
</Trans>,
|
||||
],
|
||||
})
|
||||
|
||||
if (!convoAvailability) {
|
||||
// show pending state based on declaration
|
||||
@@ -102,15 +95,9 @@ export function MessageProfileButton({
|
||||
shape="round"
|
||||
label={_(msg`Message ${profile.handle}`)}
|
||||
style={[a.justify_center]}
|
||||
onPress={onPress}>
|
||||
onPress={wrappedOnPress}>
|
||||
<ButtonIcon icon={Message} size="md" />
|
||||
</Button>
|
||||
<VerifyEmailDialog
|
||||
reasonText={_(
|
||||
msg`Before you may message another user, you must first verify your email.`,
|
||||
)}
|
||||
control={verifyEmailControl}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {useCallback} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useEmail} from '#/lib/hooks/useEmail'
|
||||
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
@@ -10,9 +10,7 @@ import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
||||
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
|
||||
export function NewChat({
|
||||
@@ -24,8 +22,7 @@ export function NewChat({
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {needsEmailVerification} = useEmail()
|
||||
const verifyEmailControl = useDialogControl()
|
||||
const requireEmailVerification = useRequireEmailVerification()
|
||||
|
||||
const {mutate: createChat} = useGetConvoForMembers({
|
||||
onSuccess: data => {
|
||||
@@ -49,17 +46,22 @@ export function NewChat({
|
||||
[control, createChat],
|
||||
)
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
control.open()
|
||||
}, [control])
|
||||
const wrappedOnPress = requireEmailVerification(onPress, {
|
||||
instructions: [
|
||||
<Trans key="new-chat">
|
||||
Before you can message another user, you must first verify your email.
|
||||
</Trans>,
|
||||
],
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<FAB
|
||||
testID="newChatFAB"
|
||||
onPress={() => {
|
||||
if (needsEmailVerification) {
|
||||
verifyEmailControl.open()
|
||||
} else {
|
||||
control.open()
|
||||
}
|
||||
}}
|
||||
onPress={wrappedOnPress}
|
||||
icon={<Plus size="lg" fill={t.palette.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New chat`)}
|
||||
@@ -74,13 +76,6 @@ export function NewChat({
|
||||
sortByMessageDeclaration
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
|
||||
<VerifyEmailDialog
|
||||
reasonText={_(
|
||||
msg`Before you may message another user, you must first verify your email.`,
|
||||
)}
|
||||
control={verifyEmailControl}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user