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

Configure Feed

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

at 999e52ed2d5a2c8b2f7b8747dfcfd0e2017e5eb0 329 lines 10 kB view raw
1import {useState} from 'react' 2import {Keyboard, View} from 'react-native' 3import {KeyboardAvoidingView} from 'react-native-keyboard-controller' 4import {useSafeAreaInsets} from 'react-native-safe-area-context' 5import {AppBskyContactStartPhoneVerification} from '@atproto/api' 6import {msg, Trans} from '@lingui/macro' 7import {useLingui} from '@lingui/react' 8import {useMutation} from '@tanstack/react-query' 9 10import {urls} from '#/lib/constants' 11import { 12 type CountryCode, 13 getDefaultCountry, 14} from '#/lib/international-telephone-codes' 15import {cleanError, isNetworkError} from '#/lib/strings/errors' 16import {logger} from '#/logger' 17import {useAgent} from '#/state/session' 18import {OnboardingPosition} from '#/screens/Onboarding/Layout' 19import { 20 android, 21 atoms as a, 22 platform, 23 tokens, 24 useGutters, 25 useTheme, 26} from '#/alf' 27import {Button, ButtonIcon, ButtonText} from '#/components/Button' 28import * as TextField from '#/components/forms/TextField' 29import {InternationalPhoneCodeSelect} from '#/components/InternationalPhoneCodeSelect' 30import * as Layout from '#/components/Layout' 31import {InlineLinkText} from '#/components/Link' 32import {Loader} from '#/components/Loader' 33import {Text} from '#/components/Typography' 34import {useAnalytics} from '#/analytics' 35import {useGeolocation} from '#/geolocation' 36import {isFindContactsFeatureEnabled} from '../country-allowlist' 37import { 38 constructFullPhoneNumber, 39 getCountryCodeFromPastedNumber, 40 processPhoneNumber, 41} from '../phone-number' 42import {type Action, type State, useOnPressBackButton} from '../state' 43 44export function PhoneInput({ 45 state, 46 dispatch, 47 context, 48 onSkip, 49}: { 50 state: Extract<State, {step: '1: phone input'}> 51 dispatch: React.ActionDispatch<[Action]> 52 context: 'Onboarding' | 'Standalone' 53 onSkip: () => void 54}) { 55 const {_} = useLingui() 56 const ax = useAnalytics() 57 const t = useTheme() 58 const agent = useAgent() 59 const location = useGeolocation() 60 const [countryCode, setCountryCode] = useState( 61 () => state.phoneCountryCode ?? getDefaultCountry(location), 62 ) 63 const [phoneNumber, setPhoneNumber] = useState(state.phoneNumber ?? '') 64 const gutters = useGutters([0, 'wide']) 65 const insets = useSafeAreaInsets() 66 // for API/generic errors 67 const [error, setError] = useState('') 68 // for issues with parsing the number 69 const [formatError, setFormatError] = useState('') 70 71 const {mutate: submit, isPending} = useMutation({ 72 mutationFn: async ({ 73 phoneCountryCode, 74 phoneNumber, 75 }: { 76 phoneCountryCode: CountryCode 77 phoneNumber: string 78 }) => { 79 // sends a onetime code to the user's phone number 80 await agent.app.bsky.contact.startPhoneVerification({ 81 phone: constructFullPhoneNumber(phoneCountryCode, phoneNumber), 82 }) 83 }, 84 onSuccess: (_data, {phoneCountryCode, phoneNumber}) => { 85 dispatch({ 86 type: 'SUBMIT_PHONE_NUMBER', 87 payload: {phoneCountryCode, phoneNumber}, 88 }) 89 90 ax.metric('contacts:phone:phoneEntered', {entryPoint: context}) 91 }, 92 onMutate: () => { 93 Keyboard.dismiss() 94 setError('') 95 setFormatError('') 96 }, 97 onError: err => { 98 if (isNetworkError(err)) { 99 setError( 100 _( 101 msg`A network error occurred. Please check your internet connection`, 102 ), 103 ) 104 } else if ( 105 err instanceof 106 AppBskyContactStartPhoneVerification.RateLimitExceededError 107 ) { 108 setError(_(msg`Rate limit exceeded. Please try again later.`)) 109 } else if ( 110 err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError 111 ) { 112 setError( 113 _( 114 msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, 115 ), 116 ) 117 } else { 118 logger.error('Verify phone number failed', {safeMessage: err}) 119 setError(_(msg`An error occurred. ${cleanError(err)}`)) 120 } 121 }, 122 }) 123 124 const isFeatureEnabled = isFindContactsFeatureEnabled(countryCode) 125 126 const onSubmitNumber = () => { 127 if (!isFeatureEnabled) return 128 if (!phoneNumber) return 129 const result = processPhoneNumber(phoneNumber, countryCode) 130 if (result.valid) { 131 setPhoneNumber(result.formatted) 132 setCountryCode(result.countryCode) 133 134 if (!isFindContactsFeatureEnabled(result.countryCode)) return 135 136 submit({ 137 phoneCountryCode: result.countryCode, 138 phoneNumber: result.formatted, 139 }) 140 } else { 141 setFormatError(result.reason ?? _(msg`Invalid phone number`)) 142 } 143 } 144 145 const paddingBottom = Math.max(insets.bottom, tokens.space.xl) 146 147 const onPressBack = useOnPressBackButton() 148 149 return ( 150 <View style={[a.h_full]}> 151 <Layout.Header.Outer noBottomBorder> 152 <Layout.Header.BackButton onPress={onPressBack} /> 153 <Layout.Header.Content /> 154 {context === 'Onboarding' ? ( 155 <Button 156 size="small" 157 color="secondary" 158 variant="ghost" 159 label={_(msg`Skip contact sharing and continue to the app`)} 160 onPress={onSkip}> 161 <ButtonText> 162 <Trans>Skip</Trans> 163 </ButtonText> 164 </Button> 165 ) : ( 166 <Layout.Header.Slot /> 167 )} 168 </Layout.Header.Outer> 169 <Layout.Content 170 contentContainerStyle={[gutters, a.pt_sm, a.flex_1]} 171 keyboardShouldPersistTaps="handled"> 172 {context === 'Onboarding' && <OnboardingPosition />} 173 <Text style={[a.font_bold, a.text_3xl]}> 174 <Trans>Verify phone number</Trans> 175 </Text> 176 <Text 177 style={[ 178 a.text_md, 179 t.atoms.text_contrast_medium, 180 a.leading_snug, 181 a.mt_sm, 182 ]}> 183 <Trans> 184 We need to verify your number before we can look for your friends. A 185 verification code will be sent to this number. 186 </Trans> 187 </Text> 188 189 <View style={[a.mt_2xl]}> 190 <TextField.LabelText> 191 <Trans>Phone number</Trans> 192 </TextField.LabelText> 193 <View style={[a.flex_row, a.gap_sm, a.align_center]}> 194 <View> 195 <InternationalPhoneCodeSelect 196 value={countryCode} 197 onChange={value => setCountryCode(value)} 198 /> 199 </View> 200 <View style={[a.flex_1]}> 201 <TextField.Root isInvalid={!!formatError || !isFeatureEnabled}> 202 <TextField.Input 203 label={_(msg`Phone number`)} 204 value={phoneNumber} 205 onChangeText={text => { 206 if (formatError) setFormatError('') 207 if (Math.abs(text.length - phoneNumber.length) > 1) { 208 // possibly pasted/autocompleted? auto-switch 209 // country code if possible 210 const result = getCountryCodeFromPastedNumber(text) 211 if (result) { 212 setCountryCode(result.countryCode) 213 setPhoneNumber(result.rest) 214 return 215 } 216 } 217 setPhoneNumber(text) 218 }} 219 placeholder={null} 220 keyboardType={platform({ 221 ios: 'number-pad', 222 android: 'phone-pad', 223 })} 224 autoComplete="tel" 225 returnKeyType={android('next')} 226 onSubmitEditing={onSubmitNumber} 227 /> 228 </TextField.Root> 229 </View> 230 </View> 231 </View> 232 233 {!isFeatureEnabled && ( 234 <ErrorText> 235 <Trans> 236 Support for this feature in your country has not been enabled yet! 237 Please check back later. 238 </Trans> 239 </ErrorText> 240 )} 241 {error && <ErrorText>{error}</ErrorText>} 242 {formatError && <ErrorText>{formatError}</ErrorText>} 243 244 <View style={[a.mt_auto, a.py_xl]}> 245 <LegalDisclaimer /> 246 </View> 247 </Layout.Content> 248 <KeyboardAvoidingView 249 behavior="padding" 250 keyboardVerticalOffset={insets.top - paddingBottom + tokens.space.xl}> 251 <View style={[gutters, {paddingBottom}]}> 252 <Button 253 disabled={!phoneNumber || isPending} 254 label={_(msg`Send code`)} 255 size="large" 256 color="primary" 257 onPress={onSubmitNumber}> 258 <ButtonText> 259 <Trans>Send code</Trans> 260 </ButtonText> 261 {isPending && <ButtonIcon icon={Loader} />} 262 </Button> 263 </View> 264 </KeyboardAvoidingView> 265 </View> 266 ) 267} 268 269function LegalDisclaimer() { 270 const t = useTheme() 271 const {_} = useLingui() 272 273 const style = [a.text_xs, t.atoms.text_contrast_medium, a.leading_snug] 274 275 return ( 276 <View style={[a.gap_xs]}> 277 <Text style={[style, a.font_medium]}> 278 <Trans>How we use your number:</Trans> 279 </Text> 280 <Text style={style}> 281 &bull;{' '} 282 <Trans>Sent to our phone number verification provider Plivo</Trans> 283 </Text> 284 <Text style={style}> 285 &bull; <Trans>Deleted by Plivo after verification</Trans> 286 </Text> 287 <Text style={style}> 288 &bull;{' '} 289 <Trans>Held by Bluesky for 7 days to prevent abuse, then deleted</Trans> 290 </Text> 291 <Text style={style}> 292 &bull;{' '} 293 <Trans>Stored as part of a secure code for matching with others</Trans> 294 </Text> 295 <Text style={[style, a.mt_xs]}> 296 <Trans> 297 By continuing, you consent to this use. You may change your mind any 298 time by visiting settings.{' '} 299 <InlineLinkText 300 to={urls.website.support.findFriendsPrivacyPolicy} 301 label={_( 302 msg({ 303 message: `Learn more about importing contacts`, 304 context: `english-only-resource`, 305 }), 306 )} 307 style={[a.text_xs, a.leading_snug]}> 308 <Trans context="english-only-resource">Learn more</Trans> 309 </InlineLinkText> 310 </Trans> 311 </Text> 312 </View> 313 ) 314} 315 316function ErrorText({children}: {children: React.ReactNode}) { 317 const t = useTheme() 318 return ( 319 <Text 320 style={[ 321 a.text_md, 322 {color: t.palette.negative_500}, 323 a.leading_snug, 324 a.mt_md, 325 ]}> 326 {children} 327 </Text> 328 ) 329}