mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at session-alignment 320 lines 8.7 kB view raw
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}