resend code logic
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import {useMemo, useState} from 'react'
|
import {useEffect, useMemo, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {Text as NestedText, View} from 'react-native'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {wait} from '#/lib/async/wait'
|
import {wait} from '#/lib/async/wait'
|
||||||
import {getPhoneCodeFromCountryCode} from '#/lib/international-telephone-codes'
|
import {getPhoneCodeFromCountryCode} from '#/lib/international-telephone-codes'
|
||||||
|
import {clamp} from '#/lib/numbers'
|
||||||
import {cleanError, isNetworkError} from '#/lib/strings/errors'
|
import {cleanError, isNetworkError} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||||
@@ -15,6 +16,7 @@ import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/componen
|
|||||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {OTPInput} from '../components/OTPInput'
|
import {OTPInput} from '../components/OTPInput'
|
||||||
@@ -88,11 +90,14 @@ export function VerifyNumber({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const {mutate: resendCode} = useMutation({
|
const {mutate: resendCode, isPending: isResendingCode} = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
},
|
},
|
||||||
onSuccess: () => Toast.show(_(msg`Code resent`)),
|
onSuccess: () => {
|
||||||
|
dispatch({type: 'RESEND_VERIFICATION_CODE'})
|
||||||
|
Toast.show(_(msg`Code resent`))
|
||||||
|
},
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
setError(null)
|
setError(null)
|
||||||
},
|
},
|
||||||
@@ -164,9 +169,11 @@ export function VerifyNumber({
|
|||||||
<OTPStatus
|
<OTPStatus
|
||||||
error={error}
|
error={error}
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
|
isResendingCode={isResendingCode}
|
||||||
isSuccess={isSuccess}
|
isSuccess={isSuccess}
|
||||||
onResend={() => resendCode()}
|
onResend={() => resendCode()}
|
||||||
onRetry={() => verifyNumber(otpCode)}
|
onRetry={() => verifyNumber(otpCode)}
|
||||||
|
lastCodeSentAt={state.lastSentAt}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
@@ -181,9 +188,11 @@ export function VerifyNumber({
|
|||||||
function OTPStatus({
|
function OTPStatus({
|
||||||
error,
|
error,
|
||||||
isPending,
|
isPending,
|
||||||
|
isResendingCode,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
onResend,
|
onResend,
|
||||||
onRetry,
|
onRetry,
|
||||||
|
lastCodeSentAt,
|
||||||
}: {
|
}: {
|
||||||
error: {
|
error: {
|
||||||
retryable: boolean
|
retryable: boolean
|
||||||
@@ -191,13 +200,29 @@ function OTPStatus({
|
|||||||
message: string
|
message: string
|
||||||
} | null
|
} | null
|
||||||
isPending: boolean
|
isPending: boolean
|
||||||
|
isResendingCode: boolean
|
||||||
isSuccess: boolean
|
isSuccess: boolean
|
||||||
onResend: () => void
|
onResend: () => void
|
||||||
onRetry: () => void
|
onRetry: () => void
|
||||||
|
lastCodeSentAt: Date | null
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
|
const [time, setTime] = useState(Date.now())
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setTime(Date.now())
|
||||||
|
}, 1000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const timeUntilCanResend = Math.max(
|
||||||
|
0,
|
||||||
|
30000 - (time - (lastCodeSentAt?.getTime() ?? 0)),
|
||||||
|
)
|
||||||
|
const isWaiting = timeUntilCanResend > 0
|
||||||
|
|
||||||
let Icon: React.ComponentType<SVGIconProps> | null = null
|
let Icon: React.ComponentType<SVGIconProps> | null = null
|
||||||
let text = ''
|
let text = ''
|
||||||
let textColor = t.atoms.text_contrast_medium.color
|
let textColor = t.atoms.text_contrast_medium.color
|
||||||
@@ -209,7 +234,8 @@ function OTPStatus({
|
|||||||
text = _(msg`Phone verified`)
|
text = _(msg`Phone verified`)
|
||||||
textColor = t.palette.positive_500
|
textColor = t.palette.positive_500
|
||||||
} else if (isPending) {
|
} else if (isPending) {
|
||||||
text = _(msg`Wait a moment...`)
|
Icon = Loader
|
||||||
|
text = _(msg`Verifying...`)
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
Icon = WarningIcon
|
Icon = WarningIcon
|
||||||
text = error.message
|
text = error.message
|
||||||
@@ -226,7 +252,7 @@ function OTPStatus({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.w_full, a.gap_2xl, a.align_center]}>
|
<View style={[a.w_full, a.align_center]}>
|
||||||
{text && (
|
{text && (
|
||||||
<View style={[a.gap_xs, a.flex_row, a.align_center]}>
|
<View style={[a.gap_xs, a.flex_row, a.align_center]}>
|
||||||
{Icon && <Icon size="xs" color={textColor} />}
|
{Icon && <Icon size="xs" color={textColor} />}
|
||||||
@@ -247,7 +273,8 @@ function OTPStatus({
|
|||||||
size="small"
|
size="small"
|
||||||
color="secondary_inverted"
|
color="secondary_inverted"
|
||||||
label={_(msg`Retry`)}
|
label={_(msg`Retry`)}
|
||||||
onPress={onRetry}>
|
onPress={onRetry}
|
||||||
|
style={[a.mt_2xl]}>
|
||||||
<ButtonIcon icon={RetryIcon} />
|
<ButtonIcon icon={RetryIcon} />
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Retry</Trans>
|
<Trans>Retry</Trans>
|
||||||
@@ -257,13 +284,28 @@ function OTPStatus({
|
|||||||
|
|
||||||
{showResendButton && (
|
{showResendButton && (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="large"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
label={_(msg`Resend code`)}
|
label={_(msg`Resend code`)}
|
||||||
onPress={onResend}>
|
disabled={isResendingCode || isWaiting}
|
||||||
|
onPress={onResend}
|
||||||
|
style={[a.mt_2xl]}>
|
||||||
|
{isResendingCode && <ButtonIcon icon={Loader} />}
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
|
{isWaiting ? (
|
||||||
|
<Trans>
|
||||||
|
Resend code in{' '}
|
||||||
|
<NestedText style={{fontVariant: ['tabular-nums']}}>
|
||||||
|
00:
|
||||||
|
{String(
|
||||||
|
clamp(Math.round(timeUntilCanResend / 1000), 0, 30),
|
||||||
|
).padStart(2, '0')}
|
||||||
|
</NestedText>
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
<Trans>Resend code</Trans>
|
<Trans>Resend code</Trans>
|
||||||
|
)}
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export type Action =
|
|||||||
phoneNumber: string
|
phoneNumber: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'RESEND_VERIFICATION_CODE'
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'VERIFY_PHONE_NUMBER_SUCCESS'
|
type: 'VERIFY_PHONE_NUMBER_SUCCESS'
|
||||||
}
|
}
|
||||||
@@ -77,6 +80,13 @@ function reducer(state: State, action: Action): State {
|
|||||||
lastSentAt: new Date(),
|
lastSentAt: new Date(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'RESEND_VERIFICATION_CODE': {
|
||||||
|
assertCurrentStep(state, '2: verify number')
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
lastSentAt: new Date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
case 'VERIFY_PHONE_NUMBER_SUCCESS': {
|
case 'VERIFY_PHONE_NUMBER_SUCCESS': {
|
||||||
assertCurrentStep(state, '2: verify number')
|
assertCurrentStep(state, '2: verify number')
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user