Add account-activation queueing to signup (#2613)
* Add deactivated-account tracking * Center button text * Add Deactivated screen * Add icon to Deactivated screen * Abort session resumption if the session is deactivated * Implement deactivated screen status checks * Bump api@0.9.5 * Use new typo-fixed scope * UI refinementszio/stable
parent
335bef3d30
commit
5443503593
|
@ -39,7 +39,7 @@
|
||||||
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android"
|
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.9.1",
|
"@atproto/api": "^0.9.5",
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
|
|
|
@ -337,6 +337,7 @@ export function Button({
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.overflow_hidden,
|
a.overflow_hidden,
|
||||||
|
a.justify_center,
|
||||||
...baseStyles,
|
...baseStyles,
|
||||||
...(state.hovered || state.pressed ? hoverStyles : []),
|
...(state.hovered || state.pressed ? hoverStyles : []),
|
||||||
...(state.focused ? focusStyles : []),
|
...(state.focused ? focusStyles : []),
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import React from 'react'
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
useSharedValue,
|
||||||
|
useAnimatedStyle,
|
||||||
|
withRepeat,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {Props, useCommonSVGProps} from '#/components/icons/common'
|
||||||
|
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
|
||||||
|
|
||||||
|
export function Loader(props: Props) {
|
||||||
|
const common = useCommonSVGProps(props)
|
||||||
|
const rotation = useSharedValue(0)
|
||||||
|
|
||||||
|
const animatedStyles = useAnimatedStyle(() => ({
|
||||||
|
transform: [{rotate: rotation.value + 'deg'}],
|
||||||
|
}))
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
rotation.value = withRepeat(
|
||||||
|
withTiming(360, {duration: 500, easing: Easing.linear}),
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
}, [rotation])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
a.relative,
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
{width: common.size, height: common.size},
|
||||||
|
animatedStyles,
|
||||||
|
]}>
|
||||||
|
<Icon {...props} style={[a.absolute, a.inset_0, props.style]} />
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Group3_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M17 16H21.1456C20.8246 11.4468 17.7199 9.48509 15.0001 10.1147M10 4C10 5.65685 8.65685 7 7 7C5.34315 7 4 5.65685 4 4C4 2.34315 5.34315 1 7 1C8.65685 1 10 2.34315 10 4ZM18.5 4.5C18.5 5.88071 17.3807 7 16 7C14.6193 7 13.5 5.88071 13.5 4.5C13.5 3.11929 14.6193 2 16 2C17.3807 2 18.5 3.11929 18.5 4.5ZM1 17H13C12.3421 7.66667 1.65792 7.66667 1 17Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Loader_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 5a7 7 0 0 0-5.218 11.666A1 1 0 0 1 5.292 18a9 9 0 1 1 13.416 0 1 1 0 1 1-1.49-1.334A7 7 0 0 0 12 5Z',
|
||||||
|
})
|
|
@ -0,0 +1,208 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
|
import {getAgent, isSessionDeactivated, useSessionApi} from '#/state/session'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {pluralize} from '#/lib/strings/helpers'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {H2, P} from '#/components/Typography'
|
||||||
|
import {ScrollView} from '#/view/com/util/Views'
|
||||||
|
import {Group3_Stroke2_Corner0_Rounded as Group3} from '#/components/icons/Group3'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
|
||||||
|
const COL_WIDTH = 400
|
||||||
|
|
||||||
|
export function Deactivated() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
|
const {logout} = useSessionApi()
|
||||||
|
|
||||||
|
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 getAgent().com.atproto.temp.checkSignupQueue()
|
||||||
|
if (res.data.activated) {
|
||||||
|
// ready to go, exchange the access token for a usable one and kick off onboarding
|
||||||
|
await getAgent().refreshSession()
|
||||||
|
if (!isSessionDeactivated(getAgent().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])
|
||||||
|
|
||||||
|
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.mt_4xl]}>
|
||||||
|
<Group3 fill="none" stroke={t.palette.contrast_900} width={120} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<H2 style={[a.pb_sm]}>
|
||||||
|
<Trans>You're in line</Trans>
|
||||||
|
</H2>
|
||||||
|
<P style={[t.atoms.text_contrast_700]}>
|
||||||
|
<Trans>
|
||||||
|
There's been a rush of new users! 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}>
|
||||||
|
<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}>
|
||||||
|
<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} ${pluralize(estimatedTimeHrs, 'hour')}`
|
||||||
|
}
|
||||||
|
// minutes
|
||||||
|
return `${estimatedTimeMins} ${pluralize(estimatedTimeMins, 'minute')}`
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ const accountSchema = z.object({
|
||||||
emailConfirmed: z.boolean().optional(),
|
emailConfirmed: z.boolean().optional(),
|
||||||
refreshJwt: z.string().optional(), // optional because it can expire
|
refreshJwt: z.string().optional(), // optional because it can expire
|
||||||
accessJwt: z.string().optional(), // optional because it can expire
|
accessJwt: z.string().optional(), // optional because it can expire
|
||||||
|
deactivated: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
export type PersistedAccount = z.infer<typeof accountSchema>
|
export type PersistedAccount = z.infer<typeof accountSchema>
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {emitSessionDropped} from '../events'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
import {useCloseAllActiveElements} from '#/state/util'
|
import {useCloseAllActiveElements} from '#/state/util'
|
||||||
import {track} from '#/lib/analytics/analytics'
|
import {track} from '#/lib/analytics/analytics'
|
||||||
|
import {hasProp} from '#/lib/type-guards'
|
||||||
|
|
||||||
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
|
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
|
||||||
|
|
||||||
|
@ -125,6 +126,7 @@ function createPersistSessionHandler(
|
||||||
handle: session?.handle || account.handle,
|
handle: session?.handle || account.handle,
|
||||||
email: session?.email || account.email,
|
email: session?.email || account.email,
|
||||||
emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
|
emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
|
||||||
|
deactivated: isSessionDeactivated(session?.accessJwt),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Tokens are undefined if the session expires, or if creation fails for
|
* Tokens are undefined if the session expires, or if creation fails for
|
||||||
|
@ -139,6 +141,7 @@ function createPersistSessionHandler(
|
||||||
did: refreshedAccount.did,
|
did: refreshedAccount.did,
|
||||||
handle: refreshedAccount.handle,
|
handle: refreshedAccount.handle,
|
||||||
service: refreshedAccount.service,
|
service: refreshedAccount.service,
|
||||||
|
deactivated: refreshedAccount.deactivated,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (expired) {
|
if (expired) {
|
||||||
|
@ -235,11 +238,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
throw new Error(`session: createAccount failed to establish a session`)
|
throw new Error(`session: createAccount failed to establish a session`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deactivated = isSessionDeactivated(agent.session.accessJwt)
|
||||||
|
if (!deactivated) {
|
||||||
/*dont await*/ agent.upsertProfile(_existing => {
|
/*dont await*/ agent.upsertProfile(_existing => {
|
||||||
return {
|
return {
|
||||||
displayName: handle,
|
displayName: handle,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const account: SessionAccount = {
|
const account: SessionAccount = {
|
||||||
service: agent.service.toString(),
|
service: agent.service.toString(),
|
||||||
|
@ -249,6 +255,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
emailConfirmed: false,
|
emailConfirmed: false,
|
||||||
refreshJwt: agent.session.refreshJwt,
|
refreshJwt: agent.session.refreshJwt,
|
||||||
accessJwt: agent.session.accessJwt,
|
accessJwt: agent.session.accessJwt,
|
||||||
|
deactivated,
|
||||||
}
|
}
|
||||||
|
|
||||||
agent.setPersistSessionHandler(
|
agent.setPersistSessionHandler(
|
||||||
|
@ -305,6 +312,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
emailConfirmed: agent.session.emailConfirmed || false,
|
emailConfirmed: agent.session.emailConfirmed || false,
|
||||||
refreshJwt: agent.session.refreshJwt,
|
refreshJwt: agent.session.refreshJwt,
|
||||||
accessJwt: agent.session.accessJwt,
|
accessJwt: agent.session.accessJwt,
|
||||||
|
deactivated: isSessionDeactivated(agent.session.accessJwt),
|
||||||
}
|
}
|
||||||
|
|
||||||
agent.setPersistSessionHandler(
|
agent.setPersistSessionHandler(
|
||||||
|
@ -392,6 +400,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
refreshJwt: account.refreshJwt || '',
|
refreshJwt: account.refreshJwt || '',
|
||||||
did: account.did,
|
did: account.did,
|
||||||
handle: account.handle,
|
handle: account.handle,
|
||||||
|
deactivated:
|
||||||
|
isSessionDeactivated(account.accessJwt) || account.deactivated,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canReusePrevSession) {
|
if (canReusePrevSession) {
|
||||||
|
@ -402,6 +412,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
queryClient.clear()
|
queryClient.clear()
|
||||||
upsertAccount(account)
|
upsertAccount(account)
|
||||||
|
|
||||||
|
if (prevSession.deactivated) {
|
||||||
|
// don't attempt to resume
|
||||||
|
// use will be taken to the deactivated screen
|
||||||
|
logger.info(`session: reusing session for deactivated account`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Intentionally not awaited to unblock the UI:
|
// Intentionally not awaited to unblock the UI:
|
||||||
resumeSessionWithFreshAccount()
|
resumeSessionWithFreshAccount()
|
||||||
.then(freshAccount => {
|
.then(freshAccount => {
|
||||||
|
@ -466,6 +483,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
emailConfirmed: agent.session.emailConfirmed || false,
|
emailConfirmed: agent.session.emailConfirmed || false,
|
||||||
refreshJwt: agent.session.refreshJwt,
|
refreshJwt: agent.session.refreshJwt,
|
||||||
accessJwt: agent.session.accessJwt,
|
accessJwt: agent.session.accessJwt,
|
||||||
|
deactivated: isSessionDeactivated(agent.session.accessJwt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -687,3 +705,13 @@ export function useRequireAuth() {
|
||||||
[hasSession, setShowLoggedOut, closeAll],
|
[hasSession, setShowLoggedOut, closeAll],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSessionDeactivated(accessJwt: string | undefined) {
|
||||||
|
if (accessJwt) {
|
||||||
|
const sessData = jwtDecode(accessJwt)
|
||||||
|
return (
|
||||||
|
hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {
|
||||||
} from '#/state/shell/logged-out'
|
} from '#/state/shell/logged-out'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {Deactivated} from '#/screens/Deactivated'
|
||||||
import {LoggedOut} from '../com/auth/LoggedOut'
|
import {LoggedOut} from '../com/auth/LoggedOut'
|
||||||
import {Onboarding} from '../com/auth/Onboarding'
|
import {Onboarding} from '../com/auth/Onboarding'
|
||||||
|
|
||||||
|
@ -92,7 +93,7 @@ function NativeStackNavigator({
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- our custom logic starts here ---
|
// --- our custom logic starts here ---
|
||||||
const {hasSession} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const activeRoute = state.routes[state.index]
|
const activeRoute = state.routes[state.index]
|
||||||
const activeDescriptor = descriptors[activeRoute.key]
|
const activeDescriptor = descriptors[activeRoute.key]
|
||||||
const activeRouteRequiresAuth = activeDescriptor.options.requireAuth ?? false
|
const activeRouteRequiresAuth = activeDescriptor.options.requireAuth ?? false
|
||||||
|
@ -103,6 +104,9 @@ function NativeStackNavigator({
|
||||||
if ((!PWI_ENABLED || activeRouteRequiresAuth) && !hasSession) {
|
if ((!PWI_ENABLED || activeRouteRequiresAuth) && !hasSession) {
|
||||||
return <LoggedOut />
|
return <LoggedOut />
|
||||||
}
|
}
|
||||||
|
if (hasSession && currentAccount?.deactivated) {
|
||||||
|
return <Deactivated />
|
||||||
|
}
|
||||||
if (showLoggedOut) {
|
if (showLoggedOut) {
|
||||||
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
|
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,10 +48,10 @@
|
||||||
typed-emitter "^2.1.0"
|
typed-emitter "^2.1.0"
|
||||||
zod "^3.21.4"
|
zod "^3.21.4"
|
||||||
|
|
||||||
"@atproto/api@^0.9.1":
|
"@atproto/api@^0.9.5":
|
||||||
version "0.9.1"
|
version "0.9.5"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.9.1.tgz#0b28baefa4af32bc4c05715b8641656f332546c6"
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.9.5.tgz#630e5d9520bba38d0cd348c8028ddbb73bd074f8"
|
||||||
integrity sha512-DHPc/dGgpf8sgPlfR9meIAk7s4YMll0g7HTq/W/LeaaaY0T6d3ZAtrgvjIU1aKCp5WNzTfzrmz0LIHIX46FHHw==
|
integrity sha512-4vlwTbiWSkCV0DkfNMawiH+26Fv7txPr4x0vwq6KPIBz28UHPK9UyPseLKxi6/Aok74aPr8ySJ4+nfcmwcp08Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/common-web" "^0.2.3"
|
"@atproto/common-web" "^0.2.3"
|
||||||
"@atproto/lexicon" "^0.3.1"
|
"@atproto/lexicon" "^0.3.1"
|
||||||
|
|
Loading…
Reference in New Issue