Align handling across native devices

This commit is contained in:
Eric Bailey
2025-06-24 16:44:41 -05:00
parent 1b12e17bbe
commit d54907eca0
+69 -42
View File
@@ -6,17 +6,22 @@ import {type AtpAgent} from '@atproto/api'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import {Logger} from '#/logger' import {Logger} from '#/logger'
import {isAndroid, isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {type SessionAccount, useAgent, useSession} from '#/state/session' import {type SessionAccount, useAgent, useSession} from '#/state/session'
import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler' import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler'
const logger = Logger.create(Logger.Context.Notifications) const logger = Logger.create(Logger.Context.Notifications)
async function registerPushToken( /**
* Registers the device's push notification token with the Bluesky server.
*/
async function _registerPushToken(
agent: AtpAgent, agent: AtpAgent,
account: SessionAccount, account: SessionAccount,
token: Notifications.DevicePushToken, token: Notifications.DevicePushToken,
) { ) {
logger.debug(`registerPushToken`)
try { try {
await agent.app.bsky.notification.registerPush({ await agent.app.bsky.notification.registerPush({
serviceDid: account.service?.includes('staging') serviceDid: account.service?.includes('staging')
@@ -31,7 +36,7 @@ async function registerPushToken(
appId: 'xyz.blueskyweb.app', appId: 'xyz.blueskyweb.app',
}) })
logger.debug(`registerPushToken`, { logger.debug(`registerPushToken: success`, {
tokenType: token.type, tokenType: token.type,
token: token.data, token: token.data,
}) })
@@ -40,16 +45,64 @@ async function registerPushToken(
} }
} }
const registerPushTokenDebounced = debounce(registerPushToken, 100) /**
* Debounced version of `_registerPushToken` to prevent multiple calls. Use this
* instead of using `_registerPushToken` directly.
*/
const registerPushToken = debounce(_registerPushToken, 100)
async function getPushToken(skipPermissionCheck = false) { /**
const granted = * Retreive the device's push notification token, if permissions are granted.
skipPermissionCheck || (await Notifications.getPermissionsAsync()).granted */
async function getPushToken() {
const granted = (await Notifications.getPermissionsAsync()).granted
if (granted) { if (granted) {
return Notifications.getDevicePushTokenAsync() return Notifications.getDevicePushTokenAsync()
} }
} }
/**
* Gets the device push token and registers it with the Bluesky server.
*
* N.B. A previous regression in `expo-notifications` caused
* `addPushTokenListener` to not fire on Android after calling
* `getPushToken()`. Therefore, as insurance, we also call
* `registerPushToken` here.
*
* Because `registerPushToken` is debounced, we can safely call it on every
* platform and only a single call will be made to the server.
*
* @see https://github.com/bluesky-social/social-app/pull/4467
* @see https://github.com/expo/expo/issues/28656
* @see https://github.com/expo/expo/issues/29909
*/
async function getAndRegisterPushToken(
agent: AtpAgent,
currentAccount: SessionAccount,
) {
try {
/**
* This will also fire the listener added via `addPushTokenListener`. That
* listener also handles registration.
*/
const token = await getPushToken()
logger.debug(`getAndRegisterPushToken`, {token})
if (token) {
/**
* The listener should have registered the token already, but just in
* case, call the debounced function again.
*/
registerPushToken(agent, currentAccount, token)
}
return token
} catch (e: any) {
logger.error(`getPushToken: failed`, {safeMessage: e.message})
}
}
export function useNotificationsRegistration() { export function useNotificationsRegistration() {
const agent = useAgent() const agent = useAgent()
const {currentAccount} = useSession() const {currentAccount} = useSession()
@@ -57,26 +110,10 @@ export function useNotificationsRegistration() {
React.useEffect(() => { React.useEffect(() => {
if (!currentAccount) return if (!currentAccount) return
/** logger.debug(`useNotificationsRegistration`)
* HACK — An apparent regression in expo-notifications causes
* `addPushTokenListener` to not fire on Android whenever the token changes
* by calling `getPushToken()`. This is a workaround to ensure we register
* the token once it is generated on Android.
*
* @see https://github.com/bluesky-social/social-app/pull/4467
*/
if (isAndroid) {
;(async () => {
const token = await getPushToken()
// Token will be undefined if we don't have notifications permission // init push token
if (token) { getAndRegisterPushToken(agent, currentAccount)
registerPushTokenDebounced(agent, currentAccount, token)
}
})()
} else {
getPushToken()
}
/** /**
* According to the Expo docs, there is a chance that the token will change * According to the Expo docs, there is a chance that the token will change
@@ -86,7 +123,7 @@ export function useNotificationsRegistration() {
* @see https://docs.expo.dev/versions/latest/sdk/notifications/#addpushtokenlistenerlistener * @see https://docs.expo.dev/versions/latest/sdk/notifications/#addpushtokenlistenerlistener
*/ */
const subscription = Notifications.addPushTokenListener(async newToken => { const subscription = Notifications.addPushTokenListener(async newToken => {
registerPushTokenDebounced(agent, currentAccount, newToken) registerPushToken(agent, currentAccount, newToken)
logger.debug(`addPushTokenListener callback`, {newToken}) logger.debug(`addPushTokenListener callback`, {newToken})
}) })
@@ -120,22 +157,14 @@ export function useRequestNotificationsPermission() {
} }
const res = await Notifications.requestPermissionsAsync() const res = await Notifications.requestPermissionsAsync()
logger.metric(`notifications:request`, { logger.metric(`notifications:request`, {
context: context, context: context,
status: res.status, status: res.status,
}) })
if (res.granted) {
/*
* This will fire a pushTokenEvent, which will handle registration of the
* token
*/
const token = await getPushToken(true)
/** /**
* Same hack as above. We cannot rely on the `addPushTokenListener` to * Note that this will run:
* fire on Android due to an Expo bug, so we will manually register it
* here. Note that this will occur only:
* *
* 1) Right after the user signs in, leading to no `currentAccount` * 1) Right after the user signs in, leading to no `currentAccount`
* account being available - this will be instead picked up from the * account being available - this will be instead picked up from the
@@ -148,12 +177,10 @@ export function useRequestNotificationsPermission() {
* registration will succeed. We should remove this once * registration will succeed. We should remove this once
* expo-notifications (and possibly FCMv1) is fixed and the * expo-notifications (and possibly FCMv1) is fixed and the
* `addPushTokenListener` is working again. * `addPushTokenListener` is working again.
*
* @see https://github.com/expo/expo/issues/28656
*/ */
if (isAndroid && currentAccount && token) { if (res.granted && currentAccount) {
registerPushTokenDebounced(agent, currentAccount, token) logger.debug(`useRequestNotificationsPermission`, {context})
} getAndRegisterPushToken(agent, currentAccount)
} }
} }
} }