diff options
Diffstat (limited to 'src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx')
-rw-r--r-- | src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx b/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx new file mode 100644 index 000000000..41e706fee --- /dev/null +++ b/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx @@ -0,0 +1,196 @@ +import {useEffect, useRef, useState} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {retry} from '#/lib/async/retry' +import {wait} from '#/lib/async/wait' +import {isNative} from '#/platform/detection' +import {useAgeAssuranceAPIContext} from '#/state/ageAssurance' +import {useAgent} from '#/state/session' +import {atoms as a, useTheme, web} from '#/alf' +import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +export type AgeAssuranceRedirectDialogState = { + result: 'success' | 'unknown' + actorDid: string +} + +/** + * Validate and parse the query parameters returned from the age assurance + * redirect. If not valid, returns `undefined` and the dialog will not open. + */ +export function parseAgeAssuranceRedirectDialogState( + state: { + result?: string + actorDid?: string + } = {}, +): AgeAssuranceRedirectDialogState | undefined { + let result: AgeAssuranceRedirectDialogState['result'] = 'unknown' + const actorDid = state.actorDid + + switch (state.result) { + case 'success': + result = 'success' + break + case 'unknown': + default: + result = 'unknown' + break + } + + if (result && actorDid) { + return { + result, + actorDid, + } + } +} + +export function useAgeAssuranceRedirectDialogControl() { + return useGlobalDialogsControlContext().ageAssuranceRedirectDialogControl +} + +export function AgeAssuranceRedirectDialog() { + const {_} = useLingui() + const control = useAgeAssuranceRedirectDialogControl() + + // TODO for testing + // Dialog.useAutoOpen(control.control, 3e3) + + return ( + <Dialog.Outer control={control.control}> + <Dialog.Handle /> + + <Dialog.ScrollableInner + label={_(msg`Verifying your age assurance status`)} + style={[web({maxWidth: 400})]}> + <Inner optimisticState={control.value} /> + </Dialog.ScrollableInner> + </Dialog.Outer> + ) +} + +export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) { + const t = useTheme() + const {_} = useLingui() + const agent = useAgent() + const polling = useRef(false) + const unmounted = useRef(false) + const control = useAgeAssuranceRedirectDialogControl() + const [error, setError] = useState(false) + const {refetch: refreshAgeAssuranceState} = useAgeAssuranceAPIContext() + + useEffect(() => { + if (polling.current) return + + polling.current = true + + wait( + 3e3, + retry( + 5, + () => true, + async () => { + if (!agent.session) return + if (unmounted.current) return + + const {data} = await agent.app.bsky.unspecced.getAgeAssuranceState() + + if (data.status !== 'assured') { + throw new Error( + `Polling for age assurance state did not receive assured status`, + ) + } + + return data + }, + 1e3, + ), + ) + .then(async data => { + if (!data) return + if (!agent.session) return + if (unmounted.current) return + + // success! update state + await refreshAgeAssuranceState() + + control.clear() + control.control.close() + }) + .catch(() => { + if (unmounted.current) return + setError(true) + // try a refetch anyway + refreshAgeAssuranceState() + }) + + return () => { + unmounted.current = true + } + }, [agent, control, refreshAgeAssuranceState]) + + return ( + <> + <View style={[a.align_start, a.w_full]}> + <AgeAssuranceBadge /> + + <View + style={[ + a.flex_row, + a.justify_between, + a.align_center, + a.gap_sm, + a.pt_lg, + a.pb_md, + ]}> + {error && <ErrorIcon size="md" fill={t.palette.negative_500} />} + + <Text style={[a.text_xl, a.font_heavy]}> + {error ? <Trans>Connection issue</Trans> : <Trans>Verifying</Trans>} + </Text> + + {!error && <Loader size="md" />} + </View> + + <Text style={[a.text_md, a.leading_snug]}> + {error ? ( + <Trans> + We were unable to receive the verification due to a connection + issue. It may arrive later. If it does, your account will update + automatically. + </Trans> + ) : ( + <Trans> + We're confirming your status with our servers. This dialog should + close in a few seconds. + </Trans> + )} + </Text> + + {error && isNative && ( + <View style={[a.w_full, a.pt_lg]}> + <Button + label={_(msg`Close`)} + size="large" + variant="solid" + color="secondary"> + <ButtonText> + <Trans>Close</Trans> + </ButtonText> + </Button> + </View> + )} + </View> + + {error && <Dialog.Close />} + </> + ) +} |