Skip to content

Commit 8eba94d

Browse files
committed
Implement token refresh functionality
1 parent ca2ba64 commit 8eba94d

5 files changed

Lines changed: 167 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,37 @@
11
# Changelog
2+
3+
# Unreleased
4+
5+
## Fixed
6+
### Android
7+
* **Fixed FCM token refresh not being handled** - Added missing `onNewToken()` handler to `FcmInstanceIdListenerService`. Previously, when FCM automatically refreshed a device's push token (which can happen after extended inactivity, security updates, or OS updates), the app would not be notified of the new token. This caused push notifications to silently stop working for affected users because the backend was still sending to the old, invalid token.
8+
9+
## Added
10+
### Android
11+
* **Added `isRefresh` property to token registration event** - The `Registered` event now includes an `isRefresh` boolean property (Android only) that indicates whether the token was received due to an FCM-initiated refresh (`true`) or from app initialization/manual refresh (`false`). This allows your backend to distinguish between initial registrations and token refreshes for analytics or debugging purposes.
12+
13+
**Important for client apps:** Ensure you register the `remoteNotificationsRegistered` event listener early in your app lifecycle (ideally in your root component constructor or `useEffect`). This listener will now be called whenever FCM refreshes the token, not just on initial registration. Your app should sync the new token to your backend each time this event fires.
14+
15+
```jsx
16+
// In your root component
17+
useEffect(() => {
18+
const subscription = Notifications.events().registerRemoteNotificationsRegistered((event) => {
19+
console.log("Device Token Received", event.deviceToken);
20+
21+
if (event.isRefresh) {
22+
console.log("This is an FCM-initiated token refresh - old token is now invalid");
23+
}
24+
25+
// Always sync the token to your backend
26+
syncTokenToBackend(event.deviceToken, { isRefresh: event.isRefresh });
27+
});
28+
29+
Notifications.registerRemoteNotifications();
30+
31+
return () => subscription.remove();
32+
}, []);
33+
```
34+
235
# 2.1.0
336
## Added
437
* react-native 0.60 Support

lib/android/app/src/main/java/com/wix/reactnativenotifications/fcm/FcmInstanceIdListenerService.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package com.wix.reactnativenotifications.fcm;
22

3+
import android.content.Context;
4+
import android.content.Intent;
35
import android.os.Bundle;
46
import android.util.Log;
57

8+
import androidx.annotation.NonNull;
9+
610
import com.google.firebase.messaging.FirebaseMessagingService;
711
import com.google.firebase.messaging.RemoteMessage;
812
import com.wix.reactnativenotifications.BuildConfig;
@@ -22,6 +26,31 @@
2226
public class FcmInstanceIdListenerService extends FirebaseMessagingService {
2327
private final IntercomPushClient intercomPushClient = new IntercomPushClient();
2428

29+
/**
30+
* Called when FCM refreshes the registration token. This can happen when:
31+
* - The app is restored on a new device
32+
* - The user uninstalls/reinstalls the app
33+
* - The user clears app data
34+
* - FCM determines that the token needs to be refreshed
35+
*
36+
* When this happens, we need to fetch the new token and notify the JS side
37+
* so the app can sync the new token with its backend server.
38+
*
39+
* @param token The new FCM registration token
40+
*/
41+
@Override
42+
public void onNewToken(@NonNull String token) {
43+
super.onNewToken(token);
44+
if (BuildConfig.DEBUG) Log.d(LOGTAG, "FCM token refreshed by Firebase: " + token);
45+
46+
// Trigger the token refresh handler to notify JS and any app-level listeners
47+
final Context appContext = getApplicationContext();
48+
final Intent tokenFetchIntent = new Intent(appContext, FcmInstanceIdRefreshHandlerService.class);
49+
// Don't set EXTRA_IS_APP_INIT or EXTRA_MANUAL_REFRESH - this uses the default path
50+
// which calls onNewTokenReady() to handle the automatic token refresh
51+
FcmInstanceIdRefreshHandlerService.enqueueWork(appContext, tokenFetchIntent);
52+
}
53+
2554
@Override
2655
public void onMessageReceived(RemoteMessage message){
2756
Bundle bundle = message.toIntent().getExtras();

lib/android/app/src/main/java/com/wix/reactnativenotifications/fcm/FcmToken.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public class FcmToken implements IFcmToken {
2020
final protected JsIOHelper mJsIOHelper;
2121

2222
protected static String sToken;
23+
24+
// Tracks whether the current token event is from an FCM-initiated refresh
25+
// (as opposed to app init or manual refresh)
26+
protected boolean mIsTokenRefresh = false;
2327

2428
protected FcmToken(Context appContext) {
2529
if (!(appContext instanceof ReactApplication)) {
@@ -40,13 +44,17 @@ public static IFcmToken get(Context context) {
4044
@Override
4145
public void onNewTokenReady() {
4246
synchronized (mAppContext) {
47+
// This is called when FCM has refreshed the token automatically
48+
mIsTokenRefresh = true;
4349
refreshToken();
4450
}
4551
}
4652

4753
@Override
4854
public void onManualRefresh() {
4955
synchronized (mAppContext) {
56+
// Manual refresh is user-initiated, not an FCM-triggered refresh
57+
mIsTokenRefresh = false;
5058
if (sToken == null) {
5159
if(BuildConfig.DEBUG) Log.i(LOGTAG, "Manual token refresh => asking for new token");
5260
refreshToken();
@@ -60,6 +68,8 @@ public void onManualRefresh() {
6068
@Override
6169
public void onAppReady() {
6270
synchronized (mAppContext) {
71+
// App initialization is not considered a refresh
72+
mIsTokenRefresh = false;
6373
if (sToken == null) {
6474
if(BuildConfig.DEBUG) Log.i(LOGTAG, "App initialized => asking for new token");
6575
refreshToken();
@@ -99,8 +109,13 @@ protected void sendTokenToJS() {
99109
if (reactContext != null && reactContext.hasActiveReactInstance()) {
100110
Bundle tokenMap = new Bundle();
101111
tokenMap.putString("deviceToken", sToken);
112+
// Indicates whether this token was received due to an FCM-initiated refresh
113+
// (true) or from app initialization/manual refresh (false)
114+
tokenMap.putBoolean("isRefresh", mIsTokenRefresh);
102115
mJsIOHelper.sendEventToJS(TOKEN_RECEIVED_EVENT_NAME, tokenMap, reactContext);
103116
}
117+
// Reset the refresh flag after sending
118+
mIsTokenRefresh = false;
104119
}
105120

106121
}

lib/src/interfaces/NotificationEvents.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ import { NotificationActionResponse } from './NotificationActionResponse';
33

44
export interface Registered {
55
deviceToken: string;
6+
/**
7+
* Indicates whether this token was received due to an FCM-initiated refresh.
8+
*
9+
* - `true`: FCM automatically refreshed the token (e.g., due to security rotation,
10+
* extended inactivity, or other FCM-internal reasons). The old token is now invalid.
11+
* - `false`: Token received during app initialization or manual refresh request.
12+
*
13+
* **Important**: Always sync the token to your backend regardless of this value,
14+
* but you may want to log or handle refresh events differently for debugging purposes.
15+
*
16+
* Note: This property is only set on Android. On iOS, it will always be `undefined`.
17+
*/
18+
isRefresh?: boolean;
619
}
720

821
export interface RegistrationError {

website/docs/docs/subscription.md

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ This section is about the first part of the flow.
1010

1111
In order to handle notifications, you must register the `remoteNotificationsRegistered` event beforehand.
1212

13+
## Important: Token Refresh Handling
14+
15+
:::caution Critical for Android
16+
FCM (Firebase Cloud Messaging) can refresh the device token at any time, not just during initial registration. This happens when:
17+
- The app is restored on a new device
18+
- The user reinstalls the app
19+
- The user clears app data
20+
- FCM determines the token needs to be refreshed (e.g., after extended inactivity)
21+
- Security-related token rotation
22+
23+
**Your app must always sync the token to your backend whenever `remoteNotificationsRegistered` fires**, not just the first time. Failure to do so will cause push notifications to silently stop working for affected users.
24+
:::
1325

1426
In your React Native app:
1527

@@ -22,15 +34,78 @@ class App extends Component {
2234
Notifications.registerRemoteNotifications();
2335

2436
Notifications.events().registerRemoteNotificationsRegistered((event: Registered) => {
25-
// TODO: Send the token to my server so it could send back push notifications...
26-
console.log("Device Token Received", event.deviceToken);
37+
// IMPORTANT: Always sync the token to your backend!
38+
// This event fires on initial registration AND on token refresh.
39+
console.log("Device Token Received/Refreshed", event.deviceToken);
40+
this.syncTokenToBackend(event.deviceToken);
2741
});
2842
Notifications.events().registerRemoteNotificationsRegistrationFailed((event: RegistrationError) => {
2943
console.error(event);
3044
});
3145
}
46+
47+
async syncTokenToBackend(token: string) {
48+
// POST the token to your server every time - even if you think it hasn't changed
49+
await fetch('https://your-server.com/api/push-token', {
50+
method: 'POST',
51+
headers: { 'Content-Type': 'application/json' },
52+
body: JSON.stringify({ token }),
53+
});
54+
}
55+
}
56+
57+
```
58+
59+
### React Hooks Example
60+
61+
```jsx
62+
import { useEffect } from 'react';
63+
import { Notifications } from 'react-native-notifications';
64+
65+
function App() {
66+
useEffect(() => {
67+
// Set up the listener BEFORE calling registerRemoteNotifications
68+
const subscription = Notifications.events().registerRemoteNotificationsRegistered((event) => {
69+
console.log("Device Token Received", event.deviceToken);
70+
71+
// On Android, you can check if this is an FCM-initiated refresh
72+
if (event.isRefresh) {
73+
console.log("Token was refreshed by FCM - old token is now invalid");
74+
}
75+
76+
// Always sync the token to your backend
77+
syncTokenToBackend(event.deviceToken, { isRefresh: event.isRefresh });
78+
});
79+
80+
const errorSubscription = Notifications.events().registerRemoteNotificationsRegistrationFailed((event) => {
81+
console.error("Registration failed", event);
82+
});
83+
84+
Notifications.registerRemoteNotifications();
85+
86+
return () => {
87+
subscription.remove();
88+
errorSubscription.remove();
89+
};
90+
}, []);
91+
92+
// ...
3293
}
94+
```
95+
96+
### The `isRefresh` Property (Android Only)
3397

98+
On Android, the `Registered` event includes an `isRefresh` boolean property:
99+
100+
- `true`: The token was refreshed by FCM automatically. This means the previous token is now **invalid** and you must update your backend immediately.
101+
- `false`: The token was received during app initialization or a manual refresh request.
102+
- `undefined`: On iOS, this property is not set.
103+
104+
```typescript
105+
interface Registered {
106+
deviceToken: string;
107+
isRefresh?: boolean; // Android only
108+
}
34109
```
35110

36111
When you have the device token, POST it to your server and register the device in your notifications provider (Amazon SNS, Azure, etc.).

0 commit comments

Comments
 (0)