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} 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}