import {useState} from 'react' import {Keyboard, View} from 'react-native' import {KeyboardAvoidingView} from 'react-native-keyboard-controller' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {AppBskyContactStartPhoneVerification} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useMutation} from '@tanstack/react-query' import {urls} from '#/lib/constants' import { type CountryCode, getDefaultCountry, } from '#/lib/international-telephone-codes' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' import {useAgent} from '#/state/session' import {OnboardingPosition} from '#/screens/Onboarding/Layout' import { android, atoms as a, platform, tokens, useGutters, useTheme, } from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as TextField from '#/components/forms/TextField' import {InternationalPhoneCodeSelect} from '#/components/InternationalPhoneCodeSelect' import * as Layout from '#/components/Layout' import {InlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {useGeolocation} from '#/geolocation' import {isFindContactsFeatureEnabled} from '../country-allowlist' import { constructFullPhoneNumber, getCountryCodeFromPastedNumber, processPhoneNumber, } from '../phone-number' import {type Action, type State, useOnPressBackButton} from '../state' export function PhoneInput({ state, dispatch, context, onSkip, }: { state: Extract dispatch: React.ActionDispatch<[Action]> context: 'Onboarding' | 'Standalone' onSkip: () => void }) { const {_} = useLingui() const ax = useAnalytics() const t = useTheme() const agent = useAgent() const location = useGeolocation() const [countryCode, setCountryCode] = useState( () => state.phoneCountryCode ?? getDefaultCountry(location), ) const [phoneNumber, setPhoneNumber] = useState(state.phoneNumber ?? '') const gutters = useGutters([0, 'wide']) const insets = useSafeAreaInsets() // for API/generic errors const [error, setError] = useState('') // for issues with parsing the number const [formatError, setFormatError] = useState('') const {mutate: submit, isPending} = useMutation({ mutationFn: async ({ phoneCountryCode, phoneNumber, }: { phoneCountryCode: CountryCode phoneNumber: string }) => { // sends a onetime code to the user's phone number await agent.app.bsky.contact.startPhoneVerification({ phone: constructFullPhoneNumber(phoneCountryCode, phoneNumber), }) }, onSuccess: (_data, {phoneCountryCode, phoneNumber}) => { dispatch({ type: 'SUBMIT_PHONE_NUMBER', payload: {phoneCountryCode, phoneNumber}, }) ax.metric('contacts:phone:phoneEntered', {entryPoint: context}) }, onMutate: () => { Keyboard.dismiss() setError('') setFormatError('') }, onError: err => { if (isNetworkError(err)) { setError( _( msg`A network error occurred. Please check your internet connection`, ), ) } else if ( err instanceof AppBskyContactStartPhoneVerification.RateLimitExceededError ) { setError(_(msg`Rate limit exceeded. Please try again later.`)) } else if ( err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError ) { setError( _( msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, ), ) } else { logger.error('Verify phone number failed', {safeMessage: err}) setError(_(msg`An error occurred. ${cleanError(err)}`)) } }, }) const isFeatureEnabled = isFindContactsFeatureEnabled(countryCode) const onSubmitNumber = () => { if (!isFeatureEnabled) return if (!phoneNumber) return const result = processPhoneNumber(phoneNumber, countryCode) if (result.valid) { setPhoneNumber(result.formatted) setCountryCode(result.countryCode) if (!isFindContactsFeatureEnabled(result.countryCode)) return submit({ phoneCountryCode: result.countryCode, phoneNumber: result.formatted, }) } else { setFormatError(result.reason ?? _(msg`Invalid phone number`)) } } const paddingBottom = Math.max(insets.bottom, tokens.space.xl) const onPressBack = useOnPressBackButton() return ( {context === 'Onboarding' ? ( ) : ( )} {context === 'Onboarding' && } Verify phone number We need to verify your number before we can look for your friends. A verification code will be sent to this number. Phone number setCountryCode(value)} /> { if (formatError) setFormatError('') if (Math.abs(text.length - phoneNumber.length) > 1) { // possibly pasted/autocompleted? auto-switch // country code if possible const result = getCountryCodeFromPastedNumber(text) if (result) { setCountryCode(result.countryCode) setPhoneNumber(result.rest) return } } setPhoneNumber(text) }} placeholder={null} keyboardType={platform({ ios: 'number-pad', android: 'phone-pad', })} autoComplete="tel" returnKeyType={android('next')} onSubmitEditing={onSubmitNumber} /> {!isFeatureEnabled && ( Support for this feature in your country has not been enabled yet! Please check back later. )} {error && {error}} {formatError && {formatError}} ) } function LegalDisclaimer() { const t = useTheme() const {_} = useLingui() const style = [a.text_xs, t.atoms.text_contrast_medium, a.leading_snug] return ( How we use your number: •{' '} Sent to our phone number verification provider Plivo Deleted by Plivo after verification •{' '} Held by Bluesky for 7 days to prevent abuse, then deleted •{' '} Stored as part of a secure code for matching with others By continuing, you consent to this use. You may change your mind any time by visiting settings.{' '} Learn more ) } function ErrorText({children}: {children: React.ReactNode}) { const t = useTheme() return ( {children} ) }