Add intent dialog

This commit is contained in:
Eric Bailey
2025-07-09 13:43:39 -05:00
parent d0ab4d8bfe
commit 9a0e043ee9
5 changed files with 209 additions and 2 deletions
@@ -0,0 +1,172 @@
import {useEffect, useRef, useState} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {retry} from '#/lib/async/retry'
import {createAgeAssuranceQueryKey} from '#/state/age-assurance'
import {useAgent} from '#/state/session'
import {atoms as a, web} from '#/alf'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import * as Dialog from '#/components/Dialog'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {Admonition} from '../Admonition'
export type AgeAssuranceRedirectDialogState = {
status: 'success' | 'unknown'
did: 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: {
status?: string
did?: string
} = {},
): AgeAssuranceRedirectDialogState | undefined {
let status: AgeAssuranceRedirectDialogState['status'] = 'unknown'
const did = state.did
switch (state.status) {
case 'success':
status = 'success'
break
case 'unknown':
default:
status = 'unknown'
break
}
if (status && did) {
return {
status,
did,
}
}
}
export function useAgeAssuranceRedirectDialogControl() {
return useGlobalDialogsControlContext().ageAssuranceRedirectDialogControl
}
export function AgeAssuranceRedirectDialog() {
const {_} = useLingui()
const control = useAgeAssuranceRedirectDialogControl()
// TODO maybe handle invalid DID here
return (
<Dialog.Outer control={control.control}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(msg`Verifying your age verification status`)}
style={[web({maxWidth: 400})]}>
<Inner optimisticState={control.value} />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
const {_} = useLingui()
const qc = useQueryClient()
const agent = useAgent()
const polling = useRef(false)
const unmounted = useRef(false)
const control = useAgeAssuranceRedirectDialogControl()
const [error, setError] = useState<string>('')
useEffect(() => {
if (polling.current) return
polling.current = true
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(data => {
if (!data) return
if (!agent.session) return
if (unmounted.current) return
qc.setQueryData(createAgeAssuranceQueryKey(agent.session.did), data)
control.clear()
control.control.close()
})
.catch(() => {
if (unmounted.current) return
setError(
_(
msg`We were unable to verify your age assurance status. But don't worry! Your account should be updated soon.`,
),
)
})
return () => {
unmounted.current = true
}
}, [_, agent, qc, control])
return (
<>
<View style={[a.align_start]}>
<AgeAssuranceBadge />
<View
style={[
a.flex_row,
a.justify_between,
a.align_center,
a.gap_md,
a.pt_lg,
a.pb_md,
]}>
<Text style={[a.text_xl, a.font_heavy]}>
<Trans>Verifying</Trans>
</Text>
{!error && <Loader size="md" />}
</View>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
We're confirming your status with our servers. This dialog should
close in a few seconds.
</Trans>
</Text>
{error && (
<View style={[a.pt_md]}>
<Admonition type="error">{error}</Admonition>
</View>
)}
</View>
{error && <Dialog.Close />}
</>
)
}
+6
View File
@@ -1,5 +1,6 @@
import {createContext, useContext, useMemo, useState} from 'react'
import {type AgeAssuranceRedirectDialogState} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
import * as Dialog from '#/components/Dialog'
import {type Screen} from '#/components/dialogs/EmailDialog/types'
@@ -22,6 +23,7 @@ type ControlsContext = {
displayText: string
share?: boolean
}>
ageAssuranceRedirectDialogControl: StatefulControl<AgeAssuranceRedirectDialogState>
}
const ControlsContext = createContext<ControlsContext | null>(null)
@@ -46,6 +48,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
displayText: string
share?: boolean
}>()
const ageAssuranceRedirectDialogControl =
useStatefulDialogControl<AgeAssuranceRedirectDialogState>()
const ctx = useMemo<ControlsContext>(
() => ({
@@ -54,6 +58,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
inAppBrowserConsentControl,
emailDialogControl,
linkWarningDialogControl,
ageAssuranceRedirectDialogControl,
}),
[
mutedWordsDialogControl,
@@ -61,6 +66,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
inAppBrowserConsentControl,
emailDialogControl,
linkWarningDialogControl,
ageAssuranceRedirectDialogControl,
],
)
+27 -2
View File
@@ -6,20 +6,27 @@ import {logEvent} from '#/lib/statsig/statsig'
import {isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useCloseAllActiveElements} from '#/state/util'
import {
parseAgeAssuranceRedirectDialogState,
useAgeAssuranceRedirectDialogControl,
} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
type IntentType = 'compose' | 'verify-email'
type IntentType = 'compose' | 'verify-email' | 'age-assurance'
const VALID_IMAGE_REGEX = /^[\w.:\-_/]+\|\d+(\.\d+)?\|\d+(\.\d+)?$/
// This needs to stay outside of react to persist between account switches
let previousIntentUrl = ''
// TODO we handle intents for logged out users?
export function useIntentHandler() {
const incomingUrl = Linking.useURL()
const composeIntent = useComposeIntent()
const verifyEmailIntent = useVerifyEmailIntent()
const ageAssuranceRedirectDialogControl =
useAgeAssuranceRedirectDialogControl()
React.useEffect(() => {
const handleIncomingURL = (url: string) => {
@@ -65,6 +72,19 @@ export function useIntentHandler() {
verifyEmailIntent(code)
return
}
case 'age-assurance': {
const state = parseAgeAssuranceRedirectDialogState({
status: params.get('status') ?? undefined,
did: params.get('did') ?? undefined,
})
// TODO maybe handle invalid DID here
if (state) {
ageAssuranceRedirectDialogControl.open(state)
}
return
}
default: {
return
}
@@ -78,7 +98,12 @@ export function useIntentHandler() {
handleIncomingURL(incomingUrl)
previousIntentUrl = incomingUrl
}
}, [incomingUrl, composeIntent, verifyEmailIntent])
}, [
incomingUrl,
composeIntent,
verifyEmailIntent,
ageAssuranceRedirectDialogControl,
])
}
export function useComposeIntent() {
+2
View File
@@ -25,6 +25,7 @@ import {ModalsContainer} from '#/view/com/modals/Modal'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {atoms as a, select, useTheme} from '#/alf'
import {setSystemUITheme} from '#/alf/util/systemUI'
import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
import {EmailDialog} from '#/components/dialogs/EmailDialog'
import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent'
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
@@ -155,6 +156,7 @@ function ShellInner() {
<MutedWordsDialog />
<SigninDialog />
<EmailDialog />
<AgeAssuranceRedirectDialog />
<InAppBrowserConsentDialog />
<LinkWarningDialog />
<Lightbox />
+2
View File
@@ -17,6 +17,7 @@ import {Lightbox} from '#/view/com/lightbox/Lightbox'
import {ModalsContainer} from '#/view/com/modals/Modal'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {atoms as a, select, useTheme} from '#/alf'
import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
import {EmailDialog} from '#/components/dialogs/EmailDialog'
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
@@ -70,6 +71,7 @@ function ShellInner() {
<MutedWordsDialog />
<SigninDialog />
<EmailDialog />
<AgeAssuranceRedirectDialog />
<LinkWarningDialog />
<Lightbox />
<PortalOutlet />