about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/statsig/events.ts2
-rw-r--r--src/screens/Deactivated.tsx307
-rw-r--r--src/screens/Settings/components/DeactivateAccountDialog.tsx60
-rw-r--r--src/screens/SignupQueued.tsx219
-rw-r--r--src/state/persisted/schema.ts5
-rw-r--r--src/state/session/__tests__/session-test.ts87
-rw-r--r--src/state/session/agent.ts10
-rw-r--r--src/state/session/index.tsx2
-rw-r--r--src/state/session/util.ts5
-rw-r--r--src/view/com/modals/DeleteAccount.tsx54
-rw-r--r--src/view/screens/Settings/index.tsx29
-rw-r--r--src/view/shell/createNativeStackNavigatorWithAuth.tsx8
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 />
   }