diff --git a/assets/icons/calendar_stroke2_corner0_rounded.svg b/assets/icons/calendar_stroke2_corner0_rounded.svg
new file mode 100644
index 00000000..703f389d
--- /dev/null
+++ b/assets/icons/calendar_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/envelope_stroke2_corner0_rounded.svg b/assets/icons/envelope_stroke2_corner0_rounded.svg
new file mode 100644
index 00000000..c3ab4598
--- /dev/null
+++ b/assets/icons/envelope_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/lock_stroke2_corner0_rounded.svg b/assets/icons/lock_stroke2_corner0_rounded.svg
new file mode 100644
index 00000000..8b094ba5
--- /dev/null
+++ b/assets/icons/lock_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/pencil_stroke2_corner0_rounded.svg b/assets/icons/pencil_stroke2_corner0_rounded.svg
new file mode 100644
index 00000000..73419898
--- /dev/null
+++ b/assets/icons/pencil_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx
index 451810a5..35c2459f 100644
--- a/src/components/forms/DateField/index.android.tsx
+++ b/src/components/forms/DateField/index.android.tsx
@@ -1,19 +1,12 @@
import React from 'react'
-import {View, Pressable} from 'react-native'
-import {useTheme, atoms} from '#/alf'
-import {Text} from '#/components/Typography'
-import {useInteractionState} from '#/components/hooks/useInteractionState'
+import {useTheme} from '#/alf'
import * as TextField from '#/components/forms/TextField'
-import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
-
import {DateFieldProps} from '#/components/forms/DateField/types'
-import {
- localizeDate,
- toSimpleDateString,
-} from '#/components/forms/DateField/utils'
+import {toSimpleDateString} from '#/components/forms/DateField/utils'
import DatePicker from 'react-native-date-picker'
import {isAndroid} from 'platform/detection'
+import {DateFieldButton} from './index.shared'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
@@ -24,18 +17,10 @@ export function DateField({
label,
isInvalid,
testID,
+ accessibilityHint,
}: DateFieldProps) {
const t = useTheme()
const [open, setOpen] = React.useState(false)
- const {
- state: pressed,
- onIn: onPressIn,
- onOut: onPressOut,
- } = useInteractionState()
- const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
-
- const {chromeFocus, chromeError, chromeErrorHover} =
- TextField.useSharedInputStyles()
const onChangeInternal = React.useCallback(
(date: Date) => {
@@ -47,45 +32,23 @@ export function DateField({
[onChangeDate, setOpen],
)
+ const onPress = React.useCallback(() => {
+ setOpen(true)
+ }, [])
+
const onCancel = React.useCallback(() => {
setOpen(false)
}, [])
return (
-
- setOpen(true)}
- onPressIn={onPressIn}
- onPressOut={onPressOut}
- onFocus={onFocus}
- onBlur={onBlur}
- style={[
- {
- paddingTop: 16,
- paddingBottom: 16,
- borderColor: 'transparent',
- borderWidth: 2,
- },
- atoms.flex_row,
- atoms.flex_1,
- atoms.w_full,
- atoms.px_lg,
- atoms.rounded_sm,
- t.atoms.bg_contrast_50,
- focused || pressed ? chromeFocus : {},
- isInvalid ? chromeError : {},
- isInvalid && (focused || pressed) ? chromeErrorHover : {},
- ]}>
-
-
-
- {localizeDate(value)}
-
-
+ <>
+
{open && (
)}
-
+ >
)
}
diff --git a/src/components/forms/DateField/index.shared.tsx b/src/components/forms/DateField/index.shared.tsx
new file mode 100644
index 00000000..29b3e8cb
--- /dev/null
+++ b/src/components/forms/DateField/index.shared.tsx
@@ -0,0 +1,99 @@
+import React from 'react'
+import {View, Pressable} from 'react-native'
+
+import {atoms as a, android, useTheme, web} from '#/alf'
+import {Text} from '#/components/Typography'
+import {useInteractionState} from '#/components/hooks/useInteractionState'
+import * as TextField from '#/components/forms/TextField'
+import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
+import {localizeDate} from './utils'
+
+// looks like a TextField.Input, but is just a button. It'll do something different on each platform on press
+// iOS: open a dialog with an inline date picker
+// Android: open the date picker modal
+
+export function DateFieldButton({
+ label,
+ value,
+ onPress,
+ isInvalid,
+ accessibilityHint,
+}: {
+ label: string
+ value: string
+ onPress: () => void
+ isInvalid?: boolean
+ accessibilityHint?: string
+}) {
+ const t = useTheme()
+
+ const {
+ state: pressed,
+ onIn: onPressIn,
+ onOut: onPressOut,
+ } = useInteractionState()
+ const {
+ state: hovered,
+ onIn: onHoverIn,
+ onOut: onHoverOut,
+ } = useInteractionState()
+ const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
+
+ const {chromeHover, chromeFocus, chromeError, chromeErrorHover} =
+ TextField.useSharedInputStyles()
+
+ return (
+
+
+
+
+ {localizeDate(value)}
+
+
+
+ )
+}
diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx
index 49e47a01..22fa3a9f 100644
--- a/src/components/forms/DateField/index.tsx
+++ b/src/components/forms/DateField/index.tsx
@@ -1,11 +1,16 @@
import React from 'react'
import {View} from 'react-native'
-import {useTheme, atoms} from '#/alf'
+import {useTheme, atoms as a} from '#/alf'
import * as TextField from '#/components/forms/TextField'
import {toSimpleDateString} from '#/components/forms/DateField/utils'
import {DateFieldProps} from '#/components/forms/DateField/types'
import DatePicker from 'react-native-date-picker'
+import * as Dialog from '#/components/Dialog'
+import {DateFieldButton} from './index.shared'
+import {Button, ButtonText} from '#/components/Button'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
@@ -22,8 +27,12 @@ export function DateField({
onChangeDate,
testID,
label,
+ isInvalid,
+ accessibilityHint,
}: DateFieldProps) {
+ const {_} = useLingui()
const t = useTheme()
+ const control = Dialog.useDialogControl()
const onChangeInternal = React.useCallback(
(date: Date | undefined) => {
@@ -36,17 +45,43 @@ export function DateField({
)
return (
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+ >
)
}
diff --git a/src/components/forms/DateField/index.web.tsx b/src/components/forms/DateField/index.web.tsx
index 32f38a5d..a3aa302f 100644
--- a/src/components/forms/DateField/index.web.tsx
+++ b/src/components/forms/DateField/index.web.tsx
@@ -2,6 +2,7 @@ import React from 'react'
import {TextInput, TextInputProps, StyleSheet} from 'react-native'
// @ts-ignore
import {unstable_createElement} from 'react-native-web'
+import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
import * as TextField from '#/components/forms/TextField'
import {toSimpleDateString} from '#/components/forms/DateField/utils'
@@ -37,6 +38,7 @@ export function DateField({
label,
isInvalid,
testID,
+ accessibilityHint,
}: DateFieldProps) {
const handleOnChange = React.useCallback(
(e: any) => {
@@ -52,12 +54,14 @@ export function DateField({
return (
+
{}}
testID={testID}
+ accessibilityHint={accessibilityHint}
/>
)
diff --git a/src/components/forms/DateField/types.ts b/src/components/forms/DateField/types.ts
index 129f5672..5400cf90 100644
--- a/src/components/forms/DateField/types.ts
+++ b/src/components/forms/DateField/types.ts
@@ -4,4 +4,5 @@ export type DateFieldProps = {
label: string
isInvalid?: boolean
testID?: string
+ accessibilityHint?: string
}
diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx
index 3cffe5b2..376883c9 100644
--- a/src/components/forms/TextField.tsx
+++ b/src/components/forms/TextField.tsx
@@ -126,8 +126,8 @@ export function useSharedInputStyles() {
export type InputProps = Omit & {
label: string
- value: string
- onChangeText: (value: string) => void
+ value?: string
+ onChangeText?: (value: string) => void
isInvalid?: boolean
inputRef?: React.RefObject
}
@@ -277,7 +277,7 @@ export function Icon({icon: Comp}: {icon: React.ComponentType}) {
= 3,
totalLength: fullHandle.length <= 253,
}
diff --git a/src/view/com/auth/create/Step3.tsx b/src/screens/Signup/StepCaptcha.tsx
similarity index 53%
rename from src/view/com/auth/create/Step3.tsx
rename to src/screens/Signup/StepCaptcha.tsx
index 53fdfdde..c4181e55 100644
--- a/src/view/com/auth/create/Step3.tsx
+++ b/src/screens/Signup/StepCaptcha.tsx
@@ -1,57 +1,39 @@
import React from 'react'
import {ActivityIndicator, StyleSheet, View} from 'react-native'
-import {
- CreateAccountState,
- CreateAccountDispatch,
- useSubmitCreateAccount,
-} from './state'
-import {StepHeader} from './StepHeader'
-import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
-import {isWeb} from 'platform/detection'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-
import {nanoid} from 'nanoid/non-secure'
+import {useSignupContext, useSubmitSignup} from '#/screens/Signup/state'
import {CaptchaWebView} from 'view/com/auth/create/CaptchaWebView'
-import {useTheme} from 'lib/ThemeContext'
import {createFullHandle} from 'lib/strings/handles'
+import {isWeb} from 'platform/detection'
+import {atoms as a, useTheme} from '#/alf'
+import {FormError} from '#/components/forms/FormError'
+import {ScreenTransition} from '#/screens/Login/ScreenTransition'
const CAPTCHA_PATH = '/gate/signup'
-export function Step3({
- uiState,
- uiDispatch,
-}: {
- uiState: CreateAccountState
- uiDispatch: CreateAccountDispatch
-}) {
+export function StepCaptcha() {
const {_} = useLingui()
const theme = useTheme()
- const submit = useSubmitCreateAccount(uiState, uiDispatch)
+ const {state, dispatch} = useSignupContext()
+ const submit = useSubmitSignup({state, dispatch})
const [completed, setCompleted] = React.useState(false)
const stateParam = React.useMemo(() => nanoid(15), [])
const url = React.useMemo(() => {
- const newUrl = new URL(uiState.serviceUrl)
+ const newUrl = new URL(state.serviceUrl)
newUrl.pathname = CAPTCHA_PATH
newUrl.searchParams.set(
'handle',
- createFullHandle(uiState.handle, uiState.userDomain),
+ createFullHandle(state.handle, state.userDomain),
)
newUrl.searchParams.set('state', stateParam)
- newUrl.searchParams.set('colorScheme', theme.colorScheme)
-
- console.log(newUrl)
+ newUrl.searchParams.set('colorScheme', theme.name)
return newUrl.href
- }, [
- uiState.serviceUrl,
- uiState.handle,
- uiState.userDomain,
- stateParam,
- theme.colorScheme,
- ])
+ }, [state.serviceUrl, state.handle, state.userDomain, stateParam, theme.name])
const onSuccess = React.useCallback(
(code: string) => {
@@ -62,33 +44,31 @@ export function Step3({
)
const onError = React.useCallback(() => {
- uiDispatch({
- type: 'set-error',
+ dispatch({
+ type: 'setError',
value: _(msg`Error receiving captcha response.`),
})
- }, [_, uiDispatch])
+ }, [_, dispatch])
return (
-
-
-
- {!completed ? (
-
- ) : (
-
- )}
+
+
+
+ {!completed ? (
+
+ ) : (
+
+ )}
+
+
-
- {uiState.error ? (
-
- ) : undefined}
-
+
)
}
diff --git a/src/screens/Signup/StepHandle.tsx b/src/screens/Signup/StepHandle.tsx
new file mode 100644
index 00000000..e0a79e8f
--- /dev/null
+++ b/src/screens/Signup/StepHandle.tsx
@@ -0,0 +1,134 @@
+import React from 'react'
+import {View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {useLingui} from '@lingui/react'
+import {msg, Trans} from '@lingui/macro'
+import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
+import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
+import * as TextField from '#/components/forms/TextField'
+import {useSignupContext} from '#/screens/Signup/state'
+import {Text} from '#/components/Typography'
+import {atoms as a, useTheme} from '#/alf'
+import {
+ createFullHandle,
+ IsValidHandle,
+ validateHandle,
+} from 'lib/strings/handles'
+import {ScreenTransition} from '#/screens/Login/ScreenTransition'
+
+export function StepHandle() {
+ const {_} = useLingui()
+ const t = useTheme()
+ const {state, dispatch} = useSignupContext()
+
+ const [validCheck, setValidCheck] = React.useState({
+ handleChars: false,
+ hyphenStartOrEnd: false,
+ frontLength: false,
+ totalLength: true,
+ overall: false,
+ })
+
+ useFocusEffect(
+ React.useCallback(() => {
+ console.log('run')
+ setValidCheck(validateHandle(state.handle, state.userDomain))
+ }, [state.handle, state.userDomain]),
+ )
+
+ const onHandleChange = React.useCallback(
+ (value: string) => {
+ if (state.error) {
+ dispatch({type: 'setError', value: ''})
+ }
+
+ dispatch({
+ type: 'setHandle',
+ value,
+ })
+ },
+ [dispatch, state.error],
+ )
+
+ return (
+
+
+
+
+
+
+
+
+
+ Your full handle will be{' '}
+
+ @{createFullHandle(state.handle, state.userDomain)}
+
+
+
+
+ {state.error ? (
+
+
+ {state.error}
+
+ ) : undefined}
+ {validCheck.hyphenStartOrEnd ? (
+
+
+
+ Only contains letters, numbers, and hyphens
+
+
+ ) : (
+
+
+
+ Doesn't begin or end with a hyphen
+
+
+ )}
+
+
+ {!validCheck.totalLength ? (
+
+ No longer than 253 characters
+
+ ) : (
+
+ At least 3 characters
+
+ )}
+
+
+
+
+ )
+}
+
+function IsValidIcon({valid}: {valid: boolean}) {
+ const t = useTheme()
+ if (!valid) {
+ return
+ }
+ return
+}
diff --git a/src/screens/Signup/StepInfo.tsx b/src/screens/Signup/StepInfo.tsx
new file mode 100644
index 00000000..30a31884
--- /dev/null
+++ b/src/screens/Signup/StepInfo.tsx
@@ -0,0 +1,145 @@
+import React from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {atoms as a} from '#/alf'
+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 {is13, is18, useSignupContext} from '#/screens/Signup/state'
+import * as DateField from '#/components/forms/DateField'
+import {logger} from '#/logger'
+import {Loader} from '#/components/Loader'
+import {Policies} from 'view/com/auth/create/Policies'
+import {HostingProvider} from '#/components/forms/HostingProvider'
+import {FormError} from '#/components/forms/FormError'
+import {ScreenTransition} from '#/screens/Login/ScreenTransition'
+
+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() {
+ const {_} = useLingui()
+ const {state, dispatch} = useSignupContext()
+
+ return (
+
+
+
+
+
+ Hosting provider
+
+
+ dispatch({type: 'setServiceUrl', value: v})
+ }
+ />
+
+ {state.isLoading ? (
+
+
+
+ ) : state.serviceDescription ? (
+ <>
+ {state.serviceDescription.inviteCodeRequired && (
+
+
+ Invite code
+
+
+
+ {
+ dispatch({
+ type: 'setInviteCode',
+ value: value.trim(),
+ })
+ }}
+ label={_(msg`Required for this provider`)}
+ defaultValue={state.inviteCode}
+ autoCapitalize="none"
+ autoComplete="email"
+ keyboardType="email-address"
+ />
+
+
+ )}
+
+
+ Email
+
+
+
+ {
+ dispatch({
+ type: 'setEmail',
+ value: value.trim(),
+ })
+ }}
+ label={_(msg`Enter your email address`)}
+ defaultValue={state.email}
+ autoCapitalize="none"
+ autoComplete="email"
+ keyboardType="email-address"
+ />
+
+
+
+
+ Password
+
+
+
+ {
+ dispatch({
+ type: 'setPassword',
+ value,
+ })
+ }}
+ label={_(msg`Choose your password`)}
+ defaultValue={state.password}
+ secureTextEntry
+ autoComplete="new-password"
+ />
+
+
+
+
+ Your birth date
+
+ {
+ dispatch({
+ type: 'setDateOfBirth',
+ value: sanitizeDate(new Date(date)),
+ })
+ }}
+ label={_(msg`Date of birth`)}
+ accessibilityHint={_(msg`Select your date of birth`)}
+ />
+
+
+ >
+ ) : undefined}
+
+
+ )
+}
diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx
new file mode 100644
index 00000000..b1acbbdf
--- /dev/null
+++ b/src/screens/Signup/index.tsx
@@ -0,0 +1,225 @@
+import React from 'react'
+import {ScrollView, View} from 'react-native'
+import {useLingui} from '@lingui/react'
+import {msg, Trans} from '@lingui/macro'
+import {
+ initialState,
+ reducer,
+ SignupContext,
+ SignupStep,
+ useSubmitSignup,
+} from '#/screens/Signup/state'
+import {StepInfo} from '#/screens/Signup/StepInfo'
+import {StepHandle} from '#/screens/Signup/StepHandle'
+import {StepCaptcha} from '#/screens/Signup/StepCaptcha'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import {Text} from '#/components/Typography'
+import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
+import {FEEDBACK_FORM_URL} from 'lib/constants'
+import {InlineLink} from '#/components/Link'
+import {useServiceQuery} from 'state/queries/service'
+import {getAgent} from 'state/session'
+import {createFullHandle} from 'lib/strings/handles'
+import {useAnalytics} from 'lib/analytics/analytics'
+
+export function Signup({onPressBack}: {onPressBack: () => void}) {
+ const {_} = useLingui()
+ const t = useTheme()
+ const {screen} = useAnalytics()
+ const [state, dispatch] = React.useReducer(reducer, initialState)
+ const submit = useSubmitSignup({state, dispatch})
+
+ const {
+ data: serviceInfo,
+ isFetching,
+ isError,
+ refetch,
+ } = useServiceQuery(state.serviceUrl)
+
+ React.useEffect(() => {
+ screen('CreateAccount')
+ }, [screen])
+
+ React.useEffect(() => {
+ if (isFetching) {
+ dispatch({type: 'setIsLoading', value: true})
+ } else if (!isFetching) {
+ dispatch({type: 'setIsLoading', value: false})
+ }
+ }, [isFetching])
+
+ React.useEffect(() => {
+ if (isError) {
+ dispatch({type: 'setServiceDescription', value: undefined})
+ dispatch({
+ type: 'setError',
+ value: _(
+ msg`Unable to contact your service. Please check your Internet connection.`,
+ ),
+ })
+ } else if (serviceInfo) {
+ dispatch({type: 'setServiceDescription', value: serviceInfo})
+ dispatch({type: 'setError', value: ''})
+ }
+ }, [_, serviceInfo, isError])
+
+ const onNextPress = React.useCallback(async () => {
+ if (state.activeStep === SignupStep.HANDLE) {
+ try {
+ dispatch({type: 'setIsLoading', value: true})
+
+ const res = await getAgent().resolveHandle({
+ handle: createFullHandle(state.handle, state.userDomain),
+ })
+
+ if (res.data.did) {
+ dispatch({
+ type: 'setError',
+ value: _(msg`That handle is already taken.`),
+ })
+ return
+ }
+ } catch (e) {
+ // Don't have to handle
+ } finally {
+ dispatch({type: 'setIsLoading', value: false})
+ }
+ }
+
+ // phoneVerificationRequired is actually whether a captcha is required
+ if (
+ state.activeStep === SignupStep.HANDLE &&
+ !state.serviceDescription?.phoneVerificationRequired
+ ) {
+ submit()
+ return
+ }
+
+ dispatch({type: 'next'})
+ }, [
+ _,
+ state.activeStep,
+ state.handle,
+ state.serviceDescription?.phoneVerificationRequired,
+ state.userDomain,
+ submit,
+ ])
+
+ const onBackPress = React.useCallback(() => {
+ if (state.activeStep !== SignupStep.INFO) {
+ dispatch({type: 'prev'})
+ } else {
+ onPressBack()
+ }
+ }, [onPressBack, state.activeStep])
+
+ return (
+
+
+
+
+
+
+ Step {state.activeStep + 1} of{' '}
+ {state.serviceDescription &&
+ !state.serviceDescription.phoneVerificationRequired
+ ? '2'
+ : '3'}
+
+
+ {state.activeStep === SignupStep.INFO ? (
+ Your account
+ ) : state.activeStep === SignupStep.HANDLE ? (
+ Your user handle
+ ) : (
+ Complete the challenge
+ )}
+
+
+
+ {state.activeStep === SignupStep.INFO ? (
+
+ ) : state.activeStep === SignupStep.HANDLE ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {state.activeStep !== SignupStep.CAPTCHA && (
+ <>
+ {isError ? (
+
+ ) : (
+
+ )}
+ >
+ )}
+
+
+
+ Having trouble?{' '}
+
+ Contact support
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts
new file mode 100644
index 00000000..1ae43612
--- /dev/null
+++ b/src/screens/Signup/state.ts
@@ -0,0 +1,320 @@
+import React, {useCallback} from 'react'
+import {LayoutAnimation} from 'react-native'
+import * as EmailValidator from 'email-validator'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
+import {cleanError} from 'lib/strings/errors'
+import {
+ ComAtprotoServerCreateAccount,
+ ComAtprotoServerDescribeServer,
+} from '@atproto/api'
+
+import {logger} from '#/logger'
+import {DEFAULT_SERVICE, IS_PROD_SERVICE} from 'lib/constants'
+import {createFullHandle, validateHandle} from 'lib/strings/handles'
+import {getAge} from 'lib/strings/time'
+import {useSessionApi} from 'state/session'
+import {
+ DEFAULT_PROD_FEEDS,
+ usePreferencesSetBirthDateMutation,
+ useSetSaveFeedsMutation,
+} from 'state/queries/preferences'
+import {useOnboardingDispatch} from 'state/shell'
+
+export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
+
+const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
+
+export enum SignupStep {
+ INFO,
+ HANDLE,
+ CAPTCHA,
+}
+
+export type SignupState = {
+ hasPrev: boolean
+ canNext: boolean
+ activeStep: SignupStep
+
+ serviceUrl: string
+ serviceDescription?: ServiceDescription
+ userDomain: string
+ dateOfBirth: Date
+ email: string
+ password: string
+ inviteCode: string
+ handle: string
+
+ error: string
+ isLoading: boolean
+}
+
+export type SignupAction =
+ | {type: 'prev'}
+ | {type: 'next'}
+ | {type: 'finish'}
+ | {type: 'setStep'; value: SignupStep}
+ | {type: 'setServiceUrl'; value: string}
+ | {type: 'setServiceDescription'; value: ServiceDescription | undefined}
+ | {type: 'setEmail'; value: string}
+ | {type: 'setPassword'; value: string}
+ | {type: 'setDateOfBirth'; value: Date}
+ | {type: 'setInviteCode'; value: string}
+ | {type: 'setHandle'; value: string}
+ | {type: 'setVerificationCode'; value: string}
+ | {type: 'setError'; value: string}
+ | {type: 'setCanNext'; value: boolean}
+ | {type: 'setIsLoading'; value: boolean}
+
+export const initialState: SignupState = {
+ hasPrev: false,
+ canNext: false,
+ activeStep: SignupStep.INFO,
+
+ serviceUrl: DEFAULT_SERVICE,
+ serviceDescription: undefined,
+ userDomain: '',
+ dateOfBirth: DEFAULT_DATE,
+ email: '',
+ password: '',
+ handle: '',
+ inviteCode: '',
+
+ error: '',
+ isLoading: false,
+}
+
+export function is13(date: Date) {
+ return getAge(date) >= 13
+}
+
+export function is18(date: Date) {
+ return getAge(date) >= 18
+}
+
+export function reducer(s: SignupState, a: SignupAction): SignupState {
+ let next = {...s}
+
+ switch (a.type) {
+ case 'prev': {
+ if (s.activeStep !== SignupStep.INFO) {
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+ next.activeStep--
+ next.error = ''
+ }
+ break
+ }
+ case 'next': {
+ if (s.activeStep !== SignupStep.CAPTCHA) {
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+ next.activeStep++
+ next.error = ''
+ }
+ break
+ }
+ case 'setStep': {
+ next.activeStep = a.value
+ break
+ }
+ case 'setServiceUrl': {
+ next.serviceUrl = a.value
+ break
+ }
+ case 'setServiceDescription': {
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+
+ next.serviceDescription = a.value
+ next.userDomain = a.value?.availableUserDomains[0] ?? ''
+ next.isLoading = false
+ break
+ }
+
+ case 'setEmail': {
+ next.email = a.value
+ break
+ }
+ case 'setPassword': {
+ next.password = a.value
+ break
+ }
+ case 'setDateOfBirth': {
+ next.dateOfBirth = a.value
+ break
+ }
+ case 'setInviteCode': {
+ next.inviteCode = a.value
+ break
+ }
+ case 'setHandle': {
+ next.handle = a.value
+ break
+ }
+ case 'setCanNext': {
+ next.canNext = a.value
+ break
+ }
+ case 'setIsLoading': {
+ next.isLoading = a.value
+ break
+ }
+ case 'setError': {
+ next.error = a.value
+ break
+ }
+ }
+
+ next.hasPrev = next.activeStep !== SignupStep.INFO
+
+ switch (next.activeStep) {
+ case SignupStep.INFO: {
+ const isValidEmail = EmailValidator.validate(next.email)
+ next.canNext =
+ !!(next.email && next.password && next.dateOfBirth) &&
+ (!next.serviceDescription?.inviteCodeRequired || !!next.inviteCode) &&
+ is13(next.dateOfBirth) &&
+ isValidEmail
+ break
+ }
+ case SignupStep.HANDLE: {
+ next.canNext =
+ !!next.handle && validateHandle(next.handle, next.userDomain).overall
+ break
+ }
+ }
+
+ logger.debug('signup', next)
+
+ if (s.activeStep !== next.activeStep) {
+ logger.debug('signup: step changed', {activeStep: next.activeStep})
+ }
+
+ return next
+}
+
+interface IContext {
+ state: SignupState
+ dispatch: React.Dispatch
+}
+export const SignupContext = React.createContext({} as IContext)
+export const useSignupContext = () => React.useContext(SignupContext)
+
+export function useSubmitSignup({
+ state,
+ dispatch,
+}: {
+ state: SignupState
+ dispatch: (action: SignupAction) => void
+}) {
+ const {_} = useLingui()
+ const {createAccount} = useSessionApi()
+ const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
+ const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
+ const onboardingDispatch = useOnboardingDispatch()
+
+ return useCallback(
+ async (verificationCode?: string) => {
+ if (!state.email) {
+ dispatch({type: 'setStep', value: SignupStep.INFO})
+ return dispatch({
+ type: 'setError',
+ value: _(msg`Please enter your email.`),
+ })
+ }
+ if (!EmailValidator.validate(state.email)) {
+ dispatch({type: 'setStep', value: SignupStep.INFO})
+ return dispatch({
+ type: 'setError',
+ value: _(msg`Your email appears to be invalid.`),
+ })
+ }
+ if (!state.password) {
+ dispatch({type: 'setStep', value: SignupStep.INFO})
+ return dispatch({
+ type: 'setError',
+ value: _(msg`Please choose your password.`),
+ })
+ }
+ if (!state.handle) {
+ dispatch({type: 'setStep', value: SignupStep.HANDLE})
+ return dispatch({
+ type: 'setError',
+ value: _(msg`Please choose your handle.`),
+ })
+ }
+ if (
+ state.serviceDescription?.phoneVerificationRequired &&
+ !verificationCode
+ ) {
+ dispatch({type: 'setStep', value: SignupStep.CAPTCHA})
+ return dispatch({
+ type: 'setError',
+ value: _(msg`Please complete the verification captcha.`),
+ })
+ }
+ dispatch({type: 'setError', value: ''})
+ dispatch({type: 'setIsLoading', value: true})
+
+ try {
+ onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
+ await createAccount({
+ service: state.serviceUrl,
+ email: state.email,
+ handle: createFullHandle(state.handle, state.userDomain),
+ password: state.password,
+ inviteCode: state.inviteCode.trim(),
+ verificationCode: verificationCode,
+ })
+ setBirthDate({birthDate: state.dateOfBirth})
+ if (IS_PROD_SERVICE(state.serviceUrl)) {
+ setSavedFeeds(DEFAULT_PROD_FEEDS)
+ }
+ } catch (e: any) {
+ onboardingDispatch({type: 'skip'}) // undo starting the onboard
+ let errMsg = e.toString()
+ if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
+ dispatch({
+ type: 'setError',
+ value: _(
+ msg`Invite code not accepted. Check that you input it correctly and try again.`,
+ ),
+ })
+ dispatch({type: 'setStep', value: SignupStep.INFO})
+ return
+ }
+
+ if ([400, 429].includes(e.status)) {
+ logger.warn('Failed to create account', {message: e})
+ } else {
+ logger.error(`Failed to create account (${e.status} status)`, {
+ message: e,
+ })
+ }
+
+ const error = cleanError(errMsg)
+ const isHandleError = error.toLowerCase().includes('handle')
+
+ dispatch({type: 'setIsLoading', value: false})
+ dispatch({type: 'setError', value: cleanError(errMsg)})
+ dispatch({type: 'setStep', value: isHandleError ? 2 : 1})
+ } finally {
+ dispatch({type: 'setIsLoading', value: false})
+ }
+ },
+ [
+ state.email,
+ state.password,
+ state.handle,
+ state.serviceDescription?.phoneVerificationRequired,
+ state.serviceUrl,
+ state.userDomain,
+ state.inviteCode,
+ state.dateOfBirth,
+ dispatch,
+ _,
+ onboardingDispatch,
+ createAccount,
+ setBirthDate,
+ setSavedFeeds,
+ ],
+ )
+}
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
index 58604ec9..b22bbb7f 100644
--- a/src/view/com/auth/LoggedOut.tsx
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -7,7 +7,7 @@ import {useNavigation} from '@react-navigation/native'
import {isIOS, isNative} from '#/platform/detection'
import {Login} from '#/screens/Login'
-import {CreateAccount} from '#/view/com/auth/create/CreateAccount'
+import {Signup} from '#/screens/Signup'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {s} from '#/lib/styles'
import {usePalette} from '#/lib/hooks/usePalette'
@@ -148,7 +148,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
/>
) : undefined}
{screenState === ScreenState.S_CreateAccount ? (
-
setScreenState(ScreenState.S_LoginOrCreateAccount)
}
diff --git a/src/view/com/auth/create/CaptchaWebView.tsx b/src/view/com/auth/create/CaptchaWebView.tsx
index b0de8b4a..06b605e4 100644
--- a/src/view/com/auth/create/CaptchaWebView.tsx
+++ b/src/view/com/auth/create/CaptchaWebView.tsx
@@ -2,7 +2,7 @@ import React from 'react'
import {WebView, WebViewNavigation} from 'react-native-webview'
import {ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes'
import {StyleSheet} from 'react-native'
-import {CreateAccountState} from 'view/com/auth/create/state'
+import {SignupState} from '#/screens/Signup/state'
const ALLOWED_HOSTS = [
'bsky.social',
@@ -17,24 +17,24 @@ const ALLOWED_HOSTS = [
export function CaptchaWebView({
url,
stateParam,
- uiState,
+ state,
onSuccess,
onError,
}: {
url: string
stateParam: string
- uiState?: CreateAccountState
+ state?: SignupState
onSuccess: (code: string) => void
onError: () => void
}) {
const redirectHost = React.useMemo(() => {
- if (!uiState?.serviceUrl) return 'bsky.app'
+ if (!state?.serviceUrl) return 'bsky.app'
- return uiState?.serviceUrl &&
- new URL(uiState?.serviceUrl).host === 'staging.bsky.dev'
+ return state?.serviceUrl &&
+ new URL(state?.serviceUrl).host === 'staging.bsky.dev'
? 'staging.bsky.app'
: 'bsky.app'
- }, [uiState?.serviceUrl])
+ }, [state?.serviceUrl])
const wasSuccessful = React.useRef(false)
diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx
deleted file mode 100644
index d193802f..00000000
--- a/src/view/com/auth/create/CreateAccount.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-import React from 'react'
-import {
- ActivityIndicator,
- ScrollView,
- StyleSheet,
- TouchableOpacity,
- View,
-} from 'react-native'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {Text} from '../../util/text/Text'
-import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useCreateAccount, useSubmitCreateAccount} from './state'
-import {useServiceQuery} from '#/state/queries/service'
-import {FEEDBACK_FORM_URL, HITSLOP_10} from '#/lib/constants'
-
-import {Step1} from './Step1'
-import {Step2} from './Step2'
-import {Step3} from './Step3'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {TextLink} from '../../util/Link'
-import {getAgent} from 'state/session'
-import {createFullHandle, validateHandle} from 'lib/strings/handles'
-
-export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
- const {screen} = useAnalytics()
- const pal = usePalette('default')
- const {_} = useLingui()
- const [uiState, uiDispatch] = useCreateAccount()
- const {isTabletOrDesktop} = useWebMediaQueries()
- const submit = useSubmitCreateAccount(uiState, uiDispatch)
-
- React.useEffect(() => {
- screen('CreateAccount')
- }, [screen])
-
- // fetch service info
- // =
-
- const {
- data: serviceInfo,
- isFetching: serviceInfoIsFetching,
- error: serviceInfoError,
- refetch: refetchServiceInfo,
- } = useServiceQuery(uiState.serviceUrl)
-
- React.useEffect(() => {
- if (serviceInfo) {
- uiDispatch({type: 'set-service-description', value: serviceInfo})
- uiDispatch({type: 'set-error', value: ''})
- } else if (serviceInfoError) {
- uiDispatch({
- type: 'set-error',
- value: _(
- msg`Unable to contact your service. Please check your Internet connection.`,
- ),
- })
- }
- }, [_, uiDispatch, serviceInfo, serviceInfoError])
-
- // event handlers
- // =
-
- const onPressBackInner = React.useCallback(() => {
- if (uiState.canBack) {
- uiDispatch({type: 'back'})
- } else {
- onPressBack()
- }
- }, [uiState, uiDispatch, onPressBack])
-
- const onPressNext = React.useCallback(async () => {
- if (!uiState.canNext) {
- return
- }
-
- if (uiState.step === 2) {
- if (!validateHandle(uiState.handle, uiState.userDomain).overall) {
- return
- }
-
- uiDispatch({type: 'set-processing', value: true})
- try {
- const res = await getAgent().resolveHandle({
- handle: createFullHandle(uiState.handle, uiState.userDomain),
- })
-
- if (res.data.did) {
- uiDispatch({
- type: 'set-error',
- value: _(msg`That handle is already taken.`),
- })
- return
- }
- } catch (e) {
- // Don't need to handle
- } finally {
- uiDispatch({type: 'set-processing', value: false})
- }
-
- if (!uiState.isCaptchaRequired) {
- try {
- await submit()
- } catch {
- // dont need to handle here
- }
- // We don't need to go to the next page if there wasn't a captcha required
- return
- }
- }
-
- uiDispatch({type: 'next'})
- }, [
- uiState.canNext,
- uiState.step,
- uiState.isCaptchaRequired,
- uiState.handle,
- uiState.userDomain,
- uiDispatch,
- _,
- submit,
- ])
-
- // rendering
- // =
-
- return (
-
-
-
- {uiState.step === 1 && (
-
- )}
- {uiState.step === 2 && (
-
- )}
- {uiState.step === 3 && (
-
- )}
-
-
-
-
- Back
-
-
-
- {uiState.canNext ? (
-
- {uiState.isProcessing ? (
-
- ) : (
-
- Next
-
- )}
-
- ) : serviceInfoError ? (
- refetchServiceInfo()}
- accessibilityRole="button"
- accessibilityLabel={_(msg`Retry`)}
- accessibilityHint=""
- accessibilityLiveRegion="polite"
- hitSlop={HITSLOP_10}>
-
- Retry
-
-
- ) : serviceInfoIsFetching ? (
- <>
-
-
- Connecting...
-
- >
- ) : undefined}
-
-
-
-
-
- Having trouble?{' '}
-
-
-
-
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- stepContainer: {
- paddingHorizontal: 20,
- paddingVertical: 20,
- },
-})
diff --git a/src/view/com/auth/create/Policies.tsx b/src/view/com/auth/create/Policies.tsx
index 2c7d6081..dc3c9c17 100644
--- a/src/view/com/auth/create/Policies.tsx
+++ b/src/view/com/auth/create/Policies.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import {StyleSheet, View} from 'react-native'
+import {Linking, StyleSheet, View} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@@ -15,9 +15,11 @@ type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
export const Policies = ({
serviceDescription,
needsGuardian,
+ under13,
}: {
serviceDescription: ServiceDescription
needsGuardian: boolean
+ under13: boolean
}) => {
const pal = usePalette('default')
if (!serviceDescription) {
@@ -53,6 +55,7 @@ export const Policies = ({
href={tos}
text="Terms of Service"
style={[pal.link, s.underline]}
+ onPress={() => Linking.openURL(tos)}
/>,
)
}
@@ -63,6 +66,7 @@ export const Policies = ({
href={pp}
text="Privacy Policy"
style={[pal.link, s.underline]}
+ onPress={() => Linking.openURL(pp)}
/>,
)
}
@@ -81,12 +85,16 @@ export const Policies = ({
By creating an account you agree to the {els}.
- {needsGuardian && (
+ {under13 ? (
+
+ You must be 13 years of age or older to sign up.
+
+ ) : needsGuardian ? (
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}
)
}
diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx
deleted file mode 100644
index 1f6852f8..00000000
--- a/src/view/com/auth/create/Step1.tsx
+++ /dev/null
@@ -1,261 +0,0 @@
-import React from 'react'
-import {
- ActivityIndicator,
- Keyboard,
- StyleSheet,
- TouchableOpacity,
- View,
-} from 'react-native'
-import {CreateAccountState, CreateAccountDispatch, is18} from './state'
-import {Text} from 'view/com/util/text/Text'
-import {DateInput} from 'view/com/util/forms/DateInput'
-import {StepHeader} from './StepHeader'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {TextInput} from '../util/TextInput'
-import {Policies} from './Policies'
-import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
-import {isWeb} from 'platform/detection'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {logger} from '#/logger'
-import {
- FontAwesomeIcon,
- FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {useDialogControl} from '#/components/Dialog'
-
-import {ServerInputDialog} from '../server-input'
-import {toNiceDomain} from '#/lib/strings/url-helpers'
-
-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 Step1({
- uiState,
- uiDispatch,
-}: {
- uiState: CreateAccountState
- uiDispatch: CreateAccountDispatch
-}) {
- const pal = usePalette('default')
- const {_} = useLingui()
- const serverInputControl = useDialogControl()
-
- const onPressSelectService = React.useCallback(() => {
- serverInputControl.open()
- Keyboard.dismiss()
- }, [serverInputControl])
-
- const birthDate = React.useMemo(() => {
- return sanitizeDate(uiState.birthDate)
- }, [uiState.birthDate])
-
- return (
-
- uiDispatch({type: 'set-service-url', value: url})}
- />
-
-
- {uiState.error ? (
-
- ) : undefined}
-
-
-
- Hosting provider
-
-
-
-
-
-
- {toNiceDomain(uiState.serviceUrl)}
-
-
-
-
-
-
-
-
-
- {!uiState.serviceDescription ? (
-
- ) : (
- <>
- {uiState.isInviteCodeRequired && (
-
-
- Invite code
-
- uiDispatch({type: 'set-invite-code', value})}
- accessibilityLabel={_(msg`Invite code`)}
- accessibilityHint={_(msg`Input invite code to proceed`)}
- autoCapitalize="none"
- autoComplete="off"
- autoCorrect={false}
- autoFocus={true}
- />
-
- )}
-
- {!uiState.isInviteCodeRequired || uiState.inviteCode ? (
- <>
-
-
- Email address
-
- uiDispatch({type: 'set-email', value})}
- accessibilityLabel={_(msg`Email`)}
- accessibilityHint={_(msg`Input email for Bluesky account`)}
- accessibilityLabelledBy="email"
- autoCapitalize="none"
- autoComplete="email"
- autoCorrect={false}
- autoFocus={!uiState.isInviteCodeRequired}
- />
-
-
-
-
- Password
-
- uiDispatch({type: 'set-password', value})}
- accessibilityLabel={_(msg`Password`)}
- accessibilityHint={_(msg`Set password`)}
- accessibilityLabelledBy="password"
- autoCapitalize="none"
- autoComplete="new-password"
- autoCorrect={false}
- />
-
-
-
-
- Your birth date
-
-
- uiDispatch({type: 'set-birth-date', value})
- }
- buttonType="default-light"
- buttonStyle={[pal.border, styles.dateInputButton]}
- buttonLabelType="lg"
- accessibilityLabel={_(msg`Birthday`)}
- accessibilityHint={_(msg`Enter your birth date`)}
- accessibilityLabelledBy="birthDate"
- />
-
-
- {uiState.serviceDescription && (
-
- )}
- >
- ) : undefined}
- >
- )}
-
- )
-}
-
-const styles = StyleSheet.create({
- error: {
- borderRadius: 6,
- marginBottom: 10,
- },
- dateInputButton: {
- borderWidth: 1,
- borderRadius: 6,
- paddingVertical: 14,
- },
- // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832.
- touchable: {
- ...(isWeb && {cursor: 'pointer'}),
- },
-})
diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx
deleted file mode 100644
index 5c262977..00000000
--- a/src/view/com/auth/create/Step2.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {CreateAccountState, CreateAccountDispatch} from './state'
-import {Text} from 'view/com/util/text/Text'
-import {StepHeader} from './StepHeader'
-import {s} from 'lib/styles'
-import {TextInput} from '../util/TextInput'
-import {
- createFullHandle,
- IsValidHandle,
- validateHandle,
-} from 'lib/strings/handles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {atoms as a, useTheme} from '#/alf'
-import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
-import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
-import {useFocusEffect} from '@react-navigation/native'
-
-/** STEP 3: Your user handle
- * @field User handle
- */
-export function Step2({
- uiState,
- uiDispatch,
-}: {
- uiState: CreateAccountState
- uiDispatch: CreateAccountDispatch
-}) {
- const pal = usePalette('default')
- const {_} = useLingui()
- const t = useTheme()
-
- const [validCheck, setValidCheck] = React.useState({
- handleChars: false,
- frontLength: false,
- totalLength: true,
- overall: false,
- })
-
- useFocusEffect(
- React.useCallback(() => {
- setValidCheck(validateHandle(uiState.handle, uiState.userDomain))
-
- // Disabling this, because we only want to run this when we focus the screen
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []),
- )
-
- const onHandleChange = React.useCallback(
- (value: string) => {
- if (uiState.error) {
- uiDispatch({type: 'set-error', value: ''})
- }
-
- setValidCheck(validateHandle(value, uiState.userDomain))
- uiDispatch({type: 'set-handle', value})
- },
- [uiDispatch, uiState.error, uiState.userDomain],
- )
-
- return (
-
-
-
-
-
-
- Your full handle will be{' '}
-
- @{createFullHandle(uiState.handle, uiState.userDomain)}
-
-
-
-
- {uiState.error ? (
-
-
-
- {uiState.error}
-
-
- ) : undefined}
-
-
-
- May only contain letters and numbers
-
-
-
-
- {!validCheck.totalLength ? (
-
- May not be longer than 253 characters
-
- ) : (
-
- Must be at least 3 characters
-
- )}
-
-
-
-
- )
-}
-
-function IsValidIcon({valid}: {valid: boolean}) {
- const t = useTheme()
-
- if (!valid) {
- return
- }
-
- return
-}
diff --git a/src/view/com/auth/create/StepHeader.tsx b/src/view/com/auth/create/StepHeader.tsx
deleted file mode 100644
index a98b392d..00000000
--- a/src/view/com/auth/create/StepHeader.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {Text} from 'view/com/util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Trans} from '@lingui/macro'
-import {CreateAccountState} from './state'
-
-export function StepHeader({
- uiState,
- title,
- children,
-}: React.PropsWithChildren<{uiState: CreateAccountState; title: string}>) {
- const pal = usePalette('default')
- const numSteps = 3
- return (
-
-
-
- {uiState.step === 3 ? (
- Last step!
- ) : (
-
- Step {uiState.step} of {numSteps}
-
- )}
-
-
-
- {title}
-
-
- {children}
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 20,
- },
-})