phase 4: migrate account flows, trending queries, notifications, and report subjects off the bridge
This commit is contained in:
+14
-4
@@ -1,10 +1,17 @@
|
||||
import {type Insets, Platform} from 'react-native'
|
||||
import {type Service} from '@atproto/lex-client'
|
||||
import {api} from '@bsky.app/sdk'
|
||||
|
||||
import {type ProxyHeaderValue} from '#/state/session/agent'
|
||||
import {BLUESKY_PROXY_DID, IS_DEV} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
/**
|
||||
* The `atproto-proxy` header value: a DID plus a service fragment, e.g.
|
||||
* `did:web:api.bsky.app#bsky_appview`. Kept local to this module (previously
|
||||
* lived in the now-removed session `agent.ts` compat layer).
|
||||
*/
|
||||
type ProxyHeaderValue = `did:${string}:${string}#${string}`
|
||||
|
||||
export const LOCAL_DEV_SERVICE =
|
||||
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
|
||||
export const STAGING_SERVICE = 'https://staging.bsky.dev'
|
||||
@@ -251,9 +258,12 @@ export const BLUESKY_MOD_SERVICE_HEADERS = {
|
||||
'atproto-proxy': `${api.moderation.did}#atproto_labeler`,
|
||||
}
|
||||
|
||||
export const BLUESKY_NOTIF_SERVICE_HEADERS = {
|
||||
'atproto-proxy': `${BLUESKY_PROXY_DID}#bsky_notif`,
|
||||
}
|
||||
/**
|
||||
* Service proxy identifier for the notification/entryway service. Passed as the
|
||||
* per-call `service` option on the account client so lex-client emits the
|
||||
* `atproto-proxy` header (replaces the old `BLUESKY_NOTIF_SERVICE_HEADERS`).
|
||||
*/
|
||||
export const NOTIF_SERVICE = `${BLUESKY_PROXY_DID}#bsky_notif` as Service
|
||||
|
||||
export const webLinks = {
|
||||
tos: `https://bsky.social/about/support/tos`,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import {Client} from '@atproto/lex-client'
|
||||
import {describe, expect, it} from '@jest/globals'
|
||||
|
||||
import {NOTIF_SERVICE} from '#/lib/constants'
|
||||
import {app} from '#/lexicons'
|
||||
|
||||
/*
|
||||
* Proxy-emission guard for the push-notification register/unregister calls.
|
||||
*
|
||||
* registerPush/unregisterPush move from an explicit `atproto-proxy` header on
|
||||
* the old bridge agent to a per-call `service` option on the lex account
|
||||
* client (see notifications.ts). This test proves the notif service DID
|
||||
* actually reaches the wire as the `atproto-proxy` header when that option is
|
||||
* used, so a wrong proxy target cannot fail silently (design Risk #2).
|
||||
*
|
||||
* The technique mirrors clients-bundle-test.ts: build a Client over a fake
|
||||
* `fetchHandler` agent (no session/native chain), issue a real `Client.call`
|
||||
* with the same per-call `service: NOTIF_SERVICE` option the notification
|
||||
* calls use, and assert the emitted request header. Procedure request bodies
|
||||
* cannot be encoded under the jest CID interop, so the call uses a query - the
|
||||
* `service` -> `atproto-proxy` header path is shared by queries and procedures
|
||||
* alike, so this faithfully exercises what registerPush/unregisterPush emit.
|
||||
*/
|
||||
|
||||
const DID = 'did:plc:example123'
|
||||
const HANDLE = 'alice.test'
|
||||
const SERVICE_ORIGIN = 'https://bsky.social'
|
||||
|
||||
function makeCapturingClient() {
|
||||
const seen: {path: string; headers: Headers}[] = []
|
||||
const client = new Client({
|
||||
did: DID,
|
||||
fetchHandler: (path, init) => {
|
||||
seen.push({path, headers: new Headers(init.headers)})
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify({did: DID, handle: HANDLE}), {
|
||||
status: 200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
return {seen, client}
|
||||
}
|
||||
|
||||
describe('notifications proxy emission', () => {
|
||||
it('NOTIF_SERVICE targets the notif service fragment', () => {
|
||||
/* the constant is the single source of the proxy DID reaching the wire */
|
||||
expect(NOTIF_SERVICE).toMatch(/#bsky_notif$/)
|
||||
})
|
||||
|
||||
it('emits atproto-proxy: <NOTIF_SERVICE> when the per-call service option is set', async () => {
|
||||
const {seen, client} = makeCapturingClient()
|
||||
|
||||
await client
|
||||
.call(
|
||||
app.bsky.actor.getProfile,
|
||||
{actor: HANDLE},
|
||||
{service: NOTIF_SERVICE},
|
||||
)
|
||||
.catch(() => {})
|
||||
|
||||
expect(seen.length).toBe(1)
|
||||
expect(seen[0].headers.get('atproto-proxy')).toBe(NOTIF_SERVICE)
|
||||
/* the account origin is never the proxy target */
|
||||
expect(seen[0].headers.get('atproto-proxy')).not.toContain(SERVICE_ORIGIN)
|
||||
})
|
||||
})
|
||||
@@ -2,34 +2,47 @@ import {useCallback, useEffect} from 'react'
|
||||
import {Platform} from 'react-native'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications'
|
||||
import {type Client} from '@atproto/lex-client'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import {
|
||||
BLUESKY_NOTIF_SERVICE_HEADERS,
|
||||
NOTIF_SERVICE,
|
||||
PUBLIC_APPVIEW_DID,
|
||||
PUBLIC_STAGING_APPVIEW_DID,
|
||||
} from '#/lib/constants'
|
||||
import {logger as notyLogger} from '#/lib/notifications/util'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {type SessionAccount, useAgent, useSession} from '#/state/session'
|
||||
import {type SessionAgent} from '#/state/session/session-core'
|
||||
import {type SessionAccount, usePdsClient, useSession} from '#/state/session'
|
||||
import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_DEV, IS_NATIVE} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
import {app} from '#/lexicons'
|
||||
|
||||
/**
|
||||
* A resumed throwaway account client paired with the account's service origin
|
||||
* and handle. Produced by `createTemporaryClientsAndResume` (session util) and
|
||||
* consumed by {@link unregisterPushToken}, which needs the service host to pick
|
||||
* the correct appview DID and the handle for a debug log line without reaching
|
||||
* into the session internals.
|
||||
*/
|
||||
export type TemporaryPushClient = {
|
||||
client: Client
|
||||
service: string
|
||||
handle: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Registers the device's push notification token with the Bluesky server.
|
||||
*/
|
||||
async function _registerPushToken({
|
||||
agent,
|
||||
client,
|
||||
currentAccount,
|
||||
token,
|
||||
extra = {},
|
||||
}: {
|
||||
agent: SessionAgent
|
||||
client: Client
|
||||
currentAccount: SessionAccount
|
||||
token: Notifications.DevicePushToken
|
||||
extra?: {
|
||||
@@ -49,8 +62,8 @@ async function _registerPushToken({
|
||||
|
||||
notyLogger.debug(`registerPushToken: registering`, {...payload})
|
||||
|
||||
await agent.app.bsky.notification.registerPush(payload, {
|
||||
headers: BLUESKY_NOTIF_SERVICE_HEADERS,
|
||||
await client.call(app.bsky.notification.registerPush, payload, {
|
||||
service: NOTIF_SERVICE,
|
||||
})
|
||||
|
||||
notyLogger.debug(`registerPushToken: success`)
|
||||
@@ -75,7 +88,7 @@ const _registerPushTokenDebounced = debounce(_registerPushToken, 100)
|
||||
* `_registerPushTokenDebounced` directly.
|
||||
*/
|
||||
export function useRegisterPushToken() {
|
||||
const agent = useAgent()
|
||||
const client = usePdsClient()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
return useCallback(
|
||||
@@ -88,7 +101,7 @@ export function useRegisterPushToken() {
|
||||
}) => {
|
||||
if (!currentAccount) return
|
||||
return _registerPushTokenDebounced({
|
||||
agent,
|
||||
client,
|
||||
currentAccount,
|
||||
token,
|
||||
extra: {
|
||||
@@ -96,7 +109,7 @@ export function useRegisterPushToken() {
|
||||
},
|
||||
})
|
||||
},
|
||||
[agent, currentAccount],
|
||||
[client, currentAccount],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -327,16 +340,17 @@ export async function resetBadgeCount() {
|
||||
await setBadgeCountAsync(0)
|
||||
}
|
||||
|
||||
export async function unregisterPushToken(agents: SessionAgent[]) {
|
||||
export async function unregisterPushToken(clients: TemporaryPushClient[]) {
|
||||
if (!IS_NATIVE) return
|
||||
|
||||
try {
|
||||
const token = await getPushToken()
|
||||
if (token) {
|
||||
for (const agent of agents) {
|
||||
await agent.app.bsky.notification.unregisterPush(
|
||||
for (const {client, service, handle} of clients) {
|
||||
await client.call(
|
||||
app.bsky.notification.unregisterPush,
|
||||
{
|
||||
serviceDid: agent.serviceUrl.hostname.includes('staging')
|
||||
serviceDid: service.includes('staging')
|
||||
? PUBLIC_STAGING_APPVIEW_DID
|
||||
: PUBLIC_APPVIEW_DID,
|
||||
platform: Platform.OS,
|
||||
@@ -344,10 +358,10 @@ export async function unregisterPushToken(agents: SessionAgent[]) {
|
||||
appId: 'xyz.blueskyweb.app',
|
||||
},
|
||||
{
|
||||
headers: BLUESKY_NOTIF_SERVICE_HEADERS,
|
||||
service: NOTIF_SERVICE,
|
||||
},
|
||||
)
|
||||
notyLogger.debug(`Push token unregistered for ${agent.session?.handle}`)
|
||||
notyLogger.debug(`Push token unregistered for ${handle}`)
|
||||
}
|
||||
} else {
|
||||
notyLogger.debug('Tried to unregister push token, but could not find one')
|
||||
|
||||
Reference in New Issue
Block a user