mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {useEffect, useRef, useState} from 'react'
2import {View} from 'react-native'
3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5
6import {retry} from '#/lib/async/retry'
7import {wait} from '#/lib/async/wait'
8import {isNative} from '#/platform/detection'
9import {useAgeAssuranceAPIContext} from '#/state/ageAssurance'
10import {logger} from '#/state/ageAssurance/util'
11import {useAgent} from '#/state/session'
12import {atoms as a, useTheme, web} from '#/alf'
13import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
14import {Button, ButtonText} from '#/components/Button'
15import * as Dialog from '#/components/Dialog'
16import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
17import {CheckThick_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check'
18import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
19import {Loader} from '#/components/Loader'
20import {Text} from '#/components/Typography'
21
22export type AgeAssuranceRedirectDialogState = {
23 result: 'success' | 'unknown'
24 actorDid: string
25}
26
27/**
28 * Validate and parse the query parameters returned from the age assurance
29 * redirect. If not valid, returns `undefined` and the dialog will not open.
30 */
31export function parseAgeAssuranceRedirectDialogState(
32 state: {
33 result?: string
34 actorDid?: string
35 } = {},
36): AgeAssuranceRedirectDialogState | undefined {
37 let result: AgeAssuranceRedirectDialogState['result'] = 'unknown'
38 const actorDid = state.actorDid
39
40 switch (state.result) {
41 case 'success':
42 result = 'success'
43 break
44 case 'unknown':
45 default:
46 result = 'unknown'
47 break
48 }
49
50 if (result && actorDid) {
51 return {
52 result,
53 actorDid,
54 }
55 }
56}
57
58export function useAgeAssuranceRedirectDialogControl() {
59 return useGlobalDialogsControlContext().ageAssuranceRedirectDialogControl
60}
61
62export function AgeAssuranceRedirectDialog() {
63 const {_} = useLingui()
64 const control = useAgeAssuranceRedirectDialogControl()
65
66 // TODO for testing
67 // Dialog.useAutoOpen(control.control, 3e3)
68
69 return (
70 <Dialog.Outer control={control.control} onClose={() => control.clear()}>
71 <Dialog.Handle />
72
73 <Dialog.ScrollableInner
74 label={_(msg`Verifying your age assurance status`)}
75 style={[web({maxWidth: 400})]}>
76 <Inner optimisticState={control.value} />
77 </Dialog.ScrollableInner>
78 </Dialog.Outer>
79 )
80}
81
82export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
83 const t = useTheme()
84 const {_} = useLingui()
85 const agent = useAgent()
86 const polling = useRef(false)
87 const unmounted = useRef(false)
88 const control = useAgeAssuranceRedirectDialogControl()
89 const [error, setError] = useState(false)
90 const [success, setSuccess] = useState(false)
91 const {refetch: refreshAgeAssuranceState} = useAgeAssuranceAPIContext()
92
93 useEffect(() => {
94 if (polling.current) return
95
96 polling.current = true
97
98 logger.metric('ageAssurance:redirectDialogOpen', {})
99
100 wait(
101 3e3,
102 retry(
103 5,
104 () => true,
105 async () => {
106 if (!agent.session) return
107 if (unmounted.current) return
108
109 const {data} = await agent.app.bsky.unspecced.getAgeAssuranceState()
110
111 if (data.status !== 'assured') {
112 throw new Error(
113 `Polling for age assurance state did not receive assured status`,
114 )
115 }
116
117 return data
118 },
119 1e3,
120 ),
121 )
122 .then(async data => {
123 if (!data) return
124 if (!agent.session) return
125 if (unmounted.current) return
126
127 // success! update state
128 await refreshAgeAssuranceState()
129
130 setSuccess(true)
131
132 logger.metric('ageAssurance:redirectDialogSuccess', {})
133 })
134 .catch(() => {
135 if (unmounted.current) return
136 setError(true)
137 // try a refetch anyway
138 refreshAgeAssuranceState()
139 logger.metric('ageAssurance:redirectDialogFail', {})
140 })
141
142 return () => {
143 unmounted.current = true
144 }
145 }, [agent, control, refreshAgeAssuranceState])
146
147 if (success) {
148 return (
149 <>
150 <View style={[a.align_start, a.w_full]}>
151 <AgeAssuranceBadge />
152
153 <View
154 style={[
155 a.flex_row,
156 a.justify_between,
157 a.align_center,
158 a.gap_sm,
159 a.pt_lg,
160 a.pb_md,
161 ]}>
162 <SuccessIcon size="sm" fill={t.palette.positive_600} />
163 <Text style={[a.text_xl, a.font_bold]}>
164 <Trans>Success</Trans>
165 </Text>
166 </View>
167
168 <Text style={[a.text_md, a.leading_snug]}>
169 <Trans>
170 We've confirmed your age assurance status. You can now close this
171 dialog.
172 </Trans>
173 </Text>
174
175 {isNative && (
176 <View style={[a.w_full, a.pt_lg]}>
177 <Button
178 label={_(msg`Close`)}
179 size="large"
180 variant="solid"
181 color="secondary"
182 onPress={() => control.control.close()}>
183 <ButtonText>
184 <Trans>Close</Trans>
185 </ButtonText>
186 </Button>
187 </View>
188 )}
189 </View>
190
191 <Dialog.Close />
192 </>
193 )
194 }
195
196 return (
197 <>
198 <View style={[a.align_start, a.w_full]}>
199 <AgeAssuranceBadge />
200
201 <View
202 style={[
203 a.flex_row,
204 a.justify_between,
205 a.align_center,
206 a.gap_sm,
207 a.pt_lg,
208 a.pb_md,
209 ]}>
210 {error && <ErrorIcon size="md" fill={t.palette.negative_500} />}
211
212 <Text style={[a.text_xl, a.font_bold]}>
213 {error ? <Trans>Connection issue</Trans> : <Trans>Verifying</Trans>}
214 </Text>
215
216 {!error && <Loader size="md" />}
217 </View>
218
219 <Text style={[a.text_md, a.leading_snug]}>
220 {error ? (
221 <Trans>
222 We were unable to receive the verification due to a connection
223 issue. It may arrive later. If it does, your account will update
224 automatically.
225 </Trans>
226 ) : (
227 <Trans>
228 We're confirming your age assurance status with our servers. This
229 should only take a few seconds.
230 </Trans>
231 )}
232 </Text>
233
234 {error && isNative && (
235 <View style={[a.w_full, a.pt_lg]}>
236 <Button
237 label={_(msg`Close`)}
238 size="large"
239 variant="solid"
240 color="secondary"
241 onPress={() => control.control.close()}>
242 <ButtonText>
243 <Trans>Close</Trans>
244 </ButtonText>
245 </Button>
246 </View>
247 )}
248 </View>
249
250 {error && <Dialog.Close />}
251 </>
252 )
253}