diff --git a/android-app/app/src/main/java/de/harheimertc/data/ApiService.kt b/android-app/app/src/main/java/de/harheimertc/data/ApiService.kt index e5f82a3..690fd28 100755 --- a/android-app/app/src/main/java/de/harheimertc/data/ApiService.kt +++ b/android-app/app/src/main/java/de/harheimertc/data/ApiService.kt @@ -283,6 +283,7 @@ data class PushTokenRequest( val token: String, val platform: String = "android", val appVersion: String? = null, + val installationId: String? = null, ) data class BirthdayDto( val name: String = "", diff --git a/android-app/app/src/main/java/de/harheimertc/notifications/HarheimerMessagingService.kt b/android-app/app/src/main/java/de/harheimertc/notifications/HarheimerMessagingService.kt index a4efcde..effa997 100755 --- a/android-app/app/src/main/java/de/harheimertc/notifications/HarheimerMessagingService.kt +++ b/android-app/app/src/main/java/de/harheimertc/notifications/HarheimerMessagingService.kt @@ -21,7 +21,7 @@ class HarheimerMessagingService : FirebaseMessagingService() { override fun onNewToken(token: String) { super.onNewToken(token) serviceScope.launch { - pushTokenRepository.registerToken(token) + pushTokenRepository.registerCurrentDevice() } } diff --git a/android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt b/android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt index 4cecdf3..f743f2a 100755 --- a/android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt +++ b/android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt @@ -1,6 +1,7 @@ package de.harheimertc.repositories import android.util.Log +import com.google.firebase.installations.FirebaseInstallations import com.google.firebase.messaging.FirebaseMessaging import de.harheimertc.BuildConfig import de.harheimertc.data.ApiService @@ -15,16 +16,18 @@ class PushTokenRepository @Inject constructor( ) { suspend fun registerCurrentDevice(): Result = runCatching { val token = FirebaseMessaging.getInstance().token.await() - registerToken(token).getOrThrow() + val installationId = FirebaseInstallations.getInstance().id.await() + registerToken(token, installationId).getOrThrow() } - suspend fun registerToken(token: String): Result = runCatching { + suspend fun registerToken(token: String, installationId: String? = null): Result = runCatching { if (token.isBlank()) return@runCatching retryOnNetworkFailure { val response = api.registerPushToken( PushTokenRequest( token = token, appVersion = "${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}", + installationId = installationId, ), ) if (!response.isSuccessful) { diff --git a/server/api/profile/push-token.post.js b/server/api/profile/push-token.post.js index fe8c57d..53613dc 100755 --- a/server/api/profile/push-token.post.js +++ b/server/api/profile/push-token.post.js @@ -22,7 +22,8 @@ export default defineEventHandler(async (event) => { upsertPushToken(users[userIndex], { token: body.token, platform: body.platform || 'android', - appVersion: body.appVersion || null + appVersion: body.appVersion || null, + installationId: body.installationId || null }) await writeUsers(users) return { success: true, message: 'Push-Token gespeichert.' } diff --git a/server/utils/push-notifications.js b/server/utils/push-notifications.js index f2d946e..56137c7 100755 --- a/server/utils/push-notifications.js +++ b/server/utils/push-notifications.js @@ -85,16 +85,21 @@ function pushTokensForUser(user) { : [] } -export function upsertPushToken(user, { token, platform = 'android', appVersion = null }) { +export function upsertPushToken(user, { token, platform = 'android', appVersion = null, installationId = null }) { const normalizedToken = String(token || '').trim() if (!normalizedToken) return user + const normalizedInstallationId = String(installationId || '').trim().slice(0, 200) || null const now = new Date().toISOString() const tokens = Array.isArray(user.pushTokens) ? user.pushTokens : [] - const next = tokens.filter(entry => entry?.token !== normalizedToken) + // A Firebase token can rotate for the same app installation. Retain tokens + // from other devices, but replace the previous token of this installation. + const next = tokens.filter(entry => entry?.token !== normalizedToken && + (!normalizedInstallationId || entry?.installationId !== normalizedInstallationId)) next.push({ token: normalizedToken, platform: String(platform || 'android').slice(0, 30), appVersion: appVersion ? String(appVersion).slice(0, 80) : null, + installationId: normalizedInstallationId, updatedAt: now, createdAt: tokens.find(entry => entry?.token === normalizedToken)?.createdAt || now }) diff --git a/tests/config-profile-endpoints.spec.ts b/tests/config-profile-endpoints.spec.ts index 3d3e9bb..d0d2446 100755 --- a/tests/config-profile-endpoints.spec.ts +++ b/tests/config-profile-endpoints.spec.ts @@ -247,7 +247,7 @@ describe('Config & Profil Endpoints', () => { it('speichert Android-Push-Token am Benutzer', async () => { const event = createEvent({ headers: { authorization: 'Bearer android-token' } }) - mockSuccessReadBody({ token: 'fcm-token', platform: 'android', appVersion: '1.0+1' }) + mockSuccessReadBody({ token: 'fcm-token', platform: 'android', appVersion: '1.0+1', installationId: 'firebase-installation-id' }) const users = [{ id: '1', email: 'max@test.de', roles: ['mitglied'] }] authUtils.verifyToken.mockReturnValue({ id: '1' }) authUtils.getUserFromToken.mockResolvedValue(users[0]) @@ -260,7 +260,7 @@ describe('Config & Profil Endpoints', () => { expect(authUtils.writeUsers).toHaveBeenCalledWith([ expect.objectContaining({ id: '1', - pushTokens: [expect.objectContaining({ token: 'fcm-token', platform: 'android', appVersion: '1.0+1' })] + pushTokens: [expect.objectContaining({ token: 'fcm-token', platform: 'android', appVersion: '1.0+1', installationId: 'firebase-installation-id' })] }) ]) })