[🙅] Integrate deactivate (#4308)

* Update types

(cherry picked from commit 27deac1f367825771ba76fa098ec1b0a62dcf64a)

* Integrate into deactivate dialog

(cherry picked from commit 84f299a447259cc1fbfc7be607e28197779e4ec1)

* Integrate into Deactivated screen

(cherry picked from commit 29193f34822ecdf11e2a407197fa230285dfe846)

* Bump api sdk

(cherry picked from commit 738c622d3e5a23bfbb0d3bdce3a6bdf01e54ca60)

* Update permalink

(cherry picked from commit c10bf5c071d76c3054bc4ce9d313c10b1820f038)

* Bump sdk pkg

* Update types to match backend

* Loosen types for forwards compat

* Hydrate status from persisted data

* Refresh session when re-activating, clear query cache

* Show app password error

* Refactor dialog to clear state when closed

* Add app password error to Deactivated screen
zio/stable
Eric Bailey 2024-06-04 20:02:22 -05:00 committed by GitHub
parent e64b7cf698
commit 3ece21cb45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 216 additions and 20 deletions

View File

@ -49,7 +49,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
"@atproto/api": "^0.12.14",
"@atproto/api": "^0.12.16",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",

View File

@ -4,18 +4,27 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
import {
type SessionAccount,
useAgent,
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, useTheme} from '#/alf'
import {AccountList} from '#/components/AccountList'
import {Button, ButtonText} from '#/components/Button'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
const COL_WIDTH = 400
@ -30,6 +39,10 @@ export function Deactivated() {
const hasOtherAccounts = accounts.length > 1
const setMinimalShellMode = useSetMinimalShellMode()
const {logout} = useSessionApi()
const agent = useAgent()
const [pending, setPending] = React.useState(false)
const [error, setError] = React.useState<string | undefined>()
const queryClient = useQueryClient()
useFocusEffect(
React.useCallback(() => {
@ -62,6 +75,34 @@ export function Deactivated() {
logout('Deactivated')
}, [logout])
const handleActivate = React.useCallback(async () => {
try {
setPending(true)
await agent.com.atproto.server.activateAccount()
await queryClient.resetQueries()
await agent.resumeSession(agent.session!)
} catch (e: any) {
switch (e.message) {
case 'Bad token scope':
setError(
_(
msg`You're logged in with an App Password. Please log in with your main password to continue deactivating your account.`,
),
)
break
default:
setError(_(msg`Something went wrong, please try again`))
break
}
logger.error(e, {
context: 'Failed to activate account',
})
} finally {
setPending(false)
}
}, [_, agent, setPending, setError, queryClient])
return (
<View style={[a.h_full_vh, a.flex_1, t.atoms.bg]}>
<ScrollView
@ -104,10 +145,11 @@ export function Deactivated() {
size="medium"
variant="solid"
color="primary"
onPress={() => setShowLoggedOut(true)}>
onPress={handleActivate}>
<ButtonText>
<Trans>Yes, reactivate my account</Trans>
</ButtonText>
{pending && <ButtonIcon icon={Loader} position="right" />}
</Button>
<Button
label={_(msg`Cancel reactivation and log out`)}
@ -120,6 +162,21 @@ export function Deactivated() {
</ButtonText>
</Button>
</View>
{error && (
<View
style={[
a.flex_row,
a.gap_sm,
a.mt_md,
a.p_md,
a.rounded_sm,
t.atoms.bg_contrast_25,
]}>
<CircleInfo size="md" fill={t.palette.negative_400} />
<Text style={[a.flex_1, a.leading_snug]}>{error}</Text>
</View>
)}
</View>
<View style={[a.pb_3xl]}>

View File

@ -3,9 +3,14 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {logger} from '#/logger'
import {useAgent, useSessionApi} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {DialogOuterProps} from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
@ -13,12 +18,58 @@ export function DeactivateAccountDialog({
control,
}: {
control: DialogOuterProps['control']
}) {
return (
<Prompt.Outer control={control}>
<DeactivateAccountDialogInner control={control} />
</Prompt.Outer>
)
}
function DeactivateAccountDialogInner({
control,
}: {
control: DialogOuterProps['control']
}) {
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {_} = useLingui()
const agent = useAgent()
const {logout} = useSessionApi()
const [pending, setPending] = React.useState(false)
const [error, setError] = React.useState<string | undefined>()
const handleDeactivate = React.useCallback(async () => {
try {
setPending(true)
await agent.com.atproto.server.deactivateAccount({})
control.close(() => {
logout('Deactivated')
})
} catch (e: any) {
switch (e.message) {
case 'Bad token scope':
setError(
_(
msg`You're logged in with an App Password. Please log in with your main password to continue deactivating your account.`,
),
)
break
default:
setError(_(msg`Something went wrong, please try again`))
break
}
logger.error(e, {
context: 'Failed to deactivate account',
})
} finally {
setPending(false)
}
}, [agent, control, logout, _, setPending])
return (
<Prompt.Outer control={control} testID="confirmModal">
<>
<Prompt.TitleText>{_(msg`Deactivate account`)}</Prompt.TitleText>
<Prompt.DescriptionText>
<Trans>
@ -48,13 +99,32 @@ export function DeactivateAccountDialog({
<Divider />
</View>
<Prompt.Actions>
<Prompt.Action
cta={_(msg`Yes, deactivate`)}
onPress={() => {}}
<Button
variant="solid"
color="negative"
/>
size={gtMobile ? 'small' : 'medium'}
label={_(msg`Yes, deactivate`)}
onPress={handleDeactivate}>
<ButtonText>{_(msg`Yes, deactivate`)}</ButtonText>
{pending && <ButtonIcon icon={Loader} position="right" />}
</Button>
<Prompt.Cancel />
</Prompt.Actions>
</Prompt.Outer>
{error && (
<View
style={[
a.flex_row,
a.gap_sm,
a.mt_md,
a.p_md,
a.rounded_sm,
t.atoms.bg_contrast_25,
]}>
<CircleInfo size="md" fill={t.palette.negative_400} />
<Text style={[a.flex_1, a.leading_snug]}>{error}</Text>
</View>
)}
</>
)
}

View File

@ -18,9 +18,12 @@ const accountSchema = z.object({
refreshJwt: z.string().optional(), // optional because it can expire
accessJwt: z.string().optional(), // optional because it can expire
signupQueued: z.boolean().optional(),
status: z
.enum(['active', 'takendown', 'suspended', 'deactivated'])
.optional(),
active: z.boolean().optional(), // optional for backwards compat
/**
* Known values: takendown, suspended, deactivated
* @see https://github.com/bluesky-social/atproto/blob/5441fbde9ed3b22463e91481ec80cb095643e141/lexicons/com/atproto/server/getSession.json
*/
status: z.string().optional(),
pdsUrl: z.string().optional(),
})
export type PersistedAccount = z.infer<typeof accountSchema>

View File

@ -28,6 +28,7 @@ describe('session', () => {
const agent = new BskyAgent({service: 'https://alice.com'})
agent.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -50,6 +51,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -88,6 +90,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": undefined,
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -116,6 +119,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -138,6 +142,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -162,6 +167,7 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-1',
@ -186,6 +192,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@ -199,6 +206,7 @@ describe('session', () => {
},
{
"accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -223,6 +231,7 @@ describe('session', () => {
const agent3 = new BskyAgent({service: 'https://alice.com'})
agent3.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2',
@ -247,6 +256,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -260,6 +270,7 @@ describe('session', () => {
},
{
"accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@ -284,6 +295,7 @@ describe('session', () => {
const agent4 = new BskyAgent({service: 'https://jay.com'})
agent4.session = {
active: true,
did: 'jay-did',
handle: 'jay.test',
accessJwt: 'jay-access-jwt-1',
@ -306,6 +318,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "jay-access-jwt-1",
"active": true,
"did": "jay-did",
"email": undefined,
"emailAuthFactor": false,
@ -319,6 +332,7 @@ describe('session', () => {
},
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -332,6 +346,7 @@ describe('session', () => {
},
{
"accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@ -374,6 +389,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": undefined,
"active": true,
"did": "jay-did",
"email": undefined,
"emailAuthFactor": false,
@ -387,6 +403,7 @@ describe('session', () => {
},
{
"accessJwt": undefined,
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -400,6 +417,7 @@ describe('session', () => {
},
{
"accessJwt": undefined,
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@ -428,6 +446,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -459,6 +478,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": undefined,
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -483,6 +503,7 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://alice.com'})
agent2.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-2',
@ -504,6 +525,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -532,6 +554,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -576,6 +599,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -583,6 +607,7 @@ describe('session', () => {
}
const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-1',
@ -616,6 +641,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@ -653,6 +679,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -669,6 +696,7 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('alice-did')
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2',
@ -697,6 +725,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": "alice@foo.bar",
"emailAuthFactor": false,
@ -720,6 +749,7 @@ describe('session', () => {
`)
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-3',
@ -748,6 +778,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-3",
"active": true,
"did": "alice-did",
"email": "alice@foo.baz",
"emailAuthFactor": true,
@ -771,6 +802,7 @@ describe('session', () => {
`)
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-4',
@ -799,6 +831,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-4",
"active": true,
"did": "alice-did",
"email": "alice@foo.baz",
"emailAuthFactor": false,
@ -827,6 +860,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -843,6 +877,7 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('alice-did')
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2',
@ -873,6 +908,7 @@ describe('session', () => {
expect(lastState === state).toBe(true)
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-3',
@ -896,6 +932,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -904,6 +941,7 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-1',
@ -928,6 +966,7 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('bob-did')
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2',
@ -956,6 +995,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@ -969,6 +1009,7 @@ describe('session', () => {
},
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": "alice@foo.bar",
"emailAuthFactor": false,
@ -992,6 +1033,7 @@ describe('session', () => {
`)
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob-updated.test',
accessJwt: 'bob-access-jwt-2',
@ -1018,6 +1060,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "bob-access-jwt-2",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@ -1031,6 +1074,7 @@ describe('session', () => {
},
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": "alice@foo.bar",
"emailAuthFactor": false,
@ -1083,6 +1127,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -1091,6 +1136,7 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-1',
@ -1117,6 +1163,7 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('bob-did')
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-2',
@ -1142,6 +1189,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -1179,6 +1227,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -1207,6 +1256,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -1242,6 +1292,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": undefined,
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -1270,6 +1321,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -1305,6 +1357,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": undefined,
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@ -1333,6 +1386,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@ -1340,6 +1394,7 @@ describe('session', () => {
}
const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-1',
@ -1362,6 +1417,7 @@ describe('session', () => {
const anotherTabAgent1 = new BskyAgent({service: 'https://jay.com'})
anotherTabAgent1.session = {
active: true,
did: 'jay-did',
handle: 'jay.test',
accessJwt: 'jay-access-jwt-1',
@ -1369,6 +1425,7 @@ describe('session', () => {
}
const anotherTabAgent2 = new BskyAgent({service: 'https://alice.com'})
anotherTabAgent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-2',
@ -1397,6 +1454,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "jay-access-jwt-1",
"active": true,
"did": "jay-did",
"email": undefined,
"emailAuthFactor": false,
@ -1410,6 +1468,7 @@ describe('session', () => {
},
{
"accessJwt": "bob-access-jwt-2",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@ -1434,6 +1493,7 @@ describe('session', () => {
const anotherTabAgent3 = new BskyAgent({service: 'https://clarence.com'})
anotherTabAgent3.session = {
active: true,
did: 'clarence-did',
handle: 'clarence.test',
accessJwt: 'clarence-access-jwt-2',
@ -1457,6 +1517,7 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "clarence-access-jwt-2",
"active": true,
"did": "clarence-did",
"email": undefined,
"emailAuthFactor": false,

View File

@ -46,6 +46,11 @@ export async function createAgentAndResume(
emailConfirmed: storedAccount.emailConfirmed,
handle: storedAccount.handle,
refreshJwt: storedAccount.refreshJwt ?? '',
/**
* @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188
*/
active: storedAccount.active ?? true,
status: storedAccount.status,
}
if (isSessionExpired(storedAccount)) {
await networkRetry(1, () => agent.resumeSession(prevSession))
@ -235,8 +240,8 @@ export function agentToSessionAccount(
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
signupQueued: isSignupQueued(agent.session.accessJwt),
// @ts-expect-error TODO remove when backend is ready
status: agent.session.status,
active: agent.session.active,
status: agent.session.status as SessionAccount['status'],
pdsUrl: agent.pdsUrl?.toString(),
}
}

View File

@ -34,10 +34,10 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
"@atproto/api@^0.12.14":
version "0.12.14"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.14.tgz#81252fd166ec8fe950056531e690d563437720fa"
integrity sha512-ZPh/afoRjFEQDQgMZW2FQiG5CDUifY7SxBqI0zVJUwed8Zi6fqYzGYM8fcDvD8yJfflRCqRxUE72g5fKiA1zAQ==
"@atproto/api@^0.12.16":
version "0.12.16"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.16.tgz#f5b5e06d75d379dafe79521d727ed8ad5516d3fc"
integrity sha512-v3lA/m17nkawDXiqgwXyaUSzJPeXJBMH8QKOoYxcDqN+8yG9LFlGe2ecGarXcbGQjYT0GJTAAW3Y/AaCOEwuLg==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0"