import React, {useRef} from 'react' import {type TextInput, View} from 'react-native' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' import * as EmailValidator from 'email-validator' import type tldts from 'tldts' import {DEFAULT_SERVICE} from '#/lib/constants' import {isEmailMaybeInvalid} from '#/lib/strings/email' import {logger} from '#/logger' import {useSignupContext} from '#/screens/Signup/state' import {Policies} from '#/screens/Signup/StepInfo/Policies' import {atoms as a, native} from '#/alf' import * as Admonition from '#/components/Admonition' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog' import {Divider} from '#/components/Divider' import * as DateField from '#/components/forms/DateField' import {type DateFieldRef} from '#/components/forms/DateField/types' import {FormError} from '#/components/forms/FormError' import {HostingProvider} from '#/components/forms/HostingProvider' import * as TextField from '#/components/forms/TextField' import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link' import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import {usePreemptivelyCompleteActivePolicyUpdate} from '#/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate' import {ScreenTransition} from '#/components/ScreenTransition' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import { isUnderAge, MIN_ACCESS_AGE, useAgeAssuranceRegionConfigWithFallback, } from '#/ageAssurance/util' import {useAnalytics} from '#/analytics' import {IS_NATIVE, IS_WEB} from '#/env' import { useDeviceGeolocationApi, useIsDeviceGeolocationGranted, } from '#/geolocation' import {BackNextButtons} from '../BackNextButtons' function sanitizeDate(date: Date): Date { if (!date || date.toString() === 'Invalid Date') { logger.error(`Create account: handled invalid date for birthDate`, { hasDate: !!date, }) return new Date() } return date } export function StepInfo({ onPressBack, onPressSignIn, isServerError, refetchServer, isLoadingStarterPack, }: { onPressBack: () => void onPressSignIn: () => void isServerError: boolean refetchServer: () => void isLoadingStarterPack: boolean }) { const {_} = useLingui() const ax = useAnalytics() const {state, dispatch} = useSignupContext() const preemptivelyCompleteActivePolicyUpdate = usePreemptivelyCompleteActivePolicyUpdate() const inviteCodeValueRef = useRef(state.inviteCode) const emailValueRef = useRef(state.email) const prevEmailValueRef = useRef(state.email) const passwordValueRef = useRef(state.password) const emailInputRef = useRef(null) const passwordInputRef = useRef(null) const birthdateInputRef = useRef(null) const aaRegionConfig = useAgeAssuranceRegionConfigWithFallback() const {setDeviceGeolocation} = useDeviceGeolocationApi() const locationControl = Dialog.useDialogControl() const isOverRegionMinAccessAge = true const isOverAppMinAccessAge = true const isOverMinAdultAge = true const isDeviceGeolocationGranted = true const [hasWarnedEmail, setHasWarnedEmail] = React.useState(false) const tldtsRef = React.useRef(undefined) React.useEffect(() => { // @ts-expect-error - valid path import('tldts/dist/index.cjs.min.js').then(tldts => { tldtsRef.current = tldts }) // This will get used in the avatar creator a few steps later, so lets preload it now // @ts-expect-error - valid path import('react-native-view-shot/src/index') }, []) const onNextPress = () => { const inviteCode = inviteCodeValueRef.current const email = emailValueRef.current const emailChanged = prevEmailValueRef.current !== email const password = passwordValueRef.current if (!isOverRegionMinAccessAge) { return } if (state.serviceUrl === DEFAULT_SERVICE) { return dispatch({ type: 'setError', value: _( msg`Please choose a 3rd party service host, or sign up on bsky.app.`, ), }) } if (state.serviceDescription?.inviteCodeRequired && !inviteCode) { return dispatch({ type: 'setError', value: _(msg`Please enter your invite code.`), field: 'invite-code', }) } if (!email) { return dispatch({ type: 'setError', value: _(msg`Please enter your email.`), field: 'email', }) } if (!EmailValidator.validate(email)) { return dispatch({ type: 'setError', value: _(msg`Your email appears to be invalid.`), field: 'email', }) } if (emailChanged && tldtsRef.current) { if (isEmailMaybeInvalid(email, tldtsRef.current)) { prevEmailValueRef.current = email setHasWarnedEmail(true) return dispatch({ type: 'setError', value: _( msg`Please double-check that you have entered your email address correctly.`, ), }) } } else if (hasWarnedEmail) { setHasWarnedEmail(false) } prevEmailValueRef.current = email if (!password) { return dispatch({ type: 'setError', value: _(msg`Please choose your password.`), field: 'password', }) } if (password.length < 8) { return dispatch({ type: 'setError', value: _(msg`Your password must be at least 8 characters long.`), field: 'password', }) } preemptivelyCompleteActivePolicyUpdate() dispatch({type: 'setInviteCode', value: inviteCode}) dispatch({type: 'setEmail', value: email}) dispatch({type: 'setPassword', value: password}) dispatch({type: 'next'}) ax.metric('signup:nextPressed', { activeStep: state.activeStep, }) } return ( {state.serviceUrl === DEFAULT_SERVICE && ( Witchsky is part of the{' '} { Atmosphere } —the network of apps, services, and accounts built on the AT Protocol. If you have one, sign in with an existing Bluesky account. )} dispatch({type: 'setServiceUrl', value: v})} /> {state.serviceUrl === DEFAULT_SERVICE && ( Don't have an account provider or an existing Bluesky account? To create a new account on a Bluesky-hosted PDS, sign up through{' '} {/* TODO: Xan: change to say sign up for a Witchsky account */} { bsky.app }{' '} first, then return to Witchsky and log in with the account you created. )} {state.isLoading || isLoadingStarterPack ? ( ) : state.serviceDescription && state.serviceUrl !== DEFAULT_SERVICE ? ( <> {state.serviceDescription.inviteCodeRequired && ( Invite code { inviteCodeValueRef.current = value.trim() if ( state.errorField === 'invite-code' && value.trim().length > 0 ) { dispatch({type: 'clearError'}) } }} label={_(msg`Required for this provider`)} defaultValue={state.inviteCode} autoCapitalize="none" autoComplete="email" keyboardType="email-address" returnKeyType="next" submitBehavior={native('submit')} onSubmitEditing={native(() => emailInputRef.current?.focus(), )} /> )} Email { emailValueRef.current = value.trim() if (hasWarnedEmail) { setHasWarnedEmail(false) } if ( state.errorField === 'email' && value.trim().length > 0 && EmailValidator.validate(value.trim()) ) { dispatch({type: 'clearError'}) } }} label={_(msg`Enter your email address`)} defaultValue={state.email} autoCapitalize="none" autoComplete="email" keyboardType="email-address" returnKeyType="next" submitBehavior={native('submit')} onSubmitEditing={native(() => passwordInputRef.current?.focus(), )} /> Password { passwordValueRef.current = value if (state.errorField === 'password' && value.length >= 8) { dispatch({type: 'clearError'}) } }} label={_(msg`Choose your password`)} defaultValue={state.password} secureTextEntry autoComplete="new-password" autoCapitalize="none" returnKeyType="next" submitBehavior={native('blurAndSubmit')} onSubmitEditing={native(() => birthdateInputRef.current?.focus(), )} passwordRules="minlength: 8;" /> Your birth date { dispatch({ type: 'setDateOfBirth', value: sanitizeDate(new Date(date)), }) }} label={_(msg`Date of birth`)} accessibilityHint={_(msg`Select your date of birth`)} maximumDate={new Date()} /> {!isOverRegionMinAccessAge || !isOverAppMinAccessAge ? ( {!isOverAppMinAccessAge ? ( ) : ( )} {IS_NATIVE && !isDeviceGeolocationGranted && isOverAppMinAccessAge && ( Have we got your location wrong?{' '} { locationControl.open() })}> Tap here to confirm your location with GPS. )} ) : !isOverMinAdultAge ? ( If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf. ) : undefined} {IS_NATIVE && ( { props.closeDialog(() => { // set this after close! setDeviceGeolocation(props.geolocation) Toast.show(_(msg`Your location has been updated.`), { type: 'success', }) }) }} /> )} ) : undefined} ) }