Bluesky app fork with some witchin' additions 馃挮
at main 354 lines 12 kB view raw
1import {useState} from 'react' 2import {View} from 'react-native' 3import {XRPCError} from '@atproto/xrpc' 4import {msg} from '@lingui/core/macro' 5import {useLingui} from '@lingui/react' 6import {Trans} from '@lingui/react/macro' 7import {validate as validateEmail} from 'email-validator' 8 9import {useCleanError} from '#/lib/hooks/useCleanError' 10import { 11 SupportCode, 12 useCreateSupportLink, 13} from '#/lib/hooks/useCreateSupportLink' 14import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 15import {useTLDs} from '#/lib/hooks/useTLDs' 16import {isEmailMaybeInvalid} from '#/lib/strings/email' 17import {type AppLanguage} from '#/locale/languages' 18import {useLanguagePrefs} from '#/state/preferences' 19import {useSession} from '#/state/session' 20import {atoms as a, web} from '#/alf' 21import {Admonition} from '#/components/Admonition' 22import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 23import {KWS_SUPPORTED_LANGS, urls} from '#/components/ageAssurance/const' 24import {Button, ButtonIcon, ButtonText} from '#/components/Button' 25import * as Dialog from '#/components/Dialog' 26import {Divider} from '#/components/Divider' 27import * as TextField from '#/components/forms/TextField' 28import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' 29import {LanguageSelect} from '#/components/LanguageSelect' 30import {SimpleInlineLinkText} from '#/components/Link' 31import {Loader} from '#/components/Loader' 32import {Text} from '#/components/Typography' 33import {useAgeAssurance} from '#/ageAssurance' 34import {useBeginAgeAssurance} from '#/ageAssurance/useBeginAgeAssurance' 35import {useAnalytics} from '#/analytics' 36 37export {useDialogControl} from '#/components/Dialog/context' 38 39export function AgeAssuranceInitDialog({ 40 control, 41}: { 42 control: Dialog.DialogControlProps 43}) { 44 const {_} = useLingui() 45 return ( 46 <Dialog.Outer control={control}> 47 <Dialog.Handle /> 48 49 <Dialog.ScrollableInner 50 label={_( 51 msg`Begin the age assurance process by completing the fields below.`, 52 )} 53 style={[ 54 web({ 55 maxWidth: 400, 56 }), 57 ]}> 58 <Inner /> 59 <Dialog.Close /> 60 </Dialog.ScrollableInner> 61 </Dialog.Outer> 62 ) 63} 64 65function Inner() { 66 const {_} = useLingui() 67 const ax = useAnalytics() 68 const {currentAccount} = useSession() 69 const langPrefs = useLanguagePrefs() 70 const cleanError = useCleanError() 71 const {close} = Dialog.useDialogContext() 72 const aa = useAgeAssurance() 73 const lastInitiatedAt = aa.state.lastInitiatedAt 74 const getTimeAgo = useGetTimeAgo() 75 const tlds = useTLDs() 76 const createSupportLink = useCreateSupportLink() 77 78 const wasRecentlyInitiated = 79 lastInitiatedAt && 80 new Date(lastInitiatedAt).getTime() > Date.now() - 5 * 60 * 1000 // 5 minutes 81 82 const [success, setSuccess] = useState(false) 83 const [email, setEmail] = useState(currentAccount?.email || '') 84 const [emailError, setEmailError] = useState<string>('') 85 const [languageError, setLanguageError] = useState(false) 86 const [disabled, setDisabled] = useState(false) 87 const [language, setLanguage] = useState<string | undefined>( 88 convertToKWSSupportedLanguage(langPrefs.appLanguage), 89 ) 90 const [error, setError] = useState<React.ReactNode>(null) 91 92 const {mutateAsync: begin, isPending} = useBeginAgeAssurance() 93 94 const runEmailValidation = () => { 95 if (validateEmail(email)) { 96 setEmailError('') 97 setDisabled(false) 98 99 if (tlds && isEmailMaybeInvalid(email, tlds)) { 100 setEmailError( 101 _( 102 msg`Please double-check that you have entered your email address correctly.`, 103 ), 104 ) 105 return {status: 'maybe'} 106 } 107 108 return {status: 'valid'} 109 } 110 111 setEmailError(_(msg`Please enter a valid email address.`)) 112 setDisabled(true) 113 114 return {status: 'invalid'} 115 } 116 117 const onSubmit = async () => { 118 setLanguageError(false) 119 120 ax.metric('ageAssurance:initDialogSubmit', {}) 121 122 try { 123 const {status} = runEmailValidation() 124 125 if (status === 'invalid') return 126 if (!language) { 127 setLanguageError(true) 128 return 129 } 130 131 await begin({ 132 email, 133 language, 134 }) 135 136 setSuccess(true) 137 } catch (e) { 138 let error: React.ReactNode = _( 139 msg`Something went wrong, please try again`, 140 ) 141 142 if (e instanceof XRPCError) { 143 if (e.error === 'InvalidEmail') { 144 error = _( 145 msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`, 146 ) 147 ax.metric('ageAssurance:initDialogError', {code: 'InvalidEmail'}) 148 } else if (e.error === 'DidTooLong') { 149 error = ( 150 <> 151 <Trans> 152 We're having issues initializing the age assurance process for 153 your account. Please{' '} 154 <SimpleInlineLinkText 155 to={createSupportLink({code: SupportCode.AA_DID, email})} 156 label={_(msg`Contact support`)}> 157 contact support 158 </SimpleInlineLinkText>{' '} 159 for assistance. 160 </Trans> 161 </> 162 ) 163 ax.metric('ageAssurance:initDialogError', {code: 'DidTooLong'}) 164 } else { 165 ax.metric('ageAssurance:initDialogError', {code: 'other'}) 166 } 167 } else { 168 const {clean, raw} = cleanError(e) 169 error = clean || raw || error 170 ax.metric('ageAssurance:initDialogError', {code: 'other'}) 171 } 172 173 setError(error) 174 } 175 } 176 177 return ( 178 <View> 179 <View style={[a.align_start]}> 180 <AgeAssuranceBadge /> 181 182 <Text style={[a.text_xl, a.font_bold, a.pt_xl, a.pb_md]}> 183 {success ? <Trans>Success!</Trans> : <Trans>Verify your age</Trans>} 184 </Text> 185 186 <View style={[a.pb_xl, a.gap_sm]}> 187 {success ? ( 188 <Text style={[a.text_sm, a.leading_snug]}> 189 <Trans> 190 Please check your email inbox for further instructions. It may 191 take a minute or two to arrive. 192 </Trans> 193 </Text> 194 ) : ( 195 <> 196 <Text style={[a.text_sm, a.leading_snug]}> 197 <Trans> 198 We have partnered with{' '} 199 <SimpleInlineLinkText 200 label={_(msg`KWS website`)} 201 to={urls.kwsHome} 202 style={[a.text_sm, a.leading_snug]}> 203 KWS 204 </SimpleInlineLinkText>{' '} 205 to handle age verification. When you click "Begin" below, KWS 206 will email you instructions to complete the verification 207 process. If your email address has already been used to verify 208 your age for another game or service that uses KWS, you won鈥檛 209 need to do it again. When you鈥檙e done, you'll be brought back 210 to continue using Bluesky. 211 </Trans> 212 </Text> 213 <Text style={[a.text_sm, a.leading_snug]}> 214 <Trans>This should only take a few minutes.</Trans> 215 </Text> 216 </> 217 )} 218 </View> 219 220 {success ? ( 221 <View style={[a.w_full]}> 222 <Button 223 label={_(msg`Close dialog`)} 224 size="large" 225 variant="solid" 226 color="secondary" 227 onPress={() => close()}> 228 <ButtonText> 229 <Trans>Close dialog</Trans> 230 </ButtonText> 231 </Button> 232 </View> 233 ) : ( 234 <> 235 <Divider /> 236 237 <View style={[a.w_full, a.pt_xl, a.gap_lg, a.pb_lg]}> 238 {wasRecentlyInitiated && ( 239 <Admonition type="warning"> 240 <Trans> 241 You initiated this flow already,{' '} 242 {getTimeAgo(lastInitiatedAt, new Date(), {format: 'long'})}{' '} 243 ago. It may take up to 5 minutes for emails to reach your 244 inbox. Please consider waiting a few minutes before trying 245 again. 246 </Trans> 247 </Admonition> 248 )} 249 250 <View> 251 <TextField.LabelText> 252 <Trans>Your email</Trans> 253 </TextField.LabelText> 254 <TextField.Root isInvalid={!!emailError}> 255 <TextField.Input 256 label={_(msg`Your email`)} 257 placeholder={_(msg`Your email`)} 258 value={email} 259 onChangeText={setEmail} 260 onFocus={() => setEmailError('')} 261 onBlur={() => { 262 runEmailValidation() 263 }} 264 returnKeyType="done" 265 autoCapitalize="none" 266 autoComplete="off" 267 autoCorrect={false} 268 onSubmitEditing={onSubmit} 269 /> 270 </TextField.Root> 271 272 {emailError ? ( 273 <Admonition type="error" style={[a.mt_sm]}> 274 {emailError} 275 </Admonition> 276 ) : ( 277 <Admonition type="tip" style={[a.mt_sm]}> 278 <Trans> 279 Use your account email address, or another real email 280 address you control, in case KWS or Bluesky needs to 281 contact you. 282 </Trans> 283 </Admonition> 284 )} 285 </View> 286 287 <View> 288 <TextField.LabelText> 289 <Trans>Your preferred language</Trans> 290 </TextField.LabelText> 291 <LanguageSelect 292 label={_(msg`Preferred language`)} 293 value={language} 294 onChange={value => { 295 setLanguage(value) 296 setLanguageError(false) 297 }} 298 items={KWS_SUPPORTED_LANGS} 299 /> 300 301 {languageError && ( 302 <Admonition type="error" style={[a.mt_sm]}> 303 <Trans>Please select a language</Trans> 304 </Admonition> 305 )} 306 </View> 307 308 {error && <Admonition type="error">{error}</Admonition>} 309 310 <Button 311 disabled={disabled} 312 label={_(msg`Begin age assurance process`)} 313 size="large" 314 variant="solid" 315 color="primary" 316 onPress={onSubmit}> 317 <ButtonText> 318 <Trans>Begin</Trans> 319 </ButtonText> 320 <ButtonIcon 321 icon={isPending ? Loader : Shield} 322 position="right" 323 /> 324 </Button> 325 </View> 326 </> 327 )} 328 </View> 329 </View> 330 ) 331} 332 333// best-effort mapping of our languages to KWS supported languages 334function convertToKWSSupportedLanguage( 335 appLanguage: string, 336): string | undefined { 337 // `${Enum}` is how you get a type of string union of the enum values (???) -sfn 338 switch (appLanguage as `${AppLanguage}`) { 339 // only en is supported 340 case 'en-GB': 341 return 'en' 342 // pt-PT is pt (pt-BR is supported independently) 343 case 'pt-PT': 344 return 'pt' 345 // only chinese (simplified) is supported, map all chinese variants 346 case 'zh-Hans-CN': 347 case 'zh-Hant-HK': 348 case 'zh-Hant-TW': 349 return 'zh-Hans' 350 default: 351 // try and map directly - if undefined, they will have to pick from the dropdown 352 return KWS_SUPPORTED_LANGS.find(v => v.value === appLanguage)?.value 353 } 354}