From f73e0c1d450ce84dfde7f7c50b555ae1d71ddf4a Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Mon, 15 Jun 2026 16:16:33 -0700 Subject: [PATCH 1/2] feat(mobile): ping last_active_at on app foreground Adds a pingActivity backend function and useActivityPing hook that fires POST /v1/users/me/ping each time the mobile app enters the foreground, recording the user's last app-open for inactivity notifications. Co-Authored-By: Claude Opus 4.6 --- .../services/audius-backend/AudiusBackend.ts | 26 +++++++++++++++++++ packages/mobile/src/hooks/useActivityPing.ts | 26 +++++++++++++++++++ .../src/screens/root-screen/RootScreen.tsx | 3 +++ 3 files changed, 55 insertions(+) create mode 100644 packages/mobile/src/hooks/useActivityPing.ts diff --git a/packages/common/src/services/audius-backend/AudiusBackend.ts b/packages/common/src/services/audius-backend/AudiusBackend.ts index ebb2806f34d..ba67b01f297 100644 --- a/packages/common/src/services/audius-backend/AudiusBackend.ts +++ b/packages/common/src/services/audius-backend/AudiusBackend.ts @@ -392,6 +392,31 @@ export const audiusBackend = ({ } } + async function pingActivity({ + sdk, + userId + }: { + sdk: AudiusSdkWithServices + userId: number + }) { + try { + const { data, signature } = await signAPIRequest({ sdk }) + if (!signature) return + const encodedUserId = encodeHashId(userId) + if (!encodedUserId) return + const base = env.API_URL.replace(/\/$/, '') + await fetch(`${base}/v1/users/me/ping?user_id=${encodedUserId}`, { + method: 'POST', + headers: { + [AuthHeaders.Message]: data, + [AuthHeaders.Signature]: signature + } + }) + } catch { + // Fire-and-forget + } + } + async function signAPIRequest({ sdk, input @@ -1181,6 +1206,7 @@ export const audiusBackend = ({ getSignature, getWAudioBalance, identityServiceUrl, + pingActivity, registerDeviceToken, reportNotificationCampaignPushOpen, sendTokens, diff --git a/packages/mobile/src/hooks/useActivityPing.ts b/packages/mobile/src/hooks/useActivityPing.ts new file mode 100644 index 00000000000..00d472f20cd --- /dev/null +++ b/packages/mobile/src/hooks/useActivityPing.ts @@ -0,0 +1,26 @@ +import { useCallback } from 'react' + +import { useCurrentUserId, useQueryContext } from '@audius/common/api' + +import { useEnterForeground } from 'app/hooks/useAppState' +import { audiusBackendInstance } from 'app/services/audius-backend-instance' + +export const useActivityPing = () => { + const { data: currentUserId } = useCurrentUserId() + const { audiusSdk } = useQueryContext() + + useEnterForeground( + useCallback(async () => { + if (!currentUserId) return + try { + const sdk = await audiusSdk() + await audiusBackendInstance.pingActivity({ + sdk, + userId: currentUserId + }) + } catch { + // Fire-and-forget + } + }, [currentUserId, audiusSdk]) + ) +} diff --git a/packages/mobile/src/screens/root-screen/RootScreen.tsx b/packages/mobile/src/screens/root-screen/RootScreen.tsx index 09758022b72..342e7e61de5 100644 --- a/packages/mobile/src/screens/root-screen/RootScreen.tsx +++ b/packages/mobile/src/screens/root-screen/RootScreen.tsx @@ -40,6 +40,8 @@ import { OAuthScreen } from '../oauth-screen/OAuthScreen' import { ResetPasswordModalScreen } from '../reset-password-screen' import { SignOnStack } from '../sign-on-screen' +import { useActivityPing } from 'app/hooks/useActivityPing' + import { StatusBar } from './StatusBar' import { useResetNotificationBadgeCount } from './useResetNotificationBadgeCount' @@ -97,6 +99,7 @@ export const RootScreen = () => { ) useResetNotificationBadgeCount() + useActivityPing() // Reset the player on first mount so a crash doesn't leak previous playback // state into the next session. PAY-1412. From 9d04485ac828910270d32e9aa8aa8fa2ea225a03 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Tue, 16 Jun 2026 11:56:56 -0700 Subject: [PATCH 2/2] fix(mobile): also ping last_active_at on initial app open AppState.addEventListener('change') only fires on state transitions. On first launch the app starts in 'active' with no transition, so the ping was never sent until the user backgrounded and re-opened the app. Co-Authored-By: Claude Opus 4.6 --- packages/mobile/src/hooks/useActivityPing.ts | 36 ++++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/mobile/src/hooks/useActivityPing.ts b/packages/mobile/src/hooks/useActivityPing.ts index 00d472f20cd..374c4890276 100644 --- a/packages/mobile/src/hooks/useActivityPing.ts +++ b/packages/mobile/src/hooks/useActivityPing.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useEffect } from 'react' import { useCurrentUserId, useQueryContext } from '@audius/common/api' @@ -9,18 +9,24 @@ export const useActivityPing = () => { const { data: currentUserId } = useCurrentUserId() const { audiusSdk } = useQueryContext() - useEnterForeground( - useCallback(async () => { - if (!currentUserId) return - try { - const sdk = await audiusSdk() - await audiusBackendInstance.pingActivity({ - sdk, - userId: currentUserId - }) - } catch { - // Fire-and-forget - } - }, [currentUserId, audiusSdk]) - ) + const pingActivity = useCallback(async () => { + if (!currentUserId) return + try { + const sdk = await audiusSdk() + await audiusBackendInstance.pingActivity({ + sdk, + userId: currentUserId + }) + } catch { + // Fire-and-forget + } + }, [currentUserId, audiusSdk]) + + // Ping on initial app open (AppState starts as 'active' with no transition) + useEffect(() => { + pingActivity() + }, [pingActivity]) + + // Ping on subsequent foreground transitions + useEnterForeground(pingActivity) }