mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {useCallback} from 'react'
2import {LayoutAnimation} from 'react-native'
3import {
4 ComAtprotoServerCreateAccount,
5 ComAtprotoServerDescribeServer,
6} from '@atproto/api'
7import {msg} from '@lingui/macro'
8import {useLingui} from '@lingui/react'
9import * as EmailValidator from 'email-validator'
10
11import {DEFAULT_SERVICE, IS_PROD_SERVICE} from '#/lib/constants'
12import {cleanError} from '#/lib/strings/errors'
13import {createFullHandle, validateHandle} from '#/lib/strings/handles'
14import {getAge} from '#/lib/strings/time'
15import {logger} from '#/logger'
16import {
17 DEFAULT_PROD_FEEDS,
18 usePreferencesSetBirthDateMutation,
19 useSetSaveFeedsMutation,
20} from '#/state/queries/preferences'
21import {useSessionApi} from '#/state/session'
22import {useOnboardingDispatch} from '#/state/shell'
23
24export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
25
26const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
27
28export enum SignupStep {
29 INFO,
30 HANDLE,
31 CAPTCHA,
32}
33
34export type SignupState = {
35 hasPrev: boolean
36 canNext: boolean
37 activeStep: SignupStep
38
39 serviceUrl: string
40 serviceDescription?: ServiceDescription
41 userDomain: string
42 dateOfBirth: Date
43 email: string
44 password: string
45 inviteCode: string
46 handle: string
47
48 error: string
49 isLoading: boolean
50}
51
52export type SignupAction =
53 | {type: 'prev'}
54 | {type: 'next'}
55 | {type: 'finish'}
56 | {type: 'setStep'; value: SignupStep}
57 | {type: 'setServiceUrl'; value: string}
58 | {type: 'setServiceDescription'; value: ServiceDescription | undefined}
59 | {type: 'setEmail'; value: string}
60 | {type: 'setPassword'; value: string}
61 | {type: 'setDateOfBirth'; value: Date}
62 | {type: 'setInviteCode'; value: string}
63 | {type: 'setHandle'; value: string}
64 | {type: 'setVerificationCode'; value: string}
65 | {type: 'setError'; value: string}
66 | {type: 'setCanNext'; value: boolean}
67 | {type: 'setIsLoading'; value: boolean}
68
69export const initialState: SignupState = {
70 hasPrev: false,
71 canNext: false,
72 activeStep: SignupStep.INFO,
73
74 serviceUrl: DEFAULT_SERVICE,
75 serviceDescription: undefined,
76 userDomain: '',
77 dateOfBirth: DEFAULT_DATE,
78 email: '',
79 password: '',
80 handle: '',
81 inviteCode: '',
82
83 error: '',
84 isLoading: false,
85}
86
87export function is13(date: Date) {
88 return getAge(date) >= 13
89}
90
91export function is18(date: Date) {
92 return getAge(date) >= 18
93}
94
95export function reducer(s: SignupState, a: SignupAction): SignupState {
96 let next = {...s}
97
98 switch (a.type) {
99 case 'prev': {
100 if (s.activeStep !== SignupStep.INFO) {
101 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
102 next.activeStep--
103 next.error = ''
104 }
105 break
106 }
107 case 'next': {
108 if (s.activeStep !== SignupStep.CAPTCHA) {
109 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
110 next.activeStep++
111 next.error = ''
112 }
113 break
114 }
115 case 'setStep': {
116 next.activeStep = a.value
117 break
118 }
119 case 'setServiceUrl': {
120 next.serviceUrl = a.value
121 break
122 }
123 case 'setServiceDescription': {
124 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
125
126 next.serviceDescription = a.value
127 next.userDomain = a.value?.availableUserDomains[0] ?? ''
128 next.isLoading = false
129 break
130 }
131
132 case 'setEmail': {
133 next.email = a.value
134 break
135 }
136 case 'setPassword': {
137 next.password = a.value
138 break
139 }
140 case 'setDateOfBirth': {
141 next.dateOfBirth = a.value
142 break
143 }
144 case 'setInviteCode': {
145 next.inviteCode = a.value
146 break
147 }
148 case 'setHandle': {
149 next.handle = a.value
150 break
151 }
152 case 'setCanNext': {
153 next.canNext = a.value
154 break
155 }
156 case 'setIsLoading': {
157 next.isLoading = a.value
158 break
159 }
160 case 'setError': {
161 next.error = a.value
162 break
163 }
164 }
165
166 next.hasPrev = next.activeStep !== SignupStep.INFO
167
168 switch (next.activeStep) {
169 case SignupStep.INFO: {
170 const isValidEmail = EmailValidator.validate(next.email)
171 next.canNext =
172 !!(next.email && next.password && next.dateOfBirth) &&
173 (!next.serviceDescription?.inviteCodeRequired || !!next.inviteCode) &&
174 is13(next.dateOfBirth) &&
175 isValidEmail
176 break
177 }
178 case SignupStep.HANDLE: {
179 next.canNext =
180 !!next.handle && validateHandle(next.handle, next.userDomain).overall
181 break
182 }
183 }
184
185 logger.debug('signup', next)
186
187 if (s.activeStep !== next.activeStep) {
188 logger.debug('signup: step changed', {activeStep: next.activeStep})
189 }
190
191 return next
192}
193
194interface IContext {
195 state: SignupState
196 dispatch: React.Dispatch<SignupAction>
197}
198export const SignupContext = React.createContext<IContext>({} as IContext)
199export const useSignupContext = () => React.useContext(SignupContext)
200
201export function useSubmitSignup({
202 state,
203 dispatch,
204}: {
205 state: SignupState
206 dispatch: (action: SignupAction) => void
207}) {
208 const {_} = useLingui()
209 const {createAccount} = useSessionApi()
210 const {mutateAsync: setBirthDate} = usePreferencesSetBirthDateMutation()
211 const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
212 const onboardingDispatch = useOnboardingDispatch()
213
214 return useCallback(
215 async (verificationCode?: string) => {
216 if (!state.email) {
217 dispatch({type: 'setStep', value: SignupStep.INFO})
218 return dispatch({
219 type: 'setError',
220 value: _(msg`Please enter your email.`),
221 })
222 }
223 if (!EmailValidator.validate(state.email)) {
224 dispatch({type: 'setStep', value: SignupStep.INFO})
225 return dispatch({
226 type: 'setError',
227 value: _(msg`Your email appears to be invalid.`),
228 })
229 }
230 if (!state.password) {
231 dispatch({type: 'setStep', value: SignupStep.INFO})
232 return dispatch({
233 type: 'setError',
234 value: _(msg`Please choose your password.`),
235 })
236 }
237 if (!state.handle) {
238 dispatch({type: 'setStep', value: SignupStep.HANDLE})
239 return dispatch({
240 type: 'setError',
241 value: _(msg`Please choose your handle.`),
242 })
243 }
244 if (
245 state.serviceDescription?.phoneVerificationRequired &&
246 !verificationCode
247 ) {
248 dispatch({type: 'setStep', value: SignupStep.CAPTCHA})
249 return dispatch({
250 type: 'setError',
251 value: _(msg`Please complete the verification captcha.`),
252 })
253 }
254 dispatch({type: 'setError', value: ''})
255 dispatch({type: 'setIsLoading', value: true})
256
257 try {
258 onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
259 await createAccount({
260 service: state.serviceUrl,
261 email: state.email,
262 handle: createFullHandle(state.handle, state.userDomain),
263 password: state.password,
264 inviteCode: state.inviteCode.trim(),
265 verificationCode: verificationCode,
266 })
267 await setBirthDate({birthDate: state.dateOfBirth})
268 if (IS_PROD_SERVICE(state.serviceUrl)) {
269 setSavedFeeds(DEFAULT_PROD_FEEDS)
270 }
271 } catch (e: any) {
272 onboardingDispatch({type: 'skip'}) // undo starting the onboard
273 let errMsg = e.toString()
274 if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
275 dispatch({
276 type: 'setError',
277 value: _(
278 msg`Invite code not accepted. Check that you input it correctly and try again.`,
279 ),
280 })
281 dispatch({type: 'setStep', value: SignupStep.INFO})
282 return
283 }
284
285 if ([400, 429].includes(e.status)) {
286 logger.warn('Failed to create account', {message: e})
287 } else {
288 logger.error(`Failed to create account (${e.status} status)`, {
289 message: e,
290 })
291 }
292
293 const error = cleanError(errMsg)
294 const isHandleError = error.toLowerCase().includes('handle')
295
296 dispatch({type: 'setIsLoading', value: false})
297 dispatch({type: 'setError', value: cleanError(errMsg)})
298 dispatch({type: 'setStep', value: isHandleError ? 2 : 1})
299 } finally {
300 dispatch({type: 'setIsLoading', value: false})
301 }
302 },
303 [
304 state.email,
305 state.password,
306 state.handle,
307 state.serviceDescription?.phoneVerificationRequired,
308 state.serviceUrl,
309 state.userDomain,
310 state.inviteCode,
311 state.dateOfBirth,
312 dispatch,
313 _,
314 onboardingDispatch,
315 createAccount,
316 setBirthDate,
317 setSavedFeeds,
318 ],
319 )
320}