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

Configure Feed

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

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