diff options
author | Eric Bailey <git@esb.lol> | 2025-07-16 13:58:07 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-07-16 13:58:07 -0500 |
commit | 1dbc331314278cb7a42ded9b190dac7038ad9878 (patch) | |
tree | b5d44e1ea75ea9d5343eec90425c8c7ac74df39f /src/components/ageAssurance/AgeAssuranceInitDialog.tsx | |
parent | 712c3ad4211e2e68d0cdbcc480967c63aeaa6c0e (diff) | |
download | voidsky-1dbc331314278cb7a42ded9b190dac7038ad9878.tar.zst |
UI for age assurance compliance (#8652)
* Add geo prop * Add prelim fetch * Add geo debug * Pass in assurance state to notifications registration * Comments * Bump git index * Add some component utils, no design, gate chat * Disable mod prefs buttons, does not yet edit mod prefs * Add initial prompt component * Refine logic for showing prompt * Add send email dialog * Hook up dialog to fake mutation * Fix geo debug bug * Move provider inside query provider * Slightly better screen gater * Ok decent fallback with isExempt * Reorg * Wrap prompt in new logic * Override mod prefs * Use real endpoints, optimistic state * Add persistent card, add time-ago, warning to dialog * Add comment * No undefined query values * Fix case in import * Wait for AA to load before registering push * Override prefs in all locations * Small refactor of notifications registration * Register push after aa state * Add retries * Update blocked screens UI * Strengthen email validation * Add intent dialog * Do service auth for init * Rug refreshJwt * Update copy * Some mobile styles, add dev mode option * Fix links on native * Clean up intent dialog on native * Don't mutate existing session, only copy * Handle email validation error from server * Clarity is better * Moar clear * Fixes * Tweaks * Add country code * Gate it * Refresh state after redirect * Re-check on window focus * Remove todo * Enable in dev * Check for did match on redirect * Add blocked state * Add appeal dialog * Copy tweaks * Inset in blue well * Nux the prompt * Copy updates * Refetch just in case * Uppercase country code * Align copy, add notice to chat screens * Tweak copy * Add test code * Add debug code * Refactor AccountCard * Big refactor * Delay post-feed queries instead * Debug code * Clean up state * Reorg * Clean up copy * Comments * Reorg * UPdate URL * Cleanup * Remove todo * Update debug code * revert unneeded changes * UPdate nux name * Revert unneeded change * Updaet storage schema * Checkpoint: cleanup * Checkpoint: almost there * isLoaded -> isReady * Rename useAgeAssurance * isUnderage -> isDeclaredUnderage * Decompose, add docblocks * Refactor * UPdate debug * Apply suggestion from @surfdude29 Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Apply suggestion from @surfdude29 Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Apply suggestion from @surfdude29 Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Apply suggestion from @surfdude29 Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Apply suggestion from @surfdude29 Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Apply suggestion from @surfdude29 Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Drop including Bluesky * Apply suggestion from @surfdude29 Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Apply suggestion from @surfdude29 Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Remove todo * Gate debug * Revert unneeded change * Fail closed * Comments * Comment * Comment * fix prettier * rm viewheader * bump sdk * prevent overlap in admonition * add age assurance intent route * Just meow Co-authored-by: Samuel Newman <mozzius@protonmail.com> * Nix callback * Fix spelling of dismissible lol * Don't compare translated string * Better KWS link labels * Hide DMs send options in menu * Add button * Fix order * Use only supported languages * Rm button * best-effort language mapping * improve typing --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Diffstat (limited to 'src/components/ageAssurance/AgeAssuranceInitDialog.tsx')
-rw-r--r-- | src/components/ageAssurance/AgeAssuranceInitDialog.tsx | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/src/components/ageAssurance/AgeAssuranceInitDialog.tsx b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx new file mode 100644 index 000000000..ad13cc1c2 --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx @@ -0,0 +1,351 @@ +import {useState} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {validate as validateEmail} from 'email-validator' + +import {useCleanError} from '#/lib/hooks/useCleanError' +import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' +import {useTLDs} from '#/lib/hooks/useTLDs' +import {isEmailMaybeInvalid} from '#/lib/strings/email' +import {type AppLanguage} from '#/locale/languages' +import {useAgeAssuranceContext} from '#/state/ageAssurance' +import {useInitAgeAssurance} from '#/state/ageAssurance/useInitAgeAssurance' +import {useLanguagePrefs} from '#/state/preferences' +import {useSession} from '#/state/session' +import {atoms as a, useTheme, web} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' +import {urls} from '#/components/ageAssurance/const' +import {KWS_SUPPORTED_LANGS} from '#/components/ageAssurance/const' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Divider} from '#/components/Divider' +import * as TextField from '#/components/forms/TextField' +import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' +import {LanguageSelect} from '#/components/LanguageSelect' +import {InlineLinkText} from '#/components/Link' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +export {useDialogControl} from '#/components/Dialog/context' + +export function AgeAssuranceInitDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const {_} = useLingui() + return ( + <Dialog.Outer control={control}> + <Dialog.Handle /> + + <Dialog.ScrollableInner + label={_( + msg`Begin the age assurance process by completing the fields below.`, + )} + style={[ + web({ + maxWidth: 400, + }), + ]}> + <Inner /> + <Dialog.Close /> + </Dialog.ScrollableInner> + </Dialog.Outer> + ) +} + +function Inner() { + const t = useTheme() + const {_} = useLingui() + const {currentAccount} = useSession() + const langPrefs = useLanguagePrefs() + const cleanError = useCleanError() + const {close} = Dialog.useDialogContext() + const {lastInitiatedAt} = useAgeAssuranceContext() + const getTimeAgo = useGetTimeAgo() + const tlds = useTLDs() + + const wasRecentlyInitiated = + lastInitiatedAt && + new Date(lastInitiatedAt).getTime() > Date.now() - 5 * 60 * 1000 // 5 minutes + + const [success, setSuccess] = useState(false) + const [email, setEmail] = useState(currentAccount?.email || '') + const [emailError, setEmailError] = useState<string>('') + const [languageError, setLanguageError] = useState(false) + const [disabled, setDisabled] = useState(false) + const [language, setLanguage] = useState<string | undefined>( + convertToKWSSupportedLanguage(langPrefs.appLanguage), + ) + const [error, setError] = useState<string>('') + + const {mutateAsync: init, isPending} = useInitAgeAssurance() + + const runEmailValidation = () => { + if (validateEmail(email)) { + setEmailError('') + setDisabled(false) + + if (tlds && isEmailMaybeInvalid(email, tlds)) { + setEmailError( + _( + msg`Please double-check that you have entered your email address correctly.`, + ), + ) + return {status: 'maybe'} + } + + return {status: 'valid'} + } + + setEmailError(_(msg`Please enter a valid email address.`)) + setDisabled(true) + + return {status: 'invalid'} + } + + const onSubmit = async () => { + setLanguageError(false) + + try { + const {status} = runEmailValidation() + + if (status === 'invalid') return + if (!language) { + setLanguageError(true) + return + } + + await init({ + email, + language, + }) + + setSuccess(true) + } catch (e) { + const {clean, raw} = cleanError(e) + + if (clean) { + setError(clean || _(msg`Something went wrong, please try again`)) + } else { + let message = _(msg`Something went wrong, please try again`) + + if (raw) { + if (raw.startsWith('This email address is not supported')) { + message = _( + msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`, + ) + } + } + + setError(message) + } + } + } + + return ( + <View> + <View style={[a.align_start]}> + <AgeAssuranceBadge /> + + <Text style={[a.text_xl, a.font_heavy, a.pt_xl, a.pb_md]}> + {success ? <Trans>Success!</Trans> : <Trans>Verify your age</Trans>} + </Text> + + <View style={[a.pb_xl, a.gap_sm]}> + {success ? ( + <Text style={[a.text_sm, a.leading_snug]}> + <Trans> + Please check your email inbox for further instructions. It may + take a minute or two to arrive. + </Trans> + </Text> + ) : ( + <> + <Text style={[a.text_sm, a.leading_snug]}> + <Trans> + We use{' '} + <InlineLinkText + overridePresentation + disableMismatchWarning + label={_(msg`KWS website`)} + to={urls.kwsHome} + style={[a.text_sm, a.leading_snug]}> + KWS + </InlineLinkText>{' '} + to verify that you’re an adult. When you click "Begin" below, + KWS will email you instructions for verifying your age. When + you’re done, you'll be brought back to continue using Bluesky. + </Trans> + </Text> + <Text style={[a.text_sm, a.leading_snug]}> + <Trans>This should only take a few minutes.</Trans> + </Text> + </> + )} + </View> + + {success ? ( + <View style={[a.w_full]}> + <Button + label={_(msg`Close dialog`)} + size="large" + variant="solid" + color="secondary" + onPress={() => close()}> + <ButtonText> + <Trans>Close dialog</Trans> + </ButtonText> + </Button> + </View> + ) : ( + <> + <Divider /> + + <View style={[a.w_full, a.pt_xl, a.gap_lg, a.pb_lg]}> + {wasRecentlyInitiated && ( + <Admonition type="warning"> + <Trans> + You initiated this flow already,{' '} + {getTimeAgo(lastInitiatedAt, new Date(), {format: 'long'})}{' '} + ago. It may take up to 5 minutes for emails to reach your + inbox. Please consider waiting a few minutes before trying + again. + </Trans> + </Admonition> + )} + + <View> + <TextField.LabelText> + <Trans>Your email</Trans> + </TextField.LabelText> + <TextField.Root isInvalid={!!emailError}> + <TextField.Input + label={_(msg`Your email`)} + placeholder={_(msg`Your email`)} + value={email} + onChangeText={setEmail} + onFocus={() => setEmailError('')} + onBlur={() => { + runEmailValidation() + }} + returnKeyType="done" + autoCapitalize="none" + autoComplete="off" + autoCorrect={false} + onSubmitEditing={onSubmit} + /> + </TextField.Root> + + {emailError ? ( + <Admonition type="error" style={[a.mt_sm]}> + {emailError} + </Admonition> + ) : ( + <Admonition type="tip" style={[a.mt_sm]}> + <Trans> + Use your account email address, or another real email + address you control, in case KWS or Bluesky needs to + contact you. + </Trans> + </Admonition> + )} + </View> + + <View> + <TextField.LabelText> + <Trans>Your preferred language</Trans> + </TextField.LabelText> + <LanguageSelect + value={language} + onChange={value => { + setLanguage(value) + setLanguageError(false) + }} + items={KWS_SUPPORTED_LANGS} + /> + + {languageError && ( + <Admonition type="error" style={[a.mt_sm]}> + <Trans>Please select a language</Trans> + </Admonition> + )} + </View> + + {error && <Admonition type="error">{error}</Admonition>} + + <Button + disabled={disabled} + label={_(msg`Begin age assurance process`)} + size="large" + variant="solid" + color="primary" + onPress={onSubmit}> + <ButtonText> + <Trans>Begin</Trans> + </ButtonText> + <ButtonIcon + icon={isPending ? Loader : Shield} + position="right" + /> + </Button> + </View> + + <Text + style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_medium]}> + <Trans> + By continuing, you agree to the{' '} + <InlineLinkText + overridePresentation + disableMismatchWarning + label={_(msg`KWS Terms of Use`)} + to={urls.kwsTermsOfUse} + style={[a.text_xs, a.leading_snug]}> + KWS Terms of Use + </InlineLinkText>{' '} + and acknowledge that KWS will store your verified status with + your hashed email address in accordance with the{' '} + <InlineLinkText + overridePresentation + disableMismatchWarning + label={_(msg`KWS Privacy Policy`)} + to={urls.kwsPrivacyPolicy} + style={[a.text_xs, a.leading_snug]}> + KWS Privacy Policy + </InlineLinkText> + . This means you won’t need to verify again the next time you + use this email for other apps, games, and services powered by + KWS technology. + </Trans> + </Text> + </> + )} + </View> + </View> + ) +} + +// best-effort mapping of our languages to KWS supported languages +function convertToKWSSupportedLanguage( + appLanguage: string, +): string | undefined { + // `${Enum}` is how you get a type of string union of the enum values (???) -sfn + switch (appLanguage as `${AppLanguage}`) { + // only en is supported + case 'en-GB': + return 'en' + // pt-PT is pt (pt-BR is supported independently) + case 'pt-PT': + return 'pt' + // only chinese (simplified) is supported, map all chinese variants + case 'zh-Hans-CN': + case 'zh-Hant-HK': + case 'zh-Hant-TW': + return 'zh-Hans' + default: + // try and map directly - if undefined, they will have to pick from the dropdown + return KWS_SUPPORTED_LANGS.find(v => v.value === appLanguage)?.value + } +} |