diff --git a/__e2e__/flows/report-dialog/post:default.yml b/__e2e__/flows/report-dialog/post.default.yml similarity index 100% rename from __e2e__/flows/report-dialog/post:default.yml rename to __e2e__/flows/report-dialog/post.default.yml diff --git a/__e2e__/flows/report-dialog/post:edit-reason.yml b/__e2e__/flows/report-dialog/post.edit-reason.yml similarity index 100% rename from __e2e__/flows/report-dialog/post:edit-reason.yml rename to __e2e__/flows/report-dialog/post.edit-reason.yml diff --git a/__e2e__/flows/report-dialog/post:reason-other.yml b/__e2e__/flows/report-dialog/post.reason-other.yml similarity index 100% rename from __e2e__/flows/report-dialog/post:reason-other.yml rename to __e2e__/flows/report-dialog/post.reason-other.yml diff --git a/src/components/ageAssurance/AgeAssuranceInitDialog.tsx b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx index a189d9af29..08fd5f84fc 100644 --- a/src/components/ageAssurance/AgeAssuranceInitDialog.tsx +++ b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx @@ -134,16 +134,18 @@ function Inner() { setSuccess(true) } catch (e) { + let error: React.ReactNode = _( + msg`Something went wrong, please try again`, + ) + if (e instanceof XRPCError) { if (e.error === 'InvalidEmail') { - setError( - _( - msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`, - ), + error = _( + msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`, ) logger.metric('ageAssurance:initDialogError', {code: 'InvalidEmail'}) } else if (e.error === 'DidTooLong') { - setError( + error = ( <> We're having issues initializing the age assurance process for @@ -155,15 +157,19 @@ function Inner() { {' '} for assistance. - , + ) logger.metric('ageAssurance:initDialogError', {code: 'DidTooLong'}) + } else { + logger.metric('ageAssurance:initDialogError', {code: 'other'}) } } else { const {clean, raw} = cleanError(e) - setError(clean || raw || _(msg`Something went wrong, please try again`)) + error = clean || raw || error logger.metric('ageAssurance:initDialogError', {code: 'other'}) } + + setError(error) } } @@ -188,7 +194,7 @@ function Inner() { <> - We use{' '} + We have partnered with{' '} {' '} 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. + KWS will check if you have previously verified your age using + this email address for other games/services powered by KWS + technology. If not, KWS will email you instructions for + verifying your age. When you’re done, you'll be brought back + to continue using Bluesky. diff --git a/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx b/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx index ff2e0bfd0f..b1c287e1b3 100644 --- a/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx +++ b/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx @@ -14,6 +14,7 @@ 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 {CheckThick_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check' import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' @@ -66,7 +67,7 @@ export function AgeAssuranceRedirectDialog() { // Dialog.useAutoOpen(control.control, 3e3) return ( - + control.clear()}> { @@ -125,8 +127,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) { // success! update state await refreshAgeAssuranceState() - control.clear() - control.control.close() + setSuccess(true) logger.metric('ageAssurance:redirectDialogSuccess', {}) }) @@ -143,6 +144,55 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) { } }, [agent, control, refreshAgeAssuranceState]) + if (success) { + return ( + <> + + + + + + + Success + + + + + + We've confirmed your age assurance status. You can now close this + dialog. + + + + {isNative && ( + + + + )} + + + + + ) + } + return ( <> @@ -175,8 +225,8 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) { ) : ( - We're confirming your status with our servers. This dialog should - close in a few seconds. + We're confirming your age assurance status with our servers. This + should only take a few seconds. )} @@ -187,7 +237,8 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) { label={_(msg`Close`)} size="large" variant="solid" - color="secondary"> + color="secondary" + onPress={() => control.control.close()}> Close diff --git a/src/components/ageAssurance/useAgeAssuranceCopy.ts b/src/components/ageAssurance/useAgeAssuranceCopy.ts index f8a0edd79c..c861f8336b 100644 --- a/src/components/ageAssurance/useAgeAssuranceCopy.ts +++ b/src/components/ageAssurance/useAgeAssuranceCopy.ts @@ -11,7 +11,7 @@ export function useAgeAssuranceCopy() { 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. Tap to learn more.`, + msg`The laws in your location require you to verify you're an adult to access certain features. Tap to learn more.`, ), chatsInfoText: _( msg`Don't worry! All existing messages and settings are saved and will be available after you verify you're an adult.`, diff --git a/src/screens/Profile/Header/Metrics.tsx b/src/screens/Profile/Header/Metrics.tsx index 6fc77142ee..a67b025425 100644 --- a/src/screens/Profile/Header/Metrics.tsx +++ b/src/screens/Profile/Header/Metrics.tsx @@ -1,10 +1,10 @@ import {View} from 'react-native' -import {AppBskyActorDefs} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' import {makeProfileLink} from '#/lib/routes/links' -import {Shadow} from '#/state/cache/types' +import {type Shadow} from '#/state/cache/types' import {formatCount} from '#/view/com/util/numeric/format' import {atoms as a, useTheme} from '#/alf' import {InlineLinkText} from '#/components/Link' @@ -36,7 +36,7 @@ export function ProfileHeaderMetrics({ testID="profileHeaderFollowersButton" style={[a.flex_row, t.atoms.text]} to={makeProfileLink(profile, 'followers')} - label={`${followers} ${pluralizedFollowers}`}> + label={`${profile.followersCount || 0} ${pluralizedFollowers}`}> {followers} {pluralizedFollowers} @@ -46,7 +46,7 @@ export function ProfileHeaderMetrics({ testID="profileHeaderFollowsButton" style={[a.flex_row, t.atoms.text]} to={makeProfileLink(profile, 'follows')} - label={_(msg`${following} following`)}> + label={_(msg`${profile.followsCount || 0} following`)}> {following} {pluralizedFollowings} diff --git a/src/state/ageAssurance/index.tsx b/src/state/ageAssurance/index.tsx index eded747732..3451b1139d 100644 --- a/src/state/ageAssurance/index.tsx +++ b/src/state/ageAssurance/index.tsx @@ -1,4 +1,4 @@ -import {createContext, useContext, useMemo} from 'react' +import {createContext, useContext, useMemo, useState} from 'react' import {type AppBskyUnspeccedDefs} from '@atproto/api' import {useQuery} from '@tanstack/react-query' @@ -46,6 +46,7 @@ export function Provider({children}: {children: React.ReactNode}) { const {geolocation} = useGeolocation() const isAgeAssuranceEnabled = useIsAgeAssuranceEnabled() const getAndRegisterPushToken = useGetAndRegisterPushToken() + const [refetchWhilePending, setRefetchWhilePending] = useState(false) const {data, isFetched, refetch} = useQuery({ /** @@ -56,6 +57,7 @@ export function Provider({children}: {children: React.ReactNode}) { * However, it only needs to run if AA is enabled. */ enabled: isAgeAssuranceEnabled, + refetchOnWindowFocus: refetchWhilePending, queryKey: createAgeAssuranceQueryKey(agent.session?.did ?? 'never'), async queryFn() { if (!agent.session) return null @@ -109,6 +111,24 @@ export function Provider({children}: {children: React.ReactNode}) { return ctx }, [isFetched, data, isAgeAssuranceEnabled]) + if ( + !!ageAssuranceContext.lastInitiatedAt && + ageAssuranceContext.status === 'pending' && + !refetchWhilePending + ) { + /* + * If we have a pending state, we want to refetch on window focus to ensure + * that we get the latest state when the user returns to the app. + */ + setRefetchWhilePending(true) + } else if ( + !!ageAssuranceContext.lastInitiatedAt && + ageAssuranceContext.status !== 'pending' && + refetchWhilePending + ) { + setRefetchWhilePending(false) + } + const ageAssuranceAPIContext = useMemo( () => ({ refetch,