diff options
author | Eric Bailey <git@esb.lol> | 2024-06-03 20:10:43 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-04 02:10:43 +0100 |
commit | 3e1f0768916774642516d88254a6cf7a6a82331f (patch) | |
tree | 03204ed91e457eef7082c8cf8e1213bb622e5bdb /src | |
parent | de93e8de746f3c8a7b1755aaa034043951371ae0 (diff) | |
download | voidsky-3e1f0768916774642516d88254a6cf7a6a82331f.tar.zst |
[🙅] Disambiguation of the deactivation (#4267)
* Disambiguation of the deactivation * Snapshot crackle pop * Change log context * [🙅] Add status to session state (#4269) * Add status to session state * [🙅] Add new deactivated screen (#4270) * Add new deactivated screen * Update copy, handle logout * Remove icons, adjust padding * [🙅] Add deactivate account dialog (#4290) * Deactivate dialog (cherry picked from commit 33940e2dfe0d710c0665a7f68b198b46f54db4a2) * Factor out dialog, add to delete modal too (cherry picked from commit 47d70f6b74e7d2ea7330fd172499fe91ba41062d) * Update copy, icon (cherry picked from commit e6efabbe78c3f3d9f0f8fb0a06a6a1c4fbfb70a9) * Update copy (cherry picked from commit abb0ce26f6747ab0548f6f12df0dee3c64464852) * Sizing tweaks (cherry picked from commit fc716d5716873f0fddef56496fc48af0614b2e55) * Add a11y label
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/statsig/events.ts | 2 | ||||
-rw-r--r-- | src/screens/Deactivated.tsx | 307 | ||||
-rw-r--r-- | src/screens/Settings/components/DeactivateAccountDialog.tsx | 60 | ||||
-rw-r--r-- | src/screens/SignupQueued.tsx | 219 | ||||
-rw-r--r-- | src/state/persisted/schema.ts | 5 | ||||
-rw-r--r-- | src/state/session/__tests__/session-test.ts | 87 | ||||
-rw-r--r-- | src/state/session/agent.ts | 10 | ||||
-rw-r--r-- | src/state/session/index.tsx | 2 | ||||
-rw-r--r-- | src/state/session/util.ts | 5 | ||||
-rw-r--r-- | src/view/com/modals/DeleteAccount.tsx | 54 | ||||
-rw-r--r-- | src/view/screens/Settings/index.tsx | 29 | ||||
-rw-r--r-- | src/view/shell/createNativeStackNavigatorWithAuth.tsx | 8 |
12 files changed, 571 insertions, 217 deletions
diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 48651b3d9..753734edd 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -13,7 +13,7 @@ export type LogEvents = { withPassword: boolean } 'account:loggedOut': { - logContext: 'SwitchAccount' | 'Settings' | 'Deactivated' + logContext: 'SwitchAccount' | 'Settings' | 'SignupQueued' | 'Deactivated' } 'notifications:openApp': {} 'notifications:request': { diff --git a/src/screens/Deactivated.tsx b/src/screens/Deactivated.tsx index c9e9f9525..faee517cb 100644 --- a/src/screens/Deactivated.tsx +++ b/src/screens/Deactivated.tsx @@ -1,19 +1,22 @@ import React from 'react' import {View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {msg, plural, Trans} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useFocusEffect} from '@react-navigation/native' -import {logger} from '#/logger' +import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {isWeb} from '#/platform/detection' -import {isSessionDeactivated, useAgent, useSessionApi} from '#/state/session' -import {useOnboardingDispatch} from '#/state/shell' +import {type SessionAccount, useSession, useSessionApi} from '#/state/session' +import {useSetMinimalShellMode} from '#/state/shell' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {ScrollView} from '#/view/com/util/Views' import {Logo} from '#/view/icons/Logo' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import {Loader} from '#/components/Loader' -import {P, Text} from '#/components/Typography' +import {atoms as a, useTheme} from '#/alf' +import {AccountList} from '#/components/AccountList' +import {Button, ButtonText} from '#/components/Button' +import {Divider} from '#/components/Divider' +import {Text} from '#/components/Typography' const COL_WIDTH = 400 @@ -21,199 +24,151 @@ export function Deactivated() { const {_} = useLingui() const t = useTheme() const insets = useSafeAreaInsets() - const {gtMobile} = useBreakpoints() - const onboardingDispatch = useOnboardingDispatch() + const {currentAccount, accounts} = useSession() + const {onPressSwitchAccount, pendingDid} = useAccountSwitcher() + const {setShowLoggedOut} = useLoggedOutViewControls() + const hasOtherAccounts = accounts.length > 1 + const setMinimalShellMode = useSetMinimalShellMode() const {logout} = useSessionApi() - const agent = useAgent() - const [isProcessing, setProcessing] = React.useState(false) - const [estimatedTime, setEstimatedTime] = React.useState<string | undefined>( - undefined, - ) - const [placeInQueue, setPlaceInQueue] = React.useState<number | undefined>( - undefined, + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(true) + }, [setMinimalShellMode]), ) - const checkStatus = React.useCallback(async () => { - setProcessing(true) - try { - const res = await agent.com.atproto.temp.checkSignupQueue() - if (res.data.activated) { - // ready to go, exchange the access token for a usable one and kick off onboarding - await agent.refreshSession() - if (!isSessionDeactivated(agent.session?.accessJwt)) { - onboardingDispatch({type: 'start'}) - } - } else { - // not ready, update UI - setEstimatedTime(msToString(res.data.estimatedTimeMs)) - if (typeof res.data.placeInQueue !== 'undefined') { - setPlaceInQueue(Math.max(res.data.placeInQueue, 1)) - } + const onSelectAccount = React.useCallback( + (account: SessionAccount) => { + if (account.did !== currentAccount?.did) { + onPressSwitchAccount(account, 'SwitchAccount') } - } catch (e: any) { - logger.error('Failed to check signup queue', {err: e.toString()}) - } finally { - setProcessing(false) - } - }, [ - setProcessing, - setEstimatedTime, - setPlaceInQueue, - onboardingDispatch, - agent, - ]) + }, + [currentAccount, onPressSwitchAccount], + ) - React.useEffect(() => { - checkStatus() - const interval = setInterval(checkStatus, 60e3) - return () => clearInterval(interval) - }, [checkStatus]) + const onPressAddAccount = React.useCallback(() => { + setShowLoggedOut(true) + }, [setShowLoggedOut]) - const checkBtn = ( - <Button - variant="solid" - color="primary" - size="large" - label={_(msg`Check my status`)} - onPress={checkStatus} - disabled={isProcessing}> - <ButtonText> - <Trans>Check my status</Trans> - </ButtonText> - {isProcessing && <ButtonIcon icon={Loader} />} - </Button> - ) + const onPressLogout = React.useCallback(() => { + if (isWeb) { + // We're switching accounts, which remounts the entire app. + // On mobile, this gets us Home, but on the web we also need reset the URL. + // We can't change the URL via a navigate() call because the navigator + // itself is about to unmount, and it calls pushState() too late. + // So we change the URL ourselves. The navigator will pick it up on remount. + history.pushState(null, '', '/') + } + logout('Deactivated') + }, [logout]) return ( - <View - aria-modal - role="dialog" - aria-role="dialog" - aria-label={_(msg`You're in line`)} - accessibilityLabel={_(msg`You're in line`)} - accessibilityHint="" - style={[a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}> + <View style={[a.h_full_vh, a.flex_1, t.atoms.bg]}> <ScrollView style={[a.h_full, a.w_full]} contentContainerStyle={{borderWidth: 0}}> <View - style={[a.flex_row, a.justify_center, gtMobile ? a.pt_4xl : a.px_xl]}> - <View style={[a.flex_1, {maxWidth: COL_WIDTH}]}> - <View - style={[a.w_full, a.justify_center, a.align_center, a.my_4xl]}> - <Logo width={120} /> - </View> - - <Text style={[a.text_4xl, a.font_bold, a.pb_sm]}> - <Trans>You're in line</Trans> - </Text> - <P style={[t.atoms.text_contrast_medium]}> - <Trans> - There's been a rush of new users to Bluesky! We'll activate your - account as soon as we can. - </Trans> - </P> + style={[ + a.px_2xl, + { + paddingTop: isWeb ? 64 : insets.top, + paddingBottom: isWeb ? 64 : insets.bottom, + }, + ]}> + <View style={[a.flex_row, a.justify_center]}> + <View style={[a.w_full, {maxWidth: COL_WIDTH}]}> + <View + style={[a.w_full, a.justify_center, a.align_center, a.pb_5xl]}> + <Logo width={40} /> + </View> - <View - style={[ - a.rounded_sm, - a.px_2xl, - a.py_4xl, - a.mt_2xl, - t.atoms.bg_contrast_50, - ]}> - {typeof placeInQueue === 'number' && ( - <Text - style={[a.text_5xl, a.text_center, a.font_bold, a.mb_2xl]}> - {placeInQueue} + <View style={[a.gap_xs, a.pb_3xl]}> + <Text style={[a.text_xl, a.font_bold, a.leading_snug]}> + <Trans>Welcome back!</Trans> </Text> - )} - <P style={[a.text_center]}> - {typeof placeInQueue === 'number' ? ( - <Trans>left to go.</Trans> - ) : ( - <Trans>You are in line.</Trans> - )}{' '} - {estimatedTime ? ( + <Text style={[a.text_sm, a.leading_snug]}> <Trans> - We estimate {estimatedTime} until your account is ready. + You previously deactivated @{currentAccount?.handle}. </Trans> - ) : ( + </Text> + <Text style={[a.text_sm, a.leading_snug, a.pb_md]}> <Trans> - We will let you know when your account is ready. + You can reactivate your account to continue logging in. Your + profile and posts will be visible to other users. </Trans> - )} - </P> - </View> + </Text> - {isWeb && gtMobile && ( - <View style={[a.w_full, a.flex_row, a.justify_between, a.pt_5xl]}> - <Button - variant="ghost" - size="large" - label={_(msg`Log out`)} - onPress={() => logout('Deactivated')}> - <ButtonText style={[{color: t.palette.primary_500}]}> - <Trans>Log out</Trans> - </ButtonText> - </Button> - {checkBtn} + <View style={[a.gap_sm]}> + <Button + label={_(msg`Reactivate your account`)} + size="medium" + variant="solid" + color="primary" + onPress={() => setShowLoggedOut(true)}> + <ButtonText> + <Trans>Yes, reactivate my account</Trans> + </ButtonText> + </Button> + <Button + label={_(msg`Cancel reactivation and log out`)} + size="medium" + variant="solid" + color="secondary" + onPress={onPressLogout}> + <ButtonText> + <Trans>Cancel</Trans> + </ButtonText> + </Button> + </View> </View> - )} - </View> - <View style={{height: 200}} /> - </View> - </ScrollView> + <View style={[a.pb_3xl]}> + <Divider /> + </View> - {(!isWeb || !gtMobile) && ( - <View - style={[ - a.align_center, - gtMobile ? a.px_5xl : a.px_xl, - { - paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom), - }, - ]}> - <View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}> - {checkBtn} - <Button - variant="ghost" - size="large" - label={_(msg`Log out`)} - onPress={() => logout('Deactivated')}> - <ButtonText style={[{color: t.palette.primary_500}]}> - <Trans>Log out</Trans> - </ButtonText> - </Button> + {hasOtherAccounts ? ( + <> + <Text + style={[ + t.atoms.text_contrast_medium, + a.pb_md, + a.leading_snug, + ]}> + <Trans>Or, log into one of your other accounts.</Trans> + </Text> + <AccountList + onSelectAccount={onSelectAccount} + onSelectOther={onPressAddAccount} + otherLabel={_(msg`Add account`)} + pendingDid={pendingDid} + /> + </> + ) : ( + <> + <Text + style={[ + t.atoms.text_contrast_medium, + a.pb_md, + a.leading_snug, + ]}> + <Trans>Or, continue with another account.</Trans> + </Text> + <Button + label={_(msg`Log in or sign up`)} + size="medium" + variant="solid" + color="secondary" + onPress={() => setShowLoggedOut(true)}> + <ButtonText> + <Trans>Log in or sign up</Trans> + </ButtonText> + </Button> + </> + )} + </View> </View> </View> - )} + </ScrollView> </View> ) } - -function msToString(ms: number | undefined): string | undefined { - if (ms && ms > 0) { - const estimatedTimeMins = Math.ceil(ms / 60e3) - if (estimatedTimeMins > 59) { - const estimatedTimeHrs = Math.round(estimatedTimeMins / 60) - if (estimatedTimeHrs > 6) { - // dont even bother - return undefined - } - // hours - return `${estimatedTimeHrs} ${plural(estimatedTimeHrs, { - one: 'hour', - other: 'hours', - })}` - } - // minutes - return `${estimatedTimeMins} ${plural(estimatedTimeMins, { - one: 'minute', - other: 'minutes', - })}` - } - return undefined -} diff --git a/src/screens/Settings/components/DeactivateAccountDialog.tsx b/src/screens/Settings/components/DeactivateAccountDialog.tsx new file mode 100644 index 000000000..4330ffcaa --- /dev/null +++ b/src/screens/Settings/components/DeactivateAccountDialog.tsx @@ -0,0 +1,60 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {atoms as a, useTheme} from '#/alf' +import {DialogOuterProps} from '#/components/Dialog' +import {Divider} from '#/components/Divider' +import * as Prompt from '#/components/Prompt' +import {Text} from '#/components/Typography' + +export function DeactivateAccountDialog({ + control, +}: { + control: DialogOuterProps['control'] +}) { + const t = useTheme() + const {_} = useLingui() + + return ( + <Prompt.Outer control={control} testID="confirmModal"> + <Prompt.TitleText>{_(msg`Deactivate account`)}</Prompt.TitleText> + <Prompt.DescriptionText> + <Trans> + Your profile, posts, feeds, and lists will no longer be visible to + other Bluesky users. You can reactivate your account at any time by + logging in. + </Trans> + </Prompt.DescriptionText> + + <View style={[a.pb_xl]}> + <Divider /> + <View style={[a.gap_sm, a.pt_lg, a.pb_xl]}> + <Text style={[t.atoms.text_contrast_medium, a.leading_snug]}> + <Trans> + There is no time limit for account deactivation, come back any + time. + </Trans> + </Text> + <Text style={[t.atoms.text_contrast_medium, a.leading_snug]}> + <Trans> + If you're trying to change your handle or email, do so before you + deactivate. + </Trans> + </Text> + </View> + + <Divider /> + </View> + <Prompt.Actions> + <Prompt.Action + cta={_(msg`Yes, deactivate`)} + onPress={() => {}} + color="negative" + /> + <Prompt.Cancel /> + </Prompt.Actions> + </Prompt.Outer> + ) +} diff --git a/src/screens/SignupQueued.tsx b/src/screens/SignupQueued.tsx new file mode 100644 index 000000000..4e4fedcfa --- /dev/null +++ b/src/screens/SignupQueued.tsx @@ -0,0 +1,219 @@ +import React from 'react' +import {View} from 'react-native' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {msg, plural, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' +import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' +import {useOnboardingDispatch} from '#/state/shell' +import {ScrollView} from '#/view/com/util/Views' +import {Logo} from '#/view/icons/Logo' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {Loader} from '#/components/Loader' +import {P, Text} from '#/components/Typography' + +const COL_WIDTH = 400 + +export function SignupQueued() { + const {_} = useLingui() + const t = useTheme() + const insets = useSafeAreaInsets() + const {gtMobile} = useBreakpoints() + const onboardingDispatch = useOnboardingDispatch() + const {logout} = useSessionApi() + const agent = useAgent() + + const [isProcessing, setProcessing] = React.useState(false) + const [estimatedTime, setEstimatedTime] = React.useState<string | undefined>( + undefined, + ) + const [placeInQueue, setPlaceInQueue] = React.useState<number | undefined>( + undefined, + ) + + const checkStatus = React.useCallback(async () => { + setProcessing(true) + try { + const res = await agent.com.atproto.temp.checkSignupQueue() + if (res.data.activated) { + // ready to go, exchange the access token for a usable one and kick off onboarding + await agent.refreshSession() + if (!isSignupQueued(agent.session?.accessJwt)) { + onboardingDispatch({type: 'start'}) + } + } else { + // not ready, update UI + setEstimatedTime(msToString(res.data.estimatedTimeMs)) + if (typeof res.data.placeInQueue !== 'undefined') { + setPlaceInQueue(Math.max(res.data.placeInQueue, 1)) + } + } + } catch (e: any) { + logger.error('Failed to check signup queue', {err: e.toString()}) + } finally { + setProcessing(false) + } + }, [ + setProcessing, + setEstimatedTime, + setPlaceInQueue, + onboardingDispatch, + agent, + ]) + + React.useEffect(() => { + checkStatus() + const interval = setInterval(checkStatus, 60e3) + return () => clearInterval(interval) + }, [checkStatus]) + + const checkBtn = ( + <Button + variant="solid" + color="primary" + size="large" + label={_(msg`Check my status`)} + onPress={checkStatus} + disabled={isProcessing}> + <ButtonText> + <Trans>Check my status</Trans> + </ButtonText> + {isProcessing && <ButtonIcon icon={Loader} />} + </Button> + ) + + return ( + <View + aria-modal + role="dialog" + aria-role="dialog" + aria-label={_(msg`You're in line`)} + accessibilityLabel={_(msg`You're in line`)} + accessibilityHint="" + style={[a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}> + <ScrollView + style={[a.h_full, a.w_full]} + contentContainerStyle={{borderWidth: 0}}> + <View + style={[a.flex_row, a.justify_center, gtMobile ? a.pt_4xl : a.px_xl]}> + <View style={[a.flex_1, {maxWidth: COL_WIDTH}]}> + <View + style={[a.w_full, a.justify_center, a.align_center, a.my_4xl]}> + <Logo width={120} /> + </View> + + <Text style={[a.text_4xl, a.font_bold, a.pb_sm]}> + <Trans>You're in line</Trans> + </Text> + <P style={[t.atoms.text_contrast_medium]}> + <Trans> + There's been a rush of new users to Bluesky! We'll activate your + account as soon as we can. + </Trans> + </P> + + <View + style={[ + a.rounded_sm, + a.px_2xl, + a.py_4xl, + a.mt_2xl, + t.atoms.bg_contrast_50, + ]}> + {typeof placeInQueue === 'number' && ( + <Text + style={[a.text_5xl, a.text_center, a.font_bold, a.mb_2xl]}> + {placeInQueue} + </Text> + )} + <P style={[a.text_center]}> + {typeof placeInQueue === 'number' ? ( + <Trans>left to go.</Trans> + ) : ( + <Trans>You are in line.</Trans> + )}{' '} + {estimatedTime ? ( + <Trans> + We estimate {estimatedTime} until your account is ready. + </Trans> + ) : ( + <Trans> + We will let you know when your account is ready. + </Trans> + )} + </P> + </View> + + {isWeb && gtMobile && ( + <View style={[a.w_full, a.flex_row, a.justify_between, a.pt_5xl]}> + <Button + variant="ghost" + size="large" + label={_(msg`Log out`)} + onPress={() => logout('SignupQueued')}> + <ButtonText style={[{color: t.palette.primary_500}]}> + <Trans>Log out</Trans> + </ButtonText> + </Button> + {checkBtn} + </View> + )} + </View> + + <View style={{height: 200}} /> + </View> + </ScrollView> + + {(!isWeb || !gtMobile) && ( + <View + style={[ + a.align_center, + gtMobile ? a.px_5xl : a.px_xl, + { + paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom), + }, + ]}> + <View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}> + {checkBtn} + <Button + variant="ghost" + size="large" + label={_(msg`Log out`)} + onPress={() => logout('SignupQueued')}> + <ButtonText style={[{color: t.palette.primary_500}]}> + <Trans>Log out</Trans> + </ButtonText> + </Button> + </View> + </View> + )} + </View> + ) +} + +function msToString(ms: number | undefined): string | undefined { + if (ms && ms > 0) { + const estimatedTimeMins = Math.ceil(ms / 60e3) + if (estimatedTimeMins > 59) { + const estimatedTimeHrs = Math.round(estimatedTimeMins / 60) + if (estimatedTimeHrs > 6) { + // dont even bother + return undefined + } + // hours + return `${estimatedTimeHrs} ${plural(estimatedTimeHrs, { + one: 'hour', + other: 'hours', + })}` + } + // minutes + return `${estimatedTimeMins} ${plural(estimatedTimeMins, { + one: 'minute', + other: 'minutes', + })}` + } + return undefined +} diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 1860d34de..7d579d55d 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -17,7 +17,10 @@ const accountSchema = z.object({ emailAuthFactor: z.boolean().optional(), refreshJwt: z.string().optional(), // optional because it can expire accessJwt: z.string().optional(), // optional because it can expire - deactivated: z.boolean().optional(), + signupQueued: z.boolean().optional(), + status: z + .enum(['active', 'takendown', 'suspended', 'deactivated']) + .optional(), pdsUrl: z.string().optional(), }) export type PersistedAccount = z.infer<typeof accountSchema> diff --git a/src/state/session/__tests__/session-test.ts b/src/state/session/__tests__/session-test.ts index daf8d70c2..c8c1e103f 100644 --- a/src/state/session/__tests__/session-test.ts +++ b/src/state/session/__tests__/session-test.ts @@ -50,7 +50,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "alice-access-jwt-1", - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -59,6 +58,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-1", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -87,7 +88,6 @@ describe('session', () => { "accounts": [ { "accessJwt": undefined, - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -96,6 +96,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": undefined, "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -136,7 +138,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "alice-access-jwt-1", - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -145,6 +146,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-1", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -183,7 +186,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "bob-access-jwt-1", - "deactivated": false, "did": "bob-did", "email": undefined, "emailAuthFactor": false, @@ -192,10 +194,11 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "bob-refresh-jwt-1", "service": "https://bob.com/", + "signupQueued": false, + "status": "active", }, { "accessJwt": "alice-access-jwt-1", - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -204,6 +207,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-1", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -242,7 +247,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "alice-access-jwt-2", - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -251,10 +255,11 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-2", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, { "accessJwt": "bob-access-jwt-1", - "deactivated": false, "did": "bob-did", "email": undefined, "emailAuthFactor": false, @@ -263,6 +268,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "bob-refresh-jwt-1", "service": "https://bob.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -299,7 +306,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "jay-access-jwt-1", - "deactivated": false, "did": "jay-did", "email": undefined, "emailAuthFactor": false, @@ -308,10 +314,11 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "jay-refresh-jwt-1", "service": "https://jay.com/", + "signupQueued": false, + "status": "active", }, { "accessJwt": "alice-access-jwt-2", - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -320,10 +327,11 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-2", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, { "accessJwt": "bob-access-jwt-1", - "deactivated": false, "did": "bob-did", "email": undefined, "emailAuthFactor": false, @@ -332,6 +340,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "bob-refresh-jwt-1", "service": "https://bob.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -364,7 +374,6 @@ describe('session', () => { "accounts": [ { "accessJwt": undefined, - "deactivated": false, "did": "jay-did", "email": undefined, "emailAuthFactor": false, @@ -373,10 +382,11 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": undefined, "service": "https://jay.com/", + "signupQueued": false, + "status": "active", }, { "accessJwt": undefined, - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -385,10 +395,11 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": undefined, "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, { "accessJwt": undefined, - "deactivated": false, "did": "bob-did", "email": undefined, "emailAuthFactor": false, @@ -397,6 +408,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": undefined, "service": "https://bob.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -446,7 +459,6 @@ describe('session', () => { "accounts": [ { "accessJwt": undefined, - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -455,6 +467,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": undefined, "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -490,7 +504,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "alice-access-jwt-2", - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -499,6 +512,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-2", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -601,7 +616,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "bob-access-jwt-1", - "deactivated": false, "did": "bob-did", "email": undefined, "emailAuthFactor": false, @@ -610,6 +624,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "bob-refresh-jwt-1", "service": "https://bob.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -681,7 +697,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "alice-access-jwt-2", - "deactivated": false, "did": "alice-did", "email": "alice@foo.bar", "emailAuthFactor": false, @@ -690,6 +705,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-2", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -731,7 +748,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "alice-access-jwt-3", - "deactivated": false, "did": "alice-did", "email": "alice@foo.baz", "emailAuthFactor": true, @@ -740,6 +756,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-3", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -781,7 +799,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "alice-access-jwt-4", - "deactivated": false, "did": "alice-did", "email": "alice@foo.baz", "emailAuthFactor": false, @@ -790,6 +807,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-4", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -937,7 +956,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "bob-access-jwt-1", - "deactivated": false, "did": "bob-did", "email": undefined, "emailAuthFactor": false, @@ -946,10 +964,11 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "bob-refresh-jwt-1", "service": "https://bob.com/", + "signupQueued": false, + "status": "active", }, { "accessJwt": "alice-access-jwt-2", - "deactivated": false, "did": "alice-did", "email": "alice@foo.bar", "emailAuthFactor": false, @@ -958,6 +977,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-2", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -997,7 +1018,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "bob-access-jwt-2", - "deactivated": false, "did": "bob-did", "email": undefined, "emailAuthFactor": false, @@ -1006,10 +1026,11 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "bob-refresh-jwt-2", "service": "https://bob.com/", + "signupQueued": false, + "status": "active", }, { "accessJwt": "alice-access-jwt-2", - "deactivated": false, "did": "alice-did", "email": "alice@foo.bar", "emailAuthFactor": false, @@ -1018,6 +1039,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-2", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -1156,7 +1179,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "alice-access-jwt-1", - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -1165,6 +1187,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "alice-refresh-jwt-1", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -1218,7 +1242,6 @@ describe('session', () => { "accounts": [ { "accessJwt": undefined, - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -1227,6 +1250,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": undefined, "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -1280,7 +1305,6 @@ describe('session', () => { "accounts": [ { "accessJwt": undefined, - "deactivated": false, "did": "alice-did", "email": undefined, "emailAuthFactor": false, @@ -1289,6 +1313,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": undefined, "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -1371,7 +1397,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "jay-access-jwt-1", - "deactivated": false, "did": "jay-did", "email": undefined, "emailAuthFactor": false, @@ -1380,10 +1405,11 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "jay-refresh-jwt-1", "service": "https://jay.com/", + "signupQueued": false, + "status": "active", }, { "accessJwt": "bob-access-jwt-2", - "deactivated": false, "did": "bob-did", "email": undefined, "emailAuthFactor": false, @@ -1392,6 +1418,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "bob-refresh-jwt-2", "service": "https://alice.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { @@ -1429,7 +1457,6 @@ describe('session', () => { "accounts": [ { "accessJwt": "clarence-access-jwt-2", - "deactivated": false, "did": "clarence-did", "email": undefined, "emailAuthFactor": false, @@ -1438,6 +1465,8 @@ describe('session', () => { "pdsUrl": undefined, "refreshJwt": "clarence-refresh-jwt-2", "service": "https://clarence.com/", + "signupQueued": false, + "status": "active", }, ], "currentAgentState": { diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 45013debc..cdd24cd15 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -16,7 +16,7 @@ import { configureModerationForGuest, } from './moderation' import {SessionAccount} from './types' -import {isSessionDeactivated, isSessionExpired} from './util' +import {isSessionExpired, isSignupQueued} from './util' export function createPublicAgent() { configureModerationForGuest() // Side effect but only relevant for tests @@ -51,7 +51,7 @@ export async function createAgentAndResume( await networkRetry(1, () => agent.resumeSession(prevSession)) } else { agent.session = prevSession - if (!storedAccount.deactivated) { + if (!storedAccount.signupQueued) { // Intentionally not awaited to unblock the UI: networkRetry(3, () => agent.resumeSession(prevSession)).catch( (e: any) => { @@ -135,7 +135,7 @@ export async function createAgentAndCreateAccount( const account = agentToSessionAccountOrThrow(agent) const gates = tryFetchGates(account.did, 'prefer-fresh-gates') const moderation = configureModerationForAccount(agent, account) - if (!account.deactivated) { + if (!account.signupQueued) { /*dont await*/ agent.upsertProfile(_existing => { return { displayName: '', @@ -234,7 +234,9 @@ export function agentToSessionAccount( emailAuthFactor: agent.session.emailAuthFactor || false, refreshJwt: agent.session.refreshJwt, accessJwt: agent.session.accessJwt, - deactivated: isSessionDeactivated(agent.session.accessJwt), + signupQueued: isSignupQueued(agent.session.accessJwt), + // @ts-expect-error TODO remove when backend is ready + status: agent.session.status || 'active', pdsUrl: agent.pdsUrl?.toString(), } } diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index e38dd2bb5..371bd459a 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -17,7 +17,7 @@ import { } from './agent' import {getInitialState, reducer} from './reducer' -export {isSessionDeactivated} from './util' +export {isSignupQueued} from './util' export type {SessionAccount} from '#/state/session/types' import {SessionApiContext, SessionStateContext} from '#/state/session/types' diff --git a/src/state/session/util.ts b/src/state/session/util.ts index 8948ecd6b..3a5909e82 100644 --- a/src/state/session/util.ts +++ b/src/state/session/util.ts @@ -10,11 +10,12 @@ export function readLastActiveAccount() { return accounts.find(a => a.did === currentAccount?.did) } -export function isSessionDeactivated(accessJwt: string | undefined) { +export function isSignupQueued(accessJwt: string | undefined) { if (accessJwt) { const sessData = jwtDecode(accessJwt) return ( - hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated' + hasProp(sessData, 'scope') && + sessData.scope === 'com.atproto.signupQueued' ) } return false diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx index 06f1e111a..6dd248ca7 100644 --- a/src/view/com/modals/DeleteAccount.tsx +++ b/src/view/com/modals/DeleteAccount.tsx @@ -18,7 +18,13 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {cleanError} from 'lib/strings/errors' import {colors, gradients, s} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' -import {isAndroid} from 'platform/detection' +import {isAndroid, isWeb} from 'platform/detection' +import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog' +import {atoms as a, useTheme as useNewTheme} from '#/alf' +import {useDialogControl} from '#/components/Dialog' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import {InlineLinkText} from '#/components/Link' +import {Text as NewText} from '#/components/Typography' import {resetToTab} from '../../../Navigation' import {ErrorMessage} from '../util/error/ErrorMessage' import {Text} from '../util/text/Text' @@ -30,6 +36,7 @@ export const snapPoints = isAndroid ? ['90%'] : ['55%'] export function Component({}: {}) { const pal = usePalette('default') const theme = useTheme() + const t = useNewTheme() const {currentAccount} = useSession() const agent = useAgent() const {removeAccount} = useSessionApi() @@ -41,6 +48,7 @@ export function Component({}: {}) { const [password, setPassword] = React.useState<string>('') const [isProcessing, setIsProcessing] = React.useState<boolean>(false) const [error, setError] = React.useState<string>('') + const deactivateAccountControl = useDialogControl() const onPressSendEmail = async () => { setError('') setIsProcessing(true) @@ -168,6 +176,50 @@ export function Component({}: {}) { </TouchableOpacity> </> )} + + <View style={[!isWeb && a.px_xl]}> + <View + style={[ + a.w_full, + a.flex_row, + a.gap_sm, + a.mt_lg, + a.p_lg, + a.rounded_sm, + t.atoms.bg_contrast_25, + ]}> + <CircleInfo + size="md" + style={[ + a.relative, + { + top: -1, + }, + ]} + /> + + <NewText style={[a.leading_snug, a.flex_1]}> + <Trans> + You can also temporarily deactivate your account instead, + and reactivate it at any time. + </Trans>{' '} + <InlineLinkText + label={_( + msg`Click here for more information on deactivating your account`, + )} + to="#" + onPress={e => { + e.preventDefault() + deactivateAccountControl.open() + return false + }}> + <Trans>Click here for more information.</Trans> + </InlineLinkText> + </NewText> + </View> + </View> + + <DeactivateAccountDialog control={deactivateAccountControl} /> </> ) : ( <> diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index a647ea902..d075cc696 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -60,6 +60,7 @@ import {Text} from 'view/com/util/text/Text' import * as Toast from 'view/com/util/Toast' import {UserAvatar} from 'view/com/util/UserAvatar' import {ScrollView} from 'view/com/util/Views' +import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog' import {useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' @@ -307,6 +308,11 @@ export function SettingsScreen({}: Props) { Toast.show(_(msg`Legacy storage cleared, you need to restart the app now.`)) }, [_]) + const deactivateAccountControl = useDialogControl() + const onPressDeactivateAccount = React.useCallback(() => { + deactivateAccountControl.open() + }, [deactivateAccountControl]) + const {mutate: onPressDeleteChatDeclaration} = useDeleteActorDeclaration() return ( @@ -791,6 +797,29 @@ export function SettingsScreen({}: Props) { <Trans>Export My Data</Trans> </Text> </TouchableOpacity> + + <TouchableOpacity + style={[pal.view, styles.linkCard]} + onPress={onPressDeactivateAccount} + accessible={true} + accessibilityRole="button" + accessibilityLabel={_(msg`Deactivate account`)} + accessibilityHint={_( + msg`Opens modal for account deactivation confirmation`, + )}> + <View style={[styles.iconContainer, dangerBg]}> + <FontAwesomeIcon + icon={'users-slash'} + style={dangerText as FontAwesomeIconStyle} + size={18} + /> + </View> + <Text type="lg" style={dangerText}> + <Trans>Deactivate my account</Trans> + </Text> + </TouchableOpacity> + <DeactivateAccountDialog control={deactivateAccountControl} /> + <TouchableOpacity style={[pal.view, styles.linkCard]} onPress={onPressDeleteAccount} diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx index 882fdbe6e..3c611351d 100644 --- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -32,6 +32,7 @@ import { import {isWeb} from 'platform/detection' import {Deactivated} from '#/screens/Deactivated' import {Onboarding} from '#/screens/Onboarding' +import {SignupQueued} from '#/screens/SignupQueued' import {LoggedOut} from '../com/auth/LoggedOut' import {BottomBarWeb} from './bottom-bar/BottomBarWeb' import {DesktopLeftNav} from './desktop/LeftNav' @@ -102,12 +103,15 @@ function NativeStackNavigator({ if ((!PWI_ENABLED || activeRouteRequiresAuth) && !hasSession) { return <LoggedOut /> } - if (hasSession && currentAccount?.deactivated) { - return <Deactivated /> + if (hasSession && currentAccount?.signupQueued) { + return <SignupQueued /> } if (showLoggedOut) { return <LoggedOut onDismiss={() => setShowLoggedOut(false)} /> } + if (currentAccount?.status === 'deactivated') { + return <Deactivated /> + } if (onboardingState.isActive) { return <Onboarding /> } |