Bluesky app fork with some witchin' additions ๐Ÿ’ซ
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[๐Ÿ™…] 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

authored by

Eric Bailey and committed by
GitHub
3e1f0768 de93e8de

+571 -217
+1 -1
src/lib/statsig/events.ts
··· 13 13 withPassword: boolean 14 14 } 15 15 'account:loggedOut': { 16 - logContext: 'SwitchAccount' | 'Settings' | 'Deactivated' 16 + logContext: 'SwitchAccount' | 'Settings' | 'SignupQueued' | 'Deactivated' 17 17 } 18 18 'notifications:openApp': {} 19 19 'notifications:request': {
+131 -176
src/screens/Deactivated.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 - import {msg, plural, Trans} from '@lingui/macro' 4 + import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 + import {useFocusEffect} from '@react-navigation/native' 6 7 7 - import {logger} from '#/logger' 8 + import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 8 9 import {isWeb} from '#/platform/detection' 9 - import {isSessionDeactivated, useAgent, useSessionApi} from '#/state/session' 10 - import {useOnboardingDispatch} from '#/state/shell' 10 + import {type SessionAccount, useSession, useSessionApi} from '#/state/session' 11 + import {useSetMinimalShellMode} from '#/state/shell' 12 + import {useLoggedOutViewControls} from '#/state/shell/logged-out' 11 13 import {ScrollView} from '#/view/com/util/Views' 12 14 import {Logo} from '#/view/icons/Logo' 13 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 14 - import {Button, ButtonIcon, ButtonText} from '#/components/Button' 15 - import {Loader} from '#/components/Loader' 16 - import {P, Text} from '#/components/Typography' 15 + import {atoms as a, useTheme} from '#/alf' 16 + import {AccountList} from '#/components/AccountList' 17 + import {Button, ButtonText} from '#/components/Button' 18 + import {Divider} from '#/components/Divider' 19 + import {Text} from '#/components/Typography' 17 20 18 21 const COL_WIDTH = 400 19 22 ··· 21 24 const {_} = useLingui() 22 25 const t = useTheme() 23 26 const insets = useSafeAreaInsets() 24 - const {gtMobile} = useBreakpoints() 25 - const onboardingDispatch = useOnboardingDispatch() 27 + const {currentAccount, accounts} = useSession() 28 + const {onPressSwitchAccount, pendingDid} = useAccountSwitcher() 29 + const {setShowLoggedOut} = useLoggedOutViewControls() 30 + const hasOtherAccounts = accounts.length > 1 31 + const setMinimalShellMode = useSetMinimalShellMode() 26 32 const {logout} = useSessionApi() 27 - const agent = useAgent() 28 33 29 - const [isProcessing, setProcessing] = React.useState(false) 30 - const [estimatedTime, setEstimatedTime] = React.useState<string | undefined>( 31 - undefined, 32 - ) 33 - const [placeInQueue, setPlaceInQueue] = React.useState<number | undefined>( 34 - undefined, 34 + useFocusEffect( 35 + React.useCallback(() => { 36 + setMinimalShellMode(true) 37 + }, [setMinimalShellMode]), 35 38 ) 36 39 37 - const checkStatus = React.useCallback(async () => { 38 - setProcessing(true) 39 - try { 40 - const res = await agent.com.atproto.temp.checkSignupQueue() 41 - if (res.data.activated) { 42 - // ready to go, exchange the access token for a usable one and kick off onboarding 43 - await agent.refreshSession() 44 - if (!isSessionDeactivated(agent.session?.accessJwt)) { 45 - onboardingDispatch({type: 'start'}) 46 - } 47 - } else { 48 - // not ready, update UI 49 - setEstimatedTime(msToString(res.data.estimatedTimeMs)) 50 - if (typeof res.data.placeInQueue !== 'undefined') { 51 - setPlaceInQueue(Math.max(res.data.placeInQueue, 1)) 52 - } 40 + const onSelectAccount = React.useCallback( 41 + (account: SessionAccount) => { 42 + if (account.did !== currentAccount?.did) { 43 + onPressSwitchAccount(account, 'SwitchAccount') 53 44 } 54 - } catch (e: any) { 55 - logger.error('Failed to check signup queue', {err: e.toString()}) 56 - } finally { 57 - setProcessing(false) 58 - } 59 - }, [ 60 - setProcessing, 61 - setEstimatedTime, 62 - setPlaceInQueue, 63 - onboardingDispatch, 64 - agent, 65 - ]) 45 + }, 46 + [currentAccount, onPressSwitchAccount], 47 + ) 66 48 67 - React.useEffect(() => { 68 - checkStatus() 69 - const interval = setInterval(checkStatus, 60e3) 70 - return () => clearInterval(interval) 71 - }, [checkStatus]) 49 + const onPressAddAccount = React.useCallback(() => { 50 + setShowLoggedOut(true) 51 + }, [setShowLoggedOut]) 72 52 73 - const checkBtn = ( 74 - <Button 75 - variant="solid" 76 - color="primary" 77 - size="large" 78 - label={_(msg`Check my status`)} 79 - onPress={checkStatus} 80 - disabled={isProcessing}> 81 - <ButtonText> 82 - <Trans>Check my status</Trans> 83 - </ButtonText> 84 - {isProcessing && <ButtonIcon icon={Loader} />} 85 - </Button> 86 - ) 53 + const onPressLogout = React.useCallback(() => { 54 + if (isWeb) { 55 + // We're switching accounts, which remounts the entire app. 56 + // On mobile, this gets us Home, but on the web we also need reset the URL. 57 + // We can't change the URL via a navigate() call because the navigator 58 + // itself is about to unmount, and it calls pushState() too late. 59 + // So we change the URL ourselves. The navigator will pick it up on remount. 60 + history.pushState(null, '', '/') 61 + } 62 + logout('Deactivated') 63 + }, [logout]) 87 64 88 65 return ( 89 - <View 90 - aria-modal 91 - role="dialog" 92 - aria-role="dialog" 93 - aria-label={_(msg`You're in line`)} 94 - accessibilityLabel={_(msg`You're in line`)} 95 - accessibilityHint="" 96 - style={[a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}> 66 + <View style={[a.h_full_vh, a.flex_1, t.atoms.bg]}> 97 67 <ScrollView 98 68 style={[a.h_full, a.w_full]} 99 69 contentContainerStyle={{borderWidth: 0}}> 100 70 <View 101 - style={[a.flex_row, a.justify_center, gtMobile ? a.pt_4xl : a.px_xl]}> 102 - <View style={[a.flex_1, {maxWidth: COL_WIDTH}]}> 103 - <View 104 - style={[a.w_full, a.justify_center, a.align_center, a.my_4xl]}> 105 - <Logo width={120} /> 106 - </View> 107 - 108 - <Text style={[a.text_4xl, a.font_bold, a.pb_sm]}> 109 - <Trans>You're in line</Trans> 110 - </Text> 111 - <P style={[t.atoms.text_contrast_medium]}> 112 - <Trans> 113 - There's been a rush of new users to Bluesky! We'll activate your 114 - account as soon as we can. 115 - </Trans> 116 - </P> 71 + style={[ 72 + a.px_2xl, 73 + { 74 + paddingTop: isWeb ? 64 : insets.top, 75 + paddingBottom: isWeb ? 64 : insets.bottom, 76 + }, 77 + ]}> 78 + <View style={[a.flex_row, a.justify_center]}> 79 + <View style={[a.w_full, {maxWidth: COL_WIDTH}]}> 80 + <View 81 + style={[a.w_full, a.justify_center, a.align_center, a.pb_5xl]}> 82 + <Logo width={40} /> 83 + </View> 117 84 118 - <View 119 - style={[ 120 - a.rounded_sm, 121 - a.px_2xl, 122 - a.py_4xl, 123 - a.mt_2xl, 124 - t.atoms.bg_contrast_50, 125 - ]}> 126 - {typeof placeInQueue === 'number' && ( 127 - <Text 128 - style={[a.text_5xl, a.text_center, a.font_bold, a.mb_2xl]}> 129 - {placeInQueue} 85 + <View style={[a.gap_xs, a.pb_3xl]}> 86 + <Text style={[a.text_xl, a.font_bold, a.leading_snug]}> 87 + <Trans>Welcome back!</Trans> 130 88 </Text> 131 - )} 132 - <P style={[a.text_center]}> 133 - {typeof placeInQueue === 'number' ? ( 134 - <Trans>left to go.</Trans> 135 - ) : ( 136 - <Trans>You are in line.</Trans> 137 - )}{' '} 138 - {estimatedTime ? ( 89 + <Text style={[a.text_sm, a.leading_snug]}> 139 90 <Trans> 140 - We estimate {estimatedTime} until your account is ready. 91 + You previously deactivated @{currentAccount?.handle}. 141 92 </Trans> 142 - ) : ( 93 + </Text> 94 + <Text style={[a.text_sm, a.leading_snug, a.pb_md]}> 143 95 <Trans> 144 - We will let you know when your account is ready. 96 + You can reactivate your account to continue logging in. Your 97 + profile and posts will be visible to other users. 145 98 </Trans> 146 - )} 147 - </P> 148 - </View> 99 + </Text> 149 100 150 - {isWeb && gtMobile && ( 151 - <View style={[a.w_full, a.flex_row, a.justify_between, a.pt_5xl]}> 152 - <Button 153 - variant="ghost" 154 - size="large" 155 - label={_(msg`Log out`)} 156 - onPress={() => logout('Deactivated')}> 157 - <ButtonText style={[{color: t.palette.primary_500}]}> 158 - <Trans>Log out</Trans> 159 - </ButtonText> 160 - </Button> 161 - {checkBtn} 101 + <View style={[a.gap_sm]}> 102 + <Button 103 + label={_(msg`Reactivate your account`)} 104 + size="medium" 105 + variant="solid" 106 + color="primary" 107 + onPress={() => setShowLoggedOut(true)}> 108 + <ButtonText> 109 + <Trans>Yes, reactivate my account</Trans> 110 + </ButtonText> 111 + </Button> 112 + <Button 113 + label={_(msg`Cancel reactivation and log out`)} 114 + size="medium" 115 + variant="solid" 116 + color="secondary" 117 + onPress={onPressLogout}> 118 + <ButtonText> 119 + <Trans>Cancel</Trans> 120 + </ButtonText> 121 + </Button> 122 + </View> 162 123 </View> 163 - )} 164 - </View> 165 124 166 - <View style={{height: 200}} /> 167 - </View> 168 - </ScrollView> 125 + <View style={[a.pb_3xl]}> 126 + <Divider /> 127 + </View> 169 128 170 - {(!isWeb || !gtMobile) && ( 171 - <View 172 - style={[ 173 - a.align_center, 174 - gtMobile ? a.px_5xl : a.px_xl, 175 - { 176 - paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom), 177 - }, 178 - ]}> 179 - <View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}> 180 - {checkBtn} 181 - <Button 182 - variant="ghost" 183 - size="large" 184 - label={_(msg`Log out`)} 185 - onPress={() => logout('Deactivated')}> 186 - <ButtonText style={[{color: t.palette.primary_500}]}> 187 - <Trans>Log out</Trans> 188 - </ButtonText> 189 - </Button> 129 + {hasOtherAccounts ? ( 130 + <> 131 + <Text 132 + style={[ 133 + t.atoms.text_contrast_medium, 134 + a.pb_md, 135 + a.leading_snug, 136 + ]}> 137 + <Trans>Or, log into one of your other accounts.</Trans> 138 + </Text> 139 + <AccountList 140 + onSelectAccount={onSelectAccount} 141 + onSelectOther={onPressAddAccount} 142 + otherLabel={_(msg`Add account`)} 143 + pendingDid={pendingDid} 144 + /> 145 + </> 146 + ) : ( 147 + <> 148 + <Text 149 + style={[ 150 + t.atoms.text_contrast_medium, 151 + a.pb_md, 152 + a.leading_snug, 153 + ]}> 154 + <Trans>Or, continue with another account.</Trans> 155 + </Text> 156 + <Button 157 + label={_(msg`Log in or sign up`)} 158 + size="medium" 159 + variant="solid" 160 + color="secondary" 161 + onPress={() => setShowLoggedOut(true)}> 162 + <ButtonText> 163 + <Trans>Log in or sign up</Trans> 164 + </ButtonText> 165 + </Button> 166 + </> 167 + )} 168 + </View> 190 169 </View> 191 170 </View> 192 - )} 171 + </ScrollView> 193 172 </View> 194 173 ) 195 174 } 196 - 197 - function msToString(ms: number | undefined): string | undefined { 198 - if (ms && ms > 0) { 199 - const estimatedTimeMins = Math.ceil(ms / 60e3) 200 - if (estimatedTimeMins > 59) { 201 - const estimatedTimeHrs = Math.round(estimatedTimeMins / 60) 202 - if (estimatedTimeHrs > 6) { 203 - // dont even bother 204 - return undefined 205 - } 206 - // hours 207 - return `${estimatedTimeHrs} ${plural(estimatedTimeHrs, { 208 - one: 'hour', 209 - other: 'hours', 210 - })}` 211 - } 212 - // minutes 213 - return `${estimatedTimeMins} ${plural(estimatedTimeMins, { 214 - one: 'minute', 215 - other: 'minutes', 216 - })}` 217 - } 218 - return undefined 219 - }
+60
src/screens/Settings/components/DeactivateAccountDialog.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {msg, Trans} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {atoms as a, useTheme} from '#/alf' 7 + import {DialogOuterProps} from '#/components/Dialog' 8 + import {Divider} from '#/components/Divider' 9 + import * as Prompt from '#/components/Prompt' 10 + import {Text} from '#/components/Typography' 11 + 12 + export function DeactivateAccountDialog({ 13 + control, 14 + }: { 15 + control: DialogOuterProps['control'] 16 + }) { 17 + const t = useTheme() 18 + const {_} = useLingui() 19 + 20 + return ( 21 + <Prompt.Outer control={control} testID="confirmModal"> 22 + <Prompt.TitleText>{_(msg`Deactivate account`)}</Prompt.TitleText> 23 + <Prompt.DescriptionText> 24 + <Trans> 25 + Your profile, posts, feeds, and lists will no longer be visible to 26 + other Bluesky users. You can reactivate your account at any time by 27 + logging in. 28 + </Trans> 29 + </Prompt.DescriptionText> 30 + 31 + <View style={[a.pb_xl]}> 32 + <Divider /> 33 + <View style={[a.gap_sm, a.pt_lg, a.pb_xl]}> 34 + <Text style={[t.atoms.text_contrast_medium, a.leading_snug]}> 35 + <Trans> 36 + There is no time limit for account deactivation, come back any 37 + time. 38 + </Trans> 39 + </Text> 40 + <Text style={[t.atoms.text_contrast_medium, a.leading_snug]}> 41 + <Trans> 42 + If you're trying to change your handle or email, do so before you 43 + deactivate. 44 + </Trans> 45 + </Text> 46 + </View> 47 + 48 + <Divider /> 49 + </View> 50 + <Prompt.Actions> 51 + <Prompt.Action 52 + cta={_(msg`Yes, deactivate`)} 53 + onPress={() => {}} 54 + color="negative" 55 + /> 56 + <Prompt.Cancel /> 57 + </Prompt.Actions> 58 + </Prompt.Outer> 59 + ) 60 + }
+219
src/screens/SignupQueued.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 + import {msg, plural, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + 7 + import {logger} from '#/logger' 8 + import {isWeb} from '#/platform/detection' 9 + import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' 10 + import {useOnboardingDispatch} from '#/state/shell' 11 + import {ScrollView} from '#/view/com/util/Views' 12 + import {Logo} from '#/view/icons/Logo' 13 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 14 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 15 + import {Loader} from '#/components/Loader' 16 + import {P, Text} from '#/components/Typography' 17 + 18 + const COL_WIDTH = 400 19 + 20 + export function SignupQueued() { 21 + const {_} = useLingui() 22 + const t = useTheme() 23 + const insets = useSafeAreaInsets() 24 + const {gtMobile} = useBreakpoints() 25 + const onboardingDispatch = useOnboardingDispatch() 26 + const {logout} = useSessionApi() 27 + const agent = useAgent() 28 + 29 + const [isProcessing, setProcessing] = React.useState(false) 30 + const [estimatedTime, setEstimatedTime] = React.useState<string | undefined>( 31 + undefined, 32 + ) 33 + const [placeInQueue, setPlaceInQueue] = React.useState<number | undefined>( 34 + undefined, 35 + ) 36 + 37 + const checkStatus = React.useCallback(async () => { 38 + setProcessing(true) 39 + try { 40 + const res = await agent.com.atproto.temp.checkSignupQueue() 41 + if (res.data.activated) { 42 + // ready to go, exchange the access token for a usable one and kick off onboarding 43 + await agent.refreshSession() 44 + if (!isSignupQueued(agent.session?.accessJwt)) { 45 + onboardingDispatch({type: 'start'}) 46 + } 47 + } else { 48 + // not ready, update UI 49 + setEstimatedTime(msToString(res.data.estimatedTimeMs)) 50 + if (typeof res.data.placeInQueue !== 'undefined') { 51 + setPlaceInQueue(Math.max(res.data.placeInQueue, 1)) 52 + } 53 + } 54 + } catch (e: any) { 55 + logger.error('Failed to check signup queue', {err: e.toString()}) 56 + } finally { 57 + setProcessing(false) 58 + } 59 + }, [ 60 + setProcessing, 61 + setEstimatedTime, 62 + setPlaceInQueue, 63 + onboardingDispatch, 64 + agent, 65 + ]) 66 + 67 + React.useEffect(() => { 68 + checkStatus() 69 + const interval = setInterval(checkStatus, 60e3) 70 + return () => clearInterval(interval) 71 + }, [checkStatus]) 72 + 73 + const checkBtn = ( 74 + <Button 75 + variant="solid" 76 + color="primary" 77 + size="large" 78 + label={_(msg`Check my status`)} 79 + onPress={checkStatus} 80 + disabled={isProcessing}> 81 + <ButtonText> 82 + <Trans>Check my status</Trans> 83 + </ButtonText> 84 + {isProcessing && <ButtonIcon icon={Loader} />} 85 + </Button> 86 + ) 87 + 88 + return ( 89 + <View 90 + aria-modal 91 + role="dialog" 92 + aria-role="dialog" 93 + aria-label={_(msg`You're in line`)} 94 + accessibilityLabel={_(msg`You're in line`)} 95 + accessibilityHint="" 96 + style={[a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}> 97 + <ScrollView 98 + style={[a.h_full, a.w_full]} 99 + contentContainerStyle={{borderWidth: 0}}> 100 + <View 101 + style={[a.flex_row, a.justify_center, gtMobile ? a.pt_4xl : a.px_xl]}> 102 + <View style={[a.flex_1, {maxWidth: COL_WIDTH}]}> 103 + <View 104 + style={[a.w_full, a.justify_center, a.align_center, a.my_4xl]}> 105 + <Logo width={120} /> 106 + </View> 107 + 108 + <Text style={[a.text_4xl, a.font_bold, a.pb_sm]}> 109 + <Trans>You're in line</Trans> 110 + </Text> 111 + <P style={[t.atoms.text_contrast_medium]}> 112 + <Trans> 113 + There's been a rush of new users to Bluesky! We'll activate your 114 + account as soon as we can. 115 + </Trans> 116 + </P> 117 + 118 + <View 119 + style={[ 120 + a.rounded_sm, 121 + a.px_2xl, 122 + a.py_4xl, 123 + a.mt_2xl, 124 + t.atoms.bg_contrast_50, 125 + ]}> 126 + {typeof placeInQueue === 'number' && ( 127 + <Text 128 + style={[a.text_5xl, a.text_center, a.font_bold, a.mb_2xl]}> 129 + {placeInQueue} 130 + </Text> 131 + )} 132 + <P style={[a.text_center]}> 133 + {typeof placeInQueue === 'number' ? ( 134 + <Trans>left to go.</Trans> 135 + ) : ( 136 + <Trans>You are in line.</Trans> 137 + )}{' '} 138 + {estimatedTime ? ( 139 + <Trans> 140 + We estimate {estimatedTime} until your account is ready. 141 + </Trans> 142 + ) : ( 143 + <Trans> 144 + We will let you know when your account is ready. 145 + </Trans> 146 + )} 147 + </P> 148 + </View> 149 + 150 + {isWeb && gtMobile && ( 151 + <View style={[a.w_full, a.flex_row, a.justify_between, a.pt_5xl]}> 152 + <Button 153 + variant="ghost" 154 + size="large" 155 + label={_(msg`Log out`)} 156 + onPress={() => logout('SignupQueued')}> 157 + <ButtonText style={[{color: t.palette.primary_500}]}> 158 + <Trans>Log out</Trans> 159 + </ButtonText> 160 + </Button> 161 + {checkBtn} 162 + </View> 163 + )} 164 + </View> 165 + 166 + <View style={{height: 200}} /> 167 + </View> 168 + </ScrollView> 169 + 170 + {(!isWeb || !gtMobile) && ( 171 + <View 172 + style={[ 173 + a.align_center, 174 + gtMobile ? a.px_5xl : a.px_xl, 175 + { 176 + paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom), 177 + }, 178 + ]}> 179 + <View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}> 180 + {checkBtn} 181 + <Button 182 + variant="ghost" 183 + size="large" 184 + label={_(msg`Log out`)} 185 + onPress={() => logout('SignupQueued')}> 186 + <ButtonText style={[{color: t.palette.primary_500}]}> 187 + <Trans>Log out</Trans> 188 + </ButtonText> 189 + </Button> 190 + </View> 191 + </View> 192 + )} 193 + </View> 194 + ) 195 + } 196 + 197 + function msToString(ms: number | undefined): string | undefined { 198 + if (ms && ms > 0) { 199 + const estimatedTimeMins = Math.ceil(ms / 60e3) 200 + if (estimatedTimeMins > 59) { 201 + const estimatedTimeHrs = Math.round(estimatedTimeMins / 60) 202 + if (estimatedTimeHrs > 6) { 203 + // dont even bother 204 + return undefined 205 + } 206 + // hours 207 + return `${estimatedTimeHrs} ${plural(estimatedTimeHrs, { 208 + one: 'hour', 209 + other: 'hours', 210 + })}` 211 + } 212 + // minutes 213 + return `${estimatedTimeMins} ${plural(estimatedTimeMins, { 214 + one: 'minute', 215 + other: 'minutes', 216 + })}` 217 + } 218 + return undefined 219 + }
+4 -1
src/state/persisted/schema.ts
··· 17 17 emailAuthFactor: z.boolean().optional(), 18 18 refreshJwt: z.string().optional(), // optional because it can expire 19 19 accessJwt: z.string().optional(), // optional because it can expire 20 - deactivated: z.boolean().optional(), 20 + signupQueued: z.boolean().optional(), 21 + status: z 22 + .enum(['active', 'takendown', 'suspended', 'deactivated']) 23 + .optional(), 21 24 pdsUrl: z.string().optional(), 22 25 }) 23 26 export type PersistedAccount = z.infer<typeof accountSchema>
+58 -29
src/state/session/__tests__/session-test.ts
··· 50 50 "accounts": [ 51 51 { 52 52 "accessJwt": "alice-access-jwt-1", 53 - "deactivated": false, 54 53 "did": "alice-did", 55 54 "email": undefined, 56 55 "emailAuthFactor": false, ··· 59 58 "pdsUrl": undefined, 60 59 "refreshJwt": "alice-refresh-jwt-1", 61 60 "service": "https://alice.com/", 61 + "signupQueued": false, 62 + "status": "active", 62 63 }, 63 64 ], 64 65 "currentAgentState": { ··· 87 88 "accounts": [ 88 89 { 89 90 "accessJwt": undefined, 90 - "deactivated": false, 91 91 "did": "alice-did", 92 92 "email": undefined, 93 93 "emailAuthFactor": false, ··· 96 96 "pdsUrl": undefined, 97 97 "refreshJwt": undefined, 98 98 "service": "https://alice.com/", 99 + "signupQueued": false, 100 + "status": "active", 99 101 }, 100 102 ], 101 103 "currentAgentState": { ··· 136 138 "accounts": [ 137 139 { 138 140 "accessJwt": "alice-access-jwt-1", 139 - "deactivated": false, 140 141 "did": "alice-did", 141 142 "email": undefined, 142 143 "emailAuthFactor": false, ··· 145 146 "pdsUrl": undefined, 146 147 "refreshJwt": "alice-refresh-jwt-1", 147 148 "service": "https://alice.com/", 149 + "signupQueued": false, 150 + "status": "active", 148 151 }, 149 152 ], 150 153 "currentAgentState": { ··· 183 186 "accounts": [ 184 187 { 185 188 "accessJwt": "bob-access-jwt-1", 186 - "deactivated": false, 187 189 "did": "bob-did", 188 190 "email": undefined, 189 191 "emailAuthFactor": false, ··· 192 194 "pdsUrl": undefined, 193 195 "refreshJwt": "bob-refresh-jwt-1", 194 196 "service": "https://bob.com/", 197 + "signupQueued": false, 198 + "status": "active", 195 199 }, 196 200 { 197 201 "accessJwt": "alice-access-jwt-1", 198 - "deactivated": false, 199 202 "did": "alice-did", 200 203 "email": undefined, 201 204 "emailAuthFactor": false, ··· 204 207 "pdsUrl": undefined, 205 208 "refreshJwt": "alice-refresh-jwt-1", 206 209 "service": "https://alice.com/", 210 + "signupQueued": false, 211 + "status": "active", 207 212 }, 208 213 ], 209 214 "currentAgentState": { ··· 242 247 "accounts": [ 243 248 { 244 249 "accessJwt": "alice-access-jwt-2", 245 - "deactivated": false, 246 250 "did": "alice-did", 247 251 "email": undefined, 248 252 "emailAuthFactor": false, ··· 251 255 "pdsUrl": undefined, 252 256 "refreshJwt": "alice-refresh-jwt-2", 253 257 "service": "https://alice.com/", 258 + "signupQueued": false, 259 + "status": "active", 254 260 }, 255 261 { 256 262 "accessJwt": "bob-access-jwt-1", 257 - "deactivated": false, 258 263 "did": "bob-did", 259 264 "email": undefined, 260 265 "emailAuthFactor": false, ··· 263 268 "pdsUrl": undefined, 264 269 "refreshJwt": "bob-refresh-jwt-1", 265 270 "service": "https://bob.com/", 271 + "signupQueued": false, 272 + "status": "active", 266 273 }, 267 274 ], 268 275 "currentAgentState": { ··· 299 306 "accounts": [ 300 307 { 301 308 "accessJwt": "jay-access-jwt-1", 302 - "deactivated": false, 303 309 "did": "jay-did", 304 310 "email": undefined, 305 311 "emailAuthFactor": false, ··· 308 314 "pdsUrl": undefined, 309 315 "refreshJwt": "jay-refresh-jwt-1", 310 316 "service": "https://jay.com/", 317 + "signupQueued": false, 318 + "status": "active", 311 319 }, 312 320 { 313 321 "accessJwt": "alice-access-jwt-2", 314 - "deactivated": false, 315 322 "did": "alice-did", 316 323 "email": undefined, 317 324 "emailAuthFactor": false, ··· 320 327 "pdsUrl": undefined, 321 328 "refreshJwt": "alice-refresh-jwt-2", 322 329 "service": "https://alice.com/", 330 + "signupQueued": false, 331 + "status": "active", 323 332 }, 324 333 { 325 334 "accessJwt": "bob-access-jwt-1", 326 - "deactivated": false, 327 335 "did": "bob-did", 328 336 "email": undefined, 329 337 "emailAuthFactor": false, ··· 332 340 "pdsUrl": undefined, 333 341 "refreshJwt": "bob-refresh-jwt-1", 334 342 "service": "https://bob.com/", 343 + "signupQueued": false, 344 + "status": "active", 335 345 }, 336 346 ], 337 347 "currentAgentState": { ··· 364 374 "accounts": [ 365 375 { 366 376 "accessJwt": undefined, 367 - "deactivated": false, 368 377 "did": "jay-did", 369 378 "email": undefined, 370 379 "emailAuthFactor": false, ··· 373 382 "pdsUrl": undefined, 374 383 "refreshJwt": undefined, 375 384 "service": "https://jay.com/", 385 + "signupQueued": false, 386 + "status": "active", 376 387 }, 377 388 { 378 389 "accessJwt": undefined, 379 - "deactivated": false, 380 390 "did": "alice-did", 381 391 "email": undefined, 382 392 "emailAuthFactor": false, ··· 385 395 "pdsUrl": undefined, 386 396 "refreshJwt": undefined, 387 397 "service": "https://alice.com/", 398 + "signupQueued": false, 399 + "status": "active", 388 400 }, 389 401 { 390 402 "accessJwt": undefined, 391 - "deactivated": false, 392 403 "did": "bob-did", 393 404 "email": undefined, 394 405 "emailAuthFactor": false, ··· 397 408 "pdsUrl": undefined, 398 409 "refreshJwt": undefined, 399 410 "service": "https://bob.com/", 411 + "signupQueued": false, 412 + "status": "active", 400 413 }, 401 414 ], 402 415 "currentAgentState": { ··· 446 459 "accounts": [ 447 460 { 448 461 "accessJwt": undefined, 449 - "deactivated": false, 450 462 "did": "alice-did", 451 463 "email": undefined, 452 464 "emailAuthFactor": false, ··· 455 467 "pdsUrl": undefined, 456 468 "refreshJwt": undefined, 457 469 "service": "https://alice.com/", 470 + "signupQueued": false, 471 + "status": "active", 458 472 }, 459 473 ], 460 474 "currentAgentState": { ··· 490 504 "accounts": [ 491 505 { 492 506 "accessJwt": "alice-access-jwt-2", 493 - "deactivated": false, 494 507 "did": "alice-did", 495 508 "email": undefined, 496 509 "emailAuthFactor": false, ··· 499 512 "pdsUrl": undefined, 500 513 "refreshJwt": "alice-refresh-jwt-2", 501 514 "service": "https://alice.com/", 515 + "signupQueued": false, 516 + "status": "active", 502 517 }, 503 518 ], 504 519 "currentAgentState": { ··· 601 616 "accounts": [ 602 617 { 603 618 "accessJwt": "bob-access-jwt-1", 604 - "deactivated": false, 605 619 "did": "bob-did", 606 620 "email": undefined, 607 621 "emailAuthFactor": false, ··· 610 624 "pdsUrl": undefined, 611 625 "refreshJwt": "bob-refresh-jwt-1", 612 626 "service": "https://bob.com/", 627 + "signupQueued": false, 628 + "status": "active", 613 629 }, 614 630 ], 615 631 "currentAgentState": { ··· 681 697 "accounts": [ 682 698 { 683 699 "accessJwt": "alice-access-jwt-2", 684 - "deactivated": false, 685 700 "did": "alice-did", 686 701 "email": "alice@foo.bar", 687 702 "emailAuthFactor": false, ··· 690 705 "pdsUrl": undefined, 691 706 "refreshJwt": "alice-refresh-jwt-2", 692 707 "service": "https://alice.com/", 708 + "signupQueued": false, 709 + "status": "active", 693 710 }, 694 711 ], 695 712 "currentAgentState": { ··· 731 748 "accounts": [ 732 749 { 733 750 "accessJwt": "alice-access-jwt-3", 734 - "deactivated": false, 735 751 "did": "alice-did", 736 752 "email": "alice@foo.baz", 737 753 "emailAuthFactor": true, ··· 740 756 "pdsUrl": undefined, 741 757 "refreshJwt": "alice-refresh-jwt-3", 742 758 "service": "https://alice.com/", 759 + "signupQueued": false, 760 + "status": "active", 743 761 }, 744 762 ], 745 763 "currentAgentState": { ··· 781 799 "accounts": [ 782 800 { 783 801 "accessJwt": "alice-access-jwt-4", 784 - "deactivated": false, 785 802 "did": "alice-did", 786 803 "email": "alice@foo.baz", 787 804 "emailAuthFactor": false, ··· 790 807 "pdsUrl": undefined, 791 808 "refreshJwt": "alice-refresh-jwt-4", 792 809 "service": "https://alice.com/", 810 + "signupQueued": false, 811 + "status": "active", 793 812 }, 794 813 ], 795 814 "currentAgentState": { ··· 937 956 "accounts": [ 938 957 { 939 958 "accessJwt": "bob-access-jwt-1", 940 - "deactivated": false, 941 959 "did": "bob-did", 942 960 "email": undefined, 943 961 "emailAuthFactor": false, ··· 946 964 "pdsUrl": undefined, 947 965 "refreshJwt": "bob-refresh-jwt-1", 948 966 "service": "https://bob.com/", 967 + "signupQueued": false, 968 + "status": "active", 949 969 }, 950 970 { 951 971 "accessJwt": "alice-access-jwt-2", 952 - "deactivated": false, 953 972 "did": "alice-did", 954 973 "email": "alice@foo.bar", 955 974 "emailAuthFactor": false, ··· 958 977 "pdsUrl": undefined, 959 978 "refreshJwt": "alice-refresh-jwt-2", 960 979 "service": "https://alice.com/", 980 + "signupQueued": false, 981 + "status": "active", 961 982 }, 962 983 ], 963 984 "currentAgentState": { ··· 997 1018 "accounts": [ 998 1019 { 999 1020 "accessJwt": "bob-access-jwt-2", 1000 - "deactivated": false, 1001 1021 "did": "bob-did", 1002 1022 "email": undefined, 1003 1023 "emailAuthFactor": false, ··· 1006 1026 "pdsUrl": undefined, 1007 1027 "refreshJwt": "bob-refresh-jwt-2", 1008 1028 "service": "https://bob.com/", 1029 + "signupQueued": false, 1030 + "status": "active", 1009 1031 }, 1010 1032 { 1011 1033 "accessJwt": "alice-access-jwt-2", 1012 - "deactivated": false, 1013 1034 "did": "alice-did", 1014 1035 "email": "alice@foo.bar", 1015 1036 "emailAuthFactor": false, ··· 1018 1039 "pdsUrl": undefined, 1019 1040 "refreshJwt": "alice-refresh-jwt-2", 1020 1041 "service": "https://alice.com/", 1042 + "signupQueued": false, 1043 + "status": "active", 1021 1044 }, 1022 1045 ], 1023 1046 "currentAgentState": { ··· 1156 1179 "accounts": [ 1157 1180 { 1158 1181 "accessJwt": "alice-access-jwt-1", 1159 - "deactivated": false, 1160 1182 "did": "alice-did", 1161 1183 "email": undefined, 1162 1184 "emailAuthFactor": false, ··· 1165 1187 "pdsUrl": undefined, 1166 1188 "refreshJwt": "alice-refresh-jwt-1", 1167 1189 "service": "https://alice.com/", 1190 + "signupQueued": false, 1191 + "status": "active", 1168 1192 }, 1169 1193 ], 1170 1194 "currentAgentState": { ··· 1218 1242 "accounts": [ 1219 1243 { 1220 1244 "accessJwt": undefined, 1221 - "deactivated": false, 1222 1245 "did": "alice-did", 1223 1246 "email": undefined, 1224 1247 "emailAuthFactor": false, ··· 1227 1250 "pdsUrl": undefined, 1228 1251 "refreshJwt": undefined, 1229 1252 "service": "https://alice.com/", 1253 + "signupQueued": false, 1254 + "status": "active", 1230 1255 }, 1231 1256 ], 1232 1257 "currentAgentState": { ··· 1280 1305 "accounts": [ 1281 1306 { 1282 1307 "accessJwt": undefined, 1283 - "deactivated": false, 1284 1308 "did": "alice-did", 1285 1309 "email": undefined, 1286 1310 "emailAuthFactor": false, ··· 1289 1313 "pdsUrl": undefined, 1290 1314 "refreshJwt": undefined, 1291 1315 "service": "https://alice.com/", 1316 + "signupQueued": false, 1317 + "status": "active", 1292 1318 }, 1293 1319 ], 1294 1320 "currentAgentState": { ··· 1371 1397 "accounts": [ 1372 1398 { 1373 1399 "accessJwt": "jay-access-jwt-1", 1374 - "deactivated": false, 1375 1400 "did": "jay-did", 1376 1401 "email": undefined, 1377 1402 "emailAuthFactor": false, ··· 1380 1405 "pdsUrl": undefined, 1381 1406 "refreshJwt": "jay-refresh-jwt-1", 1382 1407 "service": "https://jay.com/", 1408 + "signupQueued": false, 1409 + "status": "active", 1383 1410 }, 1384 1411 { 1385 1412 "accessJwt": "bob-access-jwt-2", 1386 - "deactivated": false, 1387 1413 "did": "bob-did", 1388 1414 "email": undefined, 1389 1415 "emailAuthFactor": false, ··· 1392 1418 "pdsUrl": undefined, 1393 1419 "refreshJwt": "bob-refresh-jwt-2", 1394 1420 "service": "https://alice.com/", 1421 + "signupQueued": false, 1422 + "status": "active", 1395 1423 }, 1396 1424 ], 1397 1425 "currentAgentState": { ··· 1429 1457 "accounts": [ 1430 1458 { 1431 1459 "accessJwt": "clarence-access-jwt-2", 1432 - "deactivated": false, 1433 1460 "did": "clarence-did", 1434 1461 "email": undefined, 1435 1462 "emailAuthFactor": false, ··· 1438 1465 "pdsUrl": undefined, 1439 1466 "refreshJwt": "clarence-refresh-jwt-2", 1440 1467 "service": "https://clarence.com/", 1468 + "signupQueued": false, 1469 + "status": "active", 1441 1470 }, 1442 1471 ], 1443 1472 "currentAgentState": {
+6 -4
src/state/session/agent.ts
··· 16 16 configureModerationForGuest, 17 17 } from './moderation' 18 18 import {SessionAccount} from './types' 19 - import {isSessionDeactivated, isSessionExpired} from './util' 19 + import {isSessionExpired, isSignupQueued} from './util' 20 20 21 21 export function createPublicAgent() { 22 22 configureModerationForGuest() // Side effect but only relevant for tests ··· 51 51 await networkRetry(1, () => agent.resumeSession(prevSession)) 52 52 } else { 53 53 agent.session = prevSession 54 - if (!storedAccount.deactivated) { 54 + if (!storedAccount.signupQueued) { 55 55 // Intentionally not awaited to unblock the UI: 56 56 networkRetry(3, () => agent.resumeSession(prevSession)).catch( 57 57 (e: any) => { ··· 135 135 const account = agentToSessionAccountOrThrow(agent) 136 136 const gates = tryFetchGates(account.did, 'prefer-fresh-gates') 137 137 const moderation = configureModerationForAccount(agent, account) 138 - if (!account.deactivated) { 138 + if (!account.signupQueued) { 139 139 /*dont await*/ agent.upsertProfile(_existing => { 140 140 return { 141 141 displayName: '', ··· 234 234 emailAuthFactor: agent.session.emailAuthFactor || false, 235 235 refreshJwt: agent.session.refreshJwt, 236 236 accessJwt: agent.session.accessJwt, 237 - deactivated: isSessionDeactivated(agent.session.accessJwt), 237 + signupQueued: isSignupQueued(agent.session.accessJwt), 238 + // @ts-expect-error TODO remove when backend is ready 239 + status: agent.session.status || 'active', 238 240 pdsUrl: agent.pdsUrl?.toString(), 239 241 } 240 242 }
+1 -1
src/state/session/index.tsx
··· 17 17 } from './agent' 18 18 import {getInitialState, reducer} from './reducer' 19 19 20 - export {isSessionDeactivated} from './util' 20 + export {isSignupQueued} from './util' 21 21 export type {SessionAccount} from '#/state/session/types' 22 22 import {SessionApiContext, SessionStateContext} from '#/state/session/types' 23 23
+3 -2
src/state/session/util.ts
··· 10 10 return accounts.find(a => a.did === currentAccount?.did) 11 11 } 12 12 13 - export function isSessionDeactivated(accessJwt: string | undefined) { 13 + export function isSignupQueued(accessJwt: string | undefined) { 14 14 if (accessJwt) { 15 15 const sessData = jwtDecode(accessJwt) 16 16 return ( 17 - hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated' 17 + hasProp(sessData, 'scope') && 18 + sessData.scope === 'com.atproto.signupQueued' 18 19 ) 19 20 } 20 21 return false
+53 -1
src/view/com/modals/DeleteAccount.tsx
··· 18 18 import {cleanError} from 'lib/strings/errors' 19 19 import {colors, gradients, s} from 'lib/styles' 20 20 import {useTheme} from 'lib/ThemeContext' 21 - import {isAndroid} from 'platform/detection' 21 + import {isAndroid, isWeb} from 'platform/detection' 22 + import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog' 23 + import {atoms as a, useTheme as useNewTheme} from '#/alf' 24 + import {useDialogControl} from '#/components/Dialog' 25 + import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 26 + import {InlineLinkText} from '#/components/Link' 27 + import {Text as NewText} from '#/components/Typography' 22 28 import {resetToTab} from '../../../Navigation' 23 29 import {ErrorMessage} from '../util/error/ErrorMessage' 24 30 import {Text} from '../util/text/Text' ··· 30 36 export function Component({}: {}) { 31 37 const pal = usePalette('default') 32 38 const theme = useTheme() 39 + const t = useNewTheme() 33 40 const {currentAccount} = useSession() 34 41 const agent = useAgent() 35 42 const {removeAccount} = useSessionApi() ··· 41 48 const [password, setPassword] = React.useState<string>('') 42 49 const [isProcessing, setIsProcessing] = React.useState<boolean>(false) 43 50 const [error, setError] = React.useState<string>('') 51 + const deactivateAccountControl = useDialogControl() 44 52 const onPressSendEmail = async () => { 45 53 setError('') 46 54 setIsProcessing(true) ··· 168 176 </TouchableOpacity> 169 177 </> 170 178 )} 179 + 180 + <View style={[!isWeb && a.px_xl]}> 181 + <View 182 + style={[ 183 + a.w_full, 184 + a.flex_row, 185 + a.gap_sm, 186 + a.mt_lg, 187 + a.p_lg, 188 + a.rounded_sm, 189 + t.atoms.bg_contrast_25, 190 + ]}> 191 + <CircleInfo 192 + size="md" 193 + style={[ 194 + a.relative, 195 + { 196 + top: -1, 197 + }, 198 + ]} 199 + /> 200 + 201 + <NewText style={[a.leading_snug, a.flex_1]}> 202 + <Trans> 203 + You can also temporarily deactivate your account instead, 204 + and reactivate it at any time. 205 + </Trans>{' '} 206 + <InlineLinkText 207 + label={_( 208 + msg`Click here for more information on deactivating your account`, 209 + )} 210 + to="#" 211 + onPress={e => { 212 + e.preventDefault() 213 + deactivateAccountControl.open() 214 + return false 215 + }}> 216 + <Trans>Click here for more information.</Trans> 217 + </InlineLinkText> 218 + </NewText> 219 + </View> 220 + </View> 221 + 222 + <DeactivateAccountDialog control={deactivateAccountControl} /> 171 223 </> 172 224 ) : ( 173 225 <>
+29
src/view/screens/Settings/index.tsx
··· 60 60 import * as Toast from 'view/com/util/Toast' 61 61 import {UserAvatar} from 'view/com/util/UserAvatar' 62 62 import {ScrollView} from 'view/com/util/Views' 63 + import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog' 63 64 import {useTheme} from '#/alf' 64 65 import {useDialogControl} from '#/components/Dialog' 65 66 import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' ··· 306 307 await clearLegacyStorage() 307 308 Toast.show(_(msg`Legacy storage cleared, you need to restart the app now.`)) 308 309 }, [_]) 310 + 311 + const deactivateAccountControl = useDialogControl() 312 + const onPressDeactivateAccount = React.useCallback(() => { 313 + deactivateAccountControl.open() 314 + }, [deactivateAccountControl]) 309 315 310 316 const {mutate: onPressDeleteChatDeclaration} = useDeleteActorDeclaration() 311 317 ··· 791 797 <Trans>Export My Data</Trans> 792 798 </Text> 793 799 </TouchableOpacity> 800 + 801 + <TouchableOpacity 802 + style={[pal.view, styles.linkCard]} 803 + onPress={onPressDeactivateAccount} 804 + accessible={true} 805 + accessibilityRole="button" 806 + accessibilityLabel={_(msg`Deactivate account`)} 807 + accessibilityHint={_( 808 + msg`Opens modal for account deactivation confirmation`, 809 + )}> 810 + <View style={[styles.iconContainer, dangerBg]}> 811 + <FontAwesomeIcon 812 + icon={'users-slash'} 813 + style={dangerText as FontAwesomeIconStyle} 814 + size={18} 815 + /> 816 + </View> 817 + <Text type="lg" style={dangerText}> 818 + <Trans>Deactivate my account</Trans> 819 + </Text> 820 + </TouchableOpacity> 821 + <DeactivateAccountDialog control={deactivateAccountControl} /> 822 + 794 823 <TouchableOpacity 795 824 style={[pal.view, styles.linkCard]} 796 825 onPress={onPressDeleteAccount}
+6 -2
src/view/shell/createNativeStackNavigatorWithAuth.tsx
··· 32 32 import {isWeb} from 'platform/detection' 33 33 import {Deactivated} from '#/screens/Deactivated' 34 34 import {Onboarding} from '#/screens/Onboarding' 35 + import {SignupQueued} from '#/screens/SignupQueued' 35 36 import {LoggedOut} from '../com/auth/LoggedOut' 36 37 import {BottomBarWeb} from './bottom-bar/BottomBarWeb' 37 38 import {DesktopLeftNav} from './desktop/LeftNav' ··· 102 103 if ((!PWI_ENABLED || activeRouteRequiresAuth) && !hasSession) { 103 104 return <LoggedOut /> 104 105 } 105 - if (hasSession && currentAccount?.deactivated) { 106 - return <Deactivated /> 106 + if (hasSession && currentAccount?.signupQueued) { 107 + return <SignupQueued /> 107 108 } 108 109 if (showLoggedOut) { 109 110 return <LoggedOut onDismiss={() => setShowLoggedOut(false)} /> 111 + } 112 + if (currentAccount?.status === 'deactivated') { 113 + return <Deactivated /> 110 114 } 111 115 if (onboardingState.isActive) { 112 116 return <Onboarding />