import {useEffect, useMemo, useState} from 'react' import {Text as NestedText, View} from 'react-native' import { AppBskyContactStartPhoneVerification, AppBskyContactVerifyPhone, } from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useMutation} from '@tanstack/react-query' import {clamp} from '#/lib/numbers' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' import {useAgent} from '#/state/session' import {OnboardingPosition} from '#/screens/Onboarding/Layout' import {atoms as a, useGutters, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate' import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck' import {type Props as SVGIconProps} from '#/components/icons/common' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {OTPInput} from '../components/OTPInput' import {constructFullPhoneNumber, prettyPhoneNumber} from '../phone-number' import {type Action, type State, useOnPressBackButton} from '../state' export function VerifyNumber({ state, dispatch, context, onSkip, }: { state: Extract dispatch: React.ActionDispatch<[Action]> context: 'Onboarding' | 'Standalone' onSkip: () => void }) { const t = useTheme() const {_} = useLingui() const ax = useAnalytics() const agent = useAgent() const gutters = useGutters([0, 'wide']) const [otpCode, setOtpCode] = useState('') const [error, setError] = useState<{ retryable: boolean isResendError: boolean message: string } | null>(null) const [prevOtpCode, setPrevOtpCode] = useState(otpCode) if (otpCode !== prevOtpCode) { setPrevOtpCode(otpCode) setError(null) } const phone = useMemo( () => constructFullPhoneNumber(state.phoneCountryCode, state.phoneNumber), [state.phoneCountryCode, state.phoneNumber], ) const prettyNumber = useMemo(() => prettyPhoneNumber(phone), [phone]) const { mutate: verifyNumber, isPending, isSuccess, } = useMutation({ mutationFn: async (code: string) => { const res = await agent.app.bsky.contact.verifyPhone({code, phone}) return res.data.token }, onSuccess: async token => { // let the success state show for a moment setTimeout(() => { dispatch({ type: 'VERIFY_PHONE_NUMBER_SUCCESS', payload: { token, }, }) }, 1000) ax.metric('contacts:phone:phoneVerified', {entryPoint: context}) }, onMutate: () => setError(null), onError: err => { setOtpCode('') if (isNetworkError(err)) { setError({ retryable: true, isResendError: false, message: _( msg`A network error occurred. Please check your internet connection.`, ), }) } else if (err instanceof AppBskyContactVerifyPhone.InvalidCodeError) { setError({ retryable: true, isResendError: true, message: _(msg`This code is invalid. Resend to get a new code.`), }) } else if (err instanceof AppBskyContactVerifyPhone.InvalidPhoneError) { setError({ retryable: false, isResendError: false, message: _( msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, ), }) } else if ( err instanceof AppBskyContactVerifyPhone.RateLimitExceededError ) { setError({ retryable: true, isResendError: false, message: _( msg`Too many attempts. Please wait a few minutes and try again.`, ), }) } else { logger.error('Verify phone number failed', {safeMessage: err}) setError({ retryable: true, isResendError: false, message: _(msg`An error occurred. ${cleanError(err)}`), }) } }, }) const {mutate: resendCode, isPending: isResendingCode} = useMutation({ mutationFn: async () => { await agent.app.bsky.contact.startPhoneVerification({phone: phone}) }, onSuccess: () => { dispatch({type: 'RESEND_VERIFICATION_CODE'}) Toast.show(_(msg`A new code has been sent`)) }, onMutate: () => { setOtpCode('') setError(null) }, onError: err => { if (isNetworkError(err)) { setError({ retryable: true, isResendError: true, message: _( msg`A network error occurred. Please check your internet connection.`, ), }) } else if ( err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError ) { setError({ retryable: false, isResendError: true, message: _( msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, ), }) } else if ( err instanceof AppBskyContactStartPhoneVerification.RateLimitExceededError ) { setError({ retryable: true, isResendError: true, message: _( msg`Too many codes sent. Please wait a few minutes and try again.`, ), }) } else { logger.error('Resend failed', {safeMessage: err}) setError({ retryable: true, isResendError: true, message: _(msg`An error occurred. ${cleanError(err)}`), }) } }, }) const onPressBack = useOnPressBackButton() return ( {context === 'Onboarding' ? ( ) : ( )} {context === 'Onboarding' && } Verify phone number Enter the 6-digit code sent to {prettyNumber} verifyNumber(code)} /> resendCode()} onRetry={() => verifyNumber(otpCode)} lastCodeSentAt={state.lastSentAt} /> ) } /** * Horrible component that takes all the state above and figures out what messages * and buttons to display. */ function OTPStatus({ error, isPending, isResendingCode, isSuccess, onResend, onRetry, lastCodeSentAt, }: { error: { retryable: boolean isResendError: boolean message: string } | null isPending: boolean isResendingCode: boolean isSuccess: boolean onResend: () => void onRetry: () => void lastCodeSentAt: Date | null }) { const {_} = useLingui() 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 | null = null let text = '' let textColor = t.atoms.text_contrast_medium.color let showResendButton = false let showRetryButton = false if (isSuccess) { Icon = CircleCheckIcon text = _(msg`Phone number verified`) textColor = t.palette.positive_500 } else if (isPending) { text = _(msg`Verifying...`) } else if (error) { Icon = WarningIcon text = error.message textColor = t.palette.negative_500 if (error.retryable) { if (error.isResendError) { showResendButton = true } else { showRetryButton = true } } } else { showResendButton = true } return ( {text && ( {Icon && } {text} )} {showRetryButton && ( )} {showResendButton && ( )} ) }