Age Assurance V2 (#9479)
* Age Assurance V2 * Tighten up test * Add todos for sdk migration * Align RQ versions * Use useEffect for side effect * Improve effects, memoize * Standarize on birthdate * Copy feedback * Copilot * Add support link * Reove double .. * Cleanup * Remove redirect dialog * Cleanup todos, add comments * Update splash in main template too * Mock some stuff * Exhaustive checks Co-authored-by: Samuel Newman <mozzius@protonmail.com> * Exhaustive checks Co-authored-by: Samuel Newman <mozzius@protonmail.com> * Small fix to bday handling * Add comment * onboarding style tweak sneaking this in sorry! * rm unreachable breaks * Put useIntentHandler back on web * Remove misleading success set * Align on birthdate --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -4,9 +4,6 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
||||
import {logger} from '#/state/ageAssurance/util'
|
||||
import {useDeviceGeolocationApi} from '#/state/geolocation'
|
||||
import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog'
|
||||
@@ -23,14 +20,13 @@ import {Divider} from '#/components/Divider'
|
||||
import {createStaticClick, InlineLinkText} from '#/components/Link'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {logger, useAgeAssurance} from '#/ageAssurance'
|
||||
import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
|
||||
import {useDeviceGeolocationApi} from '#/geolocation'
|
||||
|
||||
export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) {
|
||||
const {isReady, isAgeRestricted, isDeclaredUnderage} = useAgeAssurance()
|
||||
|
||||
if (!isReady) return null
|
||||
if (isDeclaredUnderage) return null
|
||||
if (!isAgeRestricted) return null
|
||||
|
||||
const aa = useAgeAssurance()
|
||||
if (aa.state.access === aa.Access.Full) return null
|
||||
return <Inner style={style} />
|
||||
}
|
||||
|
||||
@@ -43,10 +39,12 @@ function Inner({style}: ViewStyleProp & {}) {
|
||||
const getTimeAgo = useGetTimeAgo()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const {setDeviceGeolocation} = useDeviceGeolocationApi()
|
||||
const computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess()
|
||||
|
||||
const copy = useAgeAssuranceCopy()
|
||||
const {status, lastInitiatedAt} = useAgeAssurance()
|
||||
const isBlocked = status === 'blocked'
|
||||
const aa = useAgeAssurance()
|
||||
const {status, lastInitiatedAt} = aa.state
|
||||
const isBlocked = status === aa.Status.Blocked
|
||||
const hasInitiated = !!lastInitiatedAt
|
||||
const timeAgo = lastInitiatedAt
|
||||
? getTimeAgo(lastInitiatedAt, new Date())
|
||||
@@ -98,7 +96,10 @@ function Inner({style}: ViewStyleProp & {}) {
|
||||
<DeviceLocationRequestDialog
|
||||
control={locationControl}
|
||||
onLocationAcquired={props => {
|
||||
if (props.geolocationStatus.isAgeRestrictedGeo) {
|
||||
const access = computeAgeAssuranceRegionAccess(
|
||||
props.geolocation,
|
||||
)
|
||||
if (access !== aa.Access.Full) {
|
||||
props.disableDialogAction()
|
||||
props.setDialogError(
|
||||
_(
|
||||
@@ -108,10 +109,7 @@ function Inner({style}: ViewStyleProp & {}) {
|
||||
} else {
|
||||
props.closeDialog(() => {
|
||||
// set this after close!
|
||||
setDeviceGeolocation({
|
||||
countryCode: props.geolocationStatus.countryCode,
|
||||
regionCode: props.geolocationStatus.regionCode,
|
||||
})
|
||||
setDeviceGeolocation(props.geolocation)
|
||||
Toast.show(_(msg`Thanks! You're all set.`), {
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
@@ -2,25 +2,23 @@ import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
||||
import {logger} from '#/state/ageAssurance/util'
|
||||
import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf'
|
||||
import {useDialogControl} from '#/components/ageAssurance/AgeAssuranceInitDialog'
|
||||
import type * as Dialog from '#/components/Dialog'
|
||||
import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
import {logger} from '#/ageAssurance'
|
||||
|
||||
export function AgeAssuranceAdmonition({
|
||||
children,
|
||||
style,
|
||||
}: ViewStyleProp & {children: React.ReactNode}) {
|
||||
const control = useDialogControl()
|
||||
const {isReady, isDeclaredUnderage, isAgeRestricted} = useAgeAssurance()
|
||||
const aa = useAgeAssurance()
|
||||
|
||||
if (!isReady) return null
|
||||
if (isDeclaredUnderage) return null
|
||||
if (!isAgeRestricted) return null
|
||||
if (aa.state.access === aa.Access.Full) return null
|
||||
|
||||
return (
|
||||
<Inner style={style} control={control}>
|
||||
|
||||
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/state/ageAssurance/util'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints, web} from '#/alf'
|
||||
@@ -15,6 +14,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {logger} from '#/ageAssurance'
|
||||
|
||||
export function AgeAssuranceAppealDialog({
|
||||
control,
|
||||
|
||||
@@ -3,8 +3,6 @@ import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
||||
import {logger} from '#/state/ageAssurance/util'
|
||||
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
|
||||
import {atoms as a, select, useTheme} from '#/alf'
|
||||
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||
@@ -13,30 +11,22 @@ import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Link} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
import {logger} from '#/ageAssurance'
|
||||
|
||||
export function useInternalState() {
|
||||
const {isReady, isDeclaredUnderage, isAgeRestricted, lastInitiatedAt} =
|
||||
useAgeAssurance()
|
||||
const aa = useAgeAssurance()
|
||||
const {nux} = useNux(Nux.AgeAssuranceDismissibleFeedBanner)
|
||||
const {mutate: save, variables} = useSaveNux()
|
||||
const hidden = !!variables
|
||||
|
||||
const visible = useMemo(() => {
|
||||
if (!isReady) return false
|
||||
if (isDeclaredUnderage) return false
|
||||
if (!isAgeRestricted) return false
|
||||
if (lastInitiatedAt) return false
|
||||
if (aa.state.access === aa.Access.Full) return false
|
||||
if (aa.state.lastInitiatedAt) return false
|
||||
if (hidden) return false
|
||||
if (nux && nux.completed) return false
|
||||
return true
|
||||
}, [
|
||||
isReady,
|
||||
isDeclaredUnderage,
|
||||
isAgeRestricted,
|
||||
lastInitiatedAt,
|
||||
hidden,
|
||||
nux,
|
||||
])
|
||||
}, [aa, hidden, nux])
|
||||
|
||||
const close = () => {
|
||||
save({
|
||||
|
||||
@@ -2,28 +2,25 @@ import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
||||
import {logger} from '#/state/ageAssurance/util'
|
||||
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
|
||||
import {atoms as a, type ViewStyleProp} from '#/alf'
|
||||
import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition'
|
||||
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
import {logger} from '#/ageAssurance'
|
||||
|
||||
export function AgeAssuranceDismissibleNotice({style}: ViewStyleProp & {}) {
|
||||
const {_} = useLingui()
|
||||
const {isReady, isDeclaredUnderage, isAgeRestricted, lastInitiatedAt} =
|
||||
useAgeAssurance()
|
||||
const aa = useAgeAssurance()
|
||||
const {nux} = useNux(Nux.AgeAssuranceDismissibleNotice)
|
||||
const copy = useAgeAssuranceCopy()
|
||||
const {mutate: save, variables} = useSaveNux()
|
||||
const hidden = !!variables
|
||||
|
||||
if (!isReady) return null
|
||||
if (isDeclaredUnderage) return null
|
||||
if (!isAgeRestricted) return null
|
||||
if (lastInitiatedAt) return null
|
||||
if (aa.state.access === aa.Access.Full) return null
|
||||
if (aa.state.lastInitiatedAt) return null
|
||||
if (hidden) return null
|
||||
if (nux && nux.completed) return null
|
||||
|
||||
|
||||
@@ -14,9 +14,6 @@ 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 {logger} from '#/state/ageAssurance/util'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
@@ -30,9 +27,12 @@ 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 {SimpleInlineLinkText} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {logger} from '#/ageAssurance'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
import {useBeginAgeAssurance} from '#/ageAssurance/useBeginAgeAssurance'
|
||||
|
||||
export {useDialogControl} from '#/components/Dialog/context'
|
||||
|
||||
@@ -69,7 +69,8 @@ function Inner() {
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const cleanError = useCleanError()
|
||||
const {close} = Dialog.useDialogContext()
|
||||
const {lastInitiatedAt} = useAgeAssuranceContext()
|
||||
const aa = useAgeAssurance()
|
||||
const lastInitiatedAt = aa.state.lastInitiatedAt
|
||||
const getTimeAgo = useGetTimeAgo()
|
||||
const tlds = useTLDs()
|
||||
const createSupportLink = useCreateSupportLink()
|
||||
@@ -88,7 +89,7 @@ function Inner() {
|
||||
)
|
||||
const [error, setError] = useState<React.ReactNode>(null)
|
||||
|
||||
const {mutateAsync: init, isPending} = useInitAgeAssurance()
|
||||
const {mutateAsync: begin, isPending} = useBeginAgeAssurance()
|
||||
|
||||
const runEmailValidation = () => {
|
||||
if (validateEmail(email)) {
|
||||
@@ -127,7 +128,7 @@ function Inner() {
|
||||
return
|
||||
}
|
||||
|
||||
await init({
|
||||
await begin({
|
||||
email,
|
||||
language,
|
||||
})
|
||||
@@ -150,11 +151,11 @@ function Inner() {
|
||||
<Trans>
|
||||
We're having issues initializing the age assurance process for
|
||||
your account. Please{' '}
|
||||
<InlineLinkText
|
||||
<SimpleInlineLinkText
|
||||
to={createSupportLink({code: SupportCode.AA_DID, email})}
|
||||
label={_(msg`Contact support`)}>
|
||||
contact support
|
||||
</InlineLinkText>{' '}
|
||||
</SimpleInlineLinkText>{' '}
|
||||
for assistance.
|
||||
</Trans>
|
||||
</>
|
||||
@@ -195,14 +196,12 @@ function Inner() {
|
||||
<Text style={[a.text_sm, a.leading_snug]}>
|
||||
<Trans>
|
||||
We have partnered with{' '}
|
||||
<InlineLinkText
|
||||
overridePresentation
|
||||
disableMismatchWarning
|
||||
<SimpleInlineLinkText
|
||||
label={_(msg`KWS website`)}
|
||||
to={urls.kwsHome}
|
||||
style={[a.text_sm, a.leading_snug]}>
|
||||
KWS
|
||||
</InlineLinkText>{' '}
|
||||
</SimpleInlineLinkText>{' '}
|
||||
to verify that you’re an adult. When you click "Begin" below,
|
||||
KWS will check if you have previously verified your age using
|
||||
this email address for other games/services powered by KWS
|
||||
@@ -328,24 +327,20 @@ function Inner() {
|
||||
style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
By continuing, you agree to the{' '}
|
||||
<InlineLinkText
|
||||
overridePresentation
|
||||
disableMismatchWarning
|
||||
<SimpleInlineLinkText
|
||||
label={_(msg`KWS Terms of Use`)}
|
||||
to={urls.kwsTermsOfUse}
|
||||
style={[a.text_xs, a.leading_snug]}>
|
||||
KWS Terms of Use
|
||||
</InlineLinkText>{' '}
|
||||
</SimpleInlineLinkText>{' '}
|
||||
and acknowledge that KWS will store your verified status with
|
||||
your hashed email address in accordance with the{' '}
|
||||
<InlineLinkText
|
||||
overridePresentation
|
||||
disableMismatchWarning
|
||||
<SimpleInlineLinkText
|
||||
label={_(msg`KWS Privacy Policy`)}
|
||||
to={urls.kwsPrivacyPolicy}
|
||||
style={[a.text_xs, a.leading_snug]}>
|
||||
KWS Privacy Policy
|
||||
</InlineLinkText>
|
||||
</SimpleInlineLinkText>
|
||||
. 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.
|
||||
|
||||
@@ -6,8 +6,6 @@ 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 {logger} from '#/state/ageAssurance/util'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
|
||||
@@ -18,6 +16,8 @@ import {CheckThick_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/ic
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {refetchAgeAssuranceServerState} from '#/ageAssurance'
|
||||
import {logger} from '#/ageAssurance'
|
||||
|
||||
export type AgeAssuranceRedirectDialogState = {
|
||||
result: 'success' | 'unknown'
|
||||
@@ -63,7 +63,7 @@ export function AgeAssuranceRedirectDialog() {
|
||||
const {_} = useLingui()
|
||||
const control = useAgeAssuranceRedirectDialogControl()
|
||||
|
||||
// TODO for testing
|
||||
// for testing
|
||||
// Dialog.useAutoOpen(control.control, 3e3)
|
||||
|
||||
return (
|
||||
@@ -88,7 +88,6 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
|
||||
const control = useAgeAssuranceRedirectDialogControl()
|
||||
const [error, setError] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const {refetch: refreshAgeAssuranceState} = useAgeAssuranceAPIContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (polling.current) return
|
||||
@@ -106,9 +105,9 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
|
||||
if (!agent.session) return
|
||||
if (unmounted.current) return
|
||||
|
||||
const {data} = await agent.app.bsky.unspecced.getAgeAssuranceState()
|
||||
const data = await refetchAgeAssuranceServerState({agent})
|
||||
|
||||
if (data.status !== 'assured') {
|
||||
if (data?.state.status !== 'assured') {
|
||||
throw new Error(
|
||||
`Polling for age assurance state did not receive assured status`,
|
||||
)
|
||||
@@ -124,9 +123,6 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
|
||||
if (!agent.session) return
|
||||
if (unmounted.current) return
|
||||
|
||||
// success! update state
|
||||
await refreshAgeAssuranceState()
|
||||
|
||||
setSuccess(true)
|
||||
|
||||
logger.metric('ageAssurance:redirectDialogSuccess', {})
|
||||
@@ -134,15 +130,13 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
|
||||
.catch(() => {
|
||||
if (unmounted.current) return
|
||||
setError(true)
|
||||
// try a refetch anyway
|
||||
refreshAgeAssuranceState()
|
||||
logger.metric('ageAssurance:redirectDialogFail', {})
|
||||
})
|
||||
|
||||
return () => {
|
||||
unmounted.current = true
|
||||
}
|
||||
}, [agent, control, refreshAgeAssuranceState])
|
||||
}, [agent, control])
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
|
||||
@@ -2,8 +2,6 @@ import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useAgeAssurance} from '#/state/ageAssurance/useAgeAssurance'
|
||||
import {logger} from '#/state/ageAssurance/util'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
|
||||
@@ -13,6 +11,8 @@ import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Link} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
import {logger} from '#/ageAssurance'
|
||||
|
||||
export function AgeRestrictedScreen({
|
||||
children,
|
||||
@@ -27,22 +27,9 @@ export function AgeRestrictedScreen({
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const copy = useAgeAssuranceCopy()
|
||||
const {isReady, isAgeRestricted} = useAgeAssurance()
|
||||
const aa = useAgeAssurance()
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText> </Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<Layout.Content />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
if (!isAgeRestricted) return children
|
||||
if (aa.state.access === aa.Access.Full) return children
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
|
||||
@@ -2,14 +2,22 @@ import {useMemo} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
|
||||
export function useAgeAssuranceCopy() {
|
||||
const {_} = useLingui()
|
||||
const aa = useAgeAssurance()
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
notice: _(
|
||||
msg`The laws in your location require you to verify you're an adult before accessing certain features on Bluesky, like adult content and direct messaging.`,
|
||||
),
|
||||
notice:
|
||||
aa.state.access === aa.Access.Safe
|
||||
? _(
|
||||
msg`Due to laws in your region, certain features on Bluesky are currently restricted until you're able to verify you're an adult.`,
|
||||
)
|
||||
: _(
|
||||
msg`The laws in your location require you to verify you're an adult before accessing certain features on Bluesky, like adult content and direct messaging.`,
|
||||
),
|
||||
banner: _(
|
||||
msg`The laws in your location require you to verify you're an adult to access certain features. Tap to learn more.`,
|
||||
),
|
||||
@@ -17,5 +25,5 @@ export function useAgeAssuranceCopy() {
|
||||
msg`Don't worry! All existing messages and settings are saved and will be available after you verify you're an adult.`,
|
||||
),
|
||||
}
|
||||
}, [_])
|
||||
}, [_, aa])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user