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