Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at 8c3553cd66ad07ef8c8c4e760b495cf6ce08cc8d 386 lines 11 kB view raw
1import {useEffect, useMemo, useState} from 'react' 2import {Text as NestedText, View} from 'react-native' 3import { 4 AppBskyContactStartPhoneVerification, 5 AppBskyContactVerifyPhone, 6} from '@atproto/api' 7import {msg, Trans} from '@lingui/macro' 8import {useLingui} from '@lingui/react' 9import {useMutation} from '@tanstack/react-query' 10 11import {clamp} from '#/lib/numbers' 12import {cleanError, isNetworkError} from '#/lib/strings/errors' 13import {logger} from '#/logger' 14import {useAgent} from '#/state/session' 15import {OnboardingPosition} from '#/screens/Onboarding/Layout' 16import {atoms as a, useGutters, useTheme} from '#/alf' 17import {Button, ButtonIcon, ButtonText} from '#/components/Button' 18import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate' 19import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck' 20import {type Props as SVGIconProps} from '#/components/icons/common' 21import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 22import * as Layout from '#/components/Layout' 23import {Loader} from '#/components/Loader' 24import * as Toast from '#/components/Toast' 25import {Text} from '#/components/Typography' 26import {OTPInput} from '../components/OTPInput' 27import {constructFullPhoneNumber, prettyPhoneNumber} from '../phone-number' 28import {type Action, type State, useOnPressBackButton} from '../state' 29 30export function VerifyNumber({ 31 state, 32 dispatch, 33 context, 34 onSkip, 35}: { 36 state: Extract<State, {step: '2: verify number'}> 37 dispatch: React.ActionDispatch<[Action]> 38 context: 'Onboarding' | 'Standalone' 39 onSkip: () => void 40}) { 41 const t = useTheme() 42 const {_} = useLingui() 43 const agent = useAgent() 44 const gutters = useGutters([0, 'wide']) 45 46 const [otpCode, setOtpCode] = useState('') 47 const [error, setError] = useState<{ 48 retryable: boolean 49 isResendError: boolean 50 message: string 51 } | null>(null) 52 53 const [prevOtpCode, setPrevOtpCode] = useState(otpCode) 54 if (otpCode !== prevOtpCode) { 55 setPrevOtpCode(otpCode) 56 setError(null) 57 } 58 59 const phone = useMemo( 60 () => constructFullPhoneNumber(state.phoneCountryCode, state.phoneNumber), 61 [state.phoneCountryCode, state.phoneNumber], 62 ) 63 64 const prettyNumber = useMemo(() => prettyPhoneNumber(phone), [phone]) 65 66 const { 67 mutate: verifyNumber, 68 isPending, 69 isSuccess, 70 } = useMutation({ 71 mutationFn: async (code: string) => { 72 const res = await agent.app.bsky.contact.verifyPhone({code, phone}) 73 return res.data.token 74 }, 75 onSuccess: async token => { 76 // let the success state show for a moment 77 setTimeout(() => { 78 dispatch({ 79 type: 'VERIFY_PHONE_NUMBER_SUCCESS', 80 payload: { 81 token, 82 }, 83 }) 84 }, 1000) 85 86 logger.metric('contacts:phone:phoneVerified', {entryPoint: context}) 87 }, 88 onMutate: () => setError(null), 89 onError: err => { 90 setOtpCode('') 91 if (isNetworkError(err)) { 92 setError({ 93 retryable: true, 94 isResendError: false, 95 message: _( 96 msg`A network error occurred. Please check your internet connection.`, 97 ), 98 }) 99 } else if (err instanceof AppBskyContactVerifyPhone.InvalidCodeError) { 100 setError({ 101 retryable: true, 102 isResendError: true, 103 message: _(msg`This code is invalid. Resend to get a new code.`), 104 }) 105 } else if (err instanceof AppBskyContactVerifyPhone.InvalidPhoneError) { 106 setError({ 107 retryable: false, 108 isResendError: false, 109 message: _( 110 msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, 111 ), 112 }) 113 } else if ( 114 err instanceof AppBskyContactVerifyPhone.RateLimitExceededError 115 ) { 116 setError({ 117 retryable: true, 118 isResendError: false, 119 message: _( 120 msg`Too many attempts. Please wait a few minutes and try again.`, 121 ), 122 }) 123 } else { 124 logger.error('Verify phone number failed', {safeMessage: err}) 125 setError({ 126 retryable: true, 127 isResendError: false, 128 message: _(msg`An error occurred. ${cleanError(err)}`), 129 }) 130 } 131 }, 132 }) 133 134 const {mutate: resendCode, isPending: isResendingCode} = useMutation({ 135 mutationFn: async () => { 136 await agent.app.bsky.contact.startPhoneVerification({phone: phone}) 137 }, 138 onSuccess: () => { 139 dispatch({type: 'RESEND_VERIFICATION_CODE'}) 140 Toast.show(_(msg`A new code has been sent`)) 141 }, 142 onMutate: () => { 143 setOtpCode('') 144 setError(null) 145 }, 146 onError: err => { 147 if (isNetworkError(err)) { 148 setError({ 149 retryable: true, 150 isResendError: true, 151 message: _( 152 msg`A network error occurred. Please check your internet connection.`, 153 ), 154 }) 155 } else if ( 156 err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError 157 ) { 158 setError({ 159 retryable: false, 160 isResendError: true, 161 message: _( 162 msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, 163 ), 164 }) 165 } else if ( 166 err instanceof 167 AppBskyContactStartPhoneVerification.RateLimitExceededError 168 ) { 169 setError({ 170 retryable: true, 171 isResendError: true, 172 message: _( 173 msg`Too many codes sent. Please wait a few minutes and try again.`, 174 ), 175 }) 176 } else { 177 logger.error('Resend failed', {safeMessage: err}) 178 setError({ 179 retryable: true, 180 isResendError: true, 181 message: _(msg`An error occurred. ${cleanError(err)}`), 182 }) 183 } 184 }, 185 }) 186 187 const onPressBack = useOnPressBackButton() 188 189 return ( 190 <View style={[a.h_full]}> 191 <Layout.Header.Outer noBottomBorder> 192 <Layout.Header.BackButton onPress={onPressBack} /> 193 <Layout.Header.Content /> 194 {context === 'Onboarding' ? ( 195 <Button 196 size="small" 197 color="secondary" 198 variant="ghost" 199 label={_(msg`Skip contact sharing and continue to the app`)} 200 onPress={onSkip}> 201 <ButtonText> 202 <Trans>Skip</Trans> 203 </ButtonText> 204 </Button> 205 ) : ( 206 <Layout.Header.Slot /> 207 )} 208 </Layout.Header.Outer> 209 <Layout.Content 210 contentContainerStyle={[gutters, a.pt_sm, a.flex_1]} 211 keyboardShouldPersistTaps="always"> 212 {context === 'Onboarding' && <OnboardingPosition />} 213 <Text style={[a.font_bold, a.text_3xl]}> 214 <Trans>Verify phone number</Trans> 215 </Text> 216 <Text 217 style={[ 218 a.text_md, 219 t.atoms.text_contrast_medium, 220 a.leading_snug, 221 a.mt_sm, 222 ]}> 223 <Trans>Enter the 6-digit code sent to {prettyNumber}</Trans> 224 </Text> 225 <View style={[a.mt_2xl]}> 226 <OTPInput 227 label={_( 228 msg`Enter 6-digit code that was sent to your phone number`, 229 )} 230 value={otpCode} 231 onChange={setOtpCode} 232 onComplete={code => verifyNumber(code)} 233 /> 234 </View> 235 <View style={[a.mt_sm]}> 236 <OTPStatus 237 error={error} 238 isPending={isPending} 239 isResendingCode={isResendingCode} 240 isSuccess={isSuccess} 241 onResend={() => resendCode()} 242 onRetry={() => verifyNumber(otpCode)} 243 lastCodeSentAt={state.lastSentAt} 244 /> 245 </View> 246 </Layout.Content> 247 </View> 248 ) 249} 250 251/** 252 * Horrible component that takes all the state above and figures out what messages 253 * and buttons to display. 254 */ 255function OTPStatus({ 256 error, 257 isPending, 258 isResendingCode, 259 isSuccess, 260 onResend, 261 onRetry, 262 lastCodeSentAt, 263}: { 264 error: { 265 retryable: boolean 266 isResendError: boolean 267 message: string 268 } | null 269 isPending: boolean 270 isResendingCode: boolean 271 isSuccess: boolean 272 onResend: () => void 273 onRetry: () => void 274 lastCodeSentAt: Date | null 275}) { 276 const {_} = useLingui() 277 const t = useTheme() 278 279 const [time, setTime] = useState(Date.now()) 280 useEffect(() => { 281 const interval = setInterval(() => { 282 setTime(Date.now()) 283 }, 1000) 284 return () => clearInterval(interval) 285 }, []) 286 287 const timeUntilCanResend = Math.max( 288 0, 289 30000 - (time - (lastCodeSentAt?.getTime() ?? 0)), 290 ) 291 const isWaiting = timeUntilCanResend > 0 292 293 let Icon: React.ComponentType<SVGIconProps> | null = null 294 let text = '' 295 let textColor = t.atoms.text_contrast_medium.color 296 let showResendButton = false 297 let showRetryButton = false 298 299 if (isSuccess) { 300 Icon = CircleCheckIcon 301 text = _(msg`Phone number verified`) 302 textColor = t.palette.positive_500 303 } else if (isPending) { 304 text = _(msg`Verifying...`) 305 } else if (error) { 306 Icon = WarningIcon 307 text = error.message 308 textColor = t.palette.negative_500 309 if (error.retryable) { 310 if (error.isResendError) { 311 showResendButton = true 312 } else { 313 showRetryButton = true 314 } 315 } 316 } else { 317 showResendButton = true 318 } 319 320 return ( 321 <View style={[a.w_full, a.align_center]}> 322 {text && ( 323 <View 324 style={[ 325 a.gap_xs, 326 a.flex_row, 327 a.align_center, 328 (isSuccess || isPending) && a.mt_lg, 329 ]}> 330 {Icon && <Icon size="xs" style={{color: textColor}} />} 331 <Text 332 style={[ 333 {color: textColor}, 334 a.text_sm, 335 a.leading_snug, 336 a.text_center, 337 ]}> 338 {text} 339 </Text> 340 </View> 341 )} 342 343 {showRetryButton && ( 344 <Button 345 size="small" 346 color="secondary_inverted" 347 label={_(msg`Retry`)} 348 onPress={onRetry} 349 style={[a.mt_2xl]}> 350 <ButtonIcon icon={RetryIcon} /> 351 <ButtonText> 352 <Trans>Retry</Trans> 353 </ButtonText> 354 </Button> 355 )} 356 357 {showResendButton && ( 358 <Button 359 size="large" 360 color="secondary" 361 variant="ghost" 362 label={_(msg`Resend code`)} 363 disabled={isResendingCode || isWaiting} 364 onPress={onResend} 365 style={[a.mt_2xl]}> 366 {isResendingCode && <ButtonIcon icon={Loader} />} 367 <ButtonText> 368 {isWaiting ? ( 369 <Trans> 370 Resend code in{' '} 371 <NestedText style={{fontVariant: ['tabular-nums']}}> 372 00: 373 {String( 374 clamp(Math.round(timeUntilCanResend / 1000), 0, 30), 375 ).padStart(2, '0')} 376 </NestedText> 377 </Trans> 378 ) : ( 379 <Trans>Resend code</Trans> 380 )} 381 </ButtonText> 382 </Button> 383 )} 384 </View> 385 ) 386}