From 36103a5a366727ee5ff2bedd96b1b79833209698 Mon Sep 17 00:00:00 2001 From: Gary Tokman Date: Sat, 20 Apr 2024 12:58:21 -0400 Subject: [PATCH] fix: finishes to android --- README.md | 105 ++++++++++++------ .../push/FirebaseMessagingService.kt | 6 + .../candlefinance/push/NotificationUtils.kt | 21 +++- .../java/com/candlefinance/push/PushModule.kt | 42 ++----- 4 files changed, 109 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 4ad1504..8c411cd 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ Android support is coming soon. Check out [#1](https://github.com/candlefinance/ 1. You'll need to update your `AppDelegate.swift` to handle push check the example app [here](./example/ios/AppDelegate.swift) for an example. 2. If your AppDelegate is in Objective-C (`.mm|.m|.h`), create a new `AppDelegate.swift` file and bridging header, then delete the Objective-C AppDelegate and main.m file. Finally, copy the contents of the example app's [AppDelegate.swift](./example/ios/AppDelegate.swift) and [bridge header](./example/ios/PushExample-Bridging-Header.h) to your project. 3. Make sure you're on `iOS 15` or later. +4. You can also use Objective-C just add bridging header and import the module. +5. `UNUserNotificationCenterDelegate` set in AppDelegate. ### Android @@ -54,8 +56,8 @@ Android support is coming soon. Check out [#1](https://github.com/candlefinance/ - [x] Register for FCM token - [x] Remote push notifications - [x] Foreground - - [ ] Background - - [ ] Opened by tapping on the notification + - [x] Background + Headless JS + - [x] Opened by tapping on the notification - [ ] Local push notifications #### Setup @@ -69,46 +71,61 @@ Android support is coming soon. Check out [#1](https://github.com/candlefinance/ The following code is used to handle push notifications on the React Native side: ```js -import Push from '@candlefinance/push'; - -// Init -const push = useMemo(() => new Push(), []) +import type { PushNotificationPermissionStatus } from '@candlefinance/push'; +import { module as Push } from '@candlefinance/push'; // Shows dialog to request permission to send push notifications, gets APNS token const isGranted = await push.requestPermissions(); // Get the APNS token w/o showing permission, useful if you want silent push notifications -await push.registerForToken(); +push.registerForToken(); // Check permission status: 'granted', 'denied', or 'notDetermined' const status = await push.getAuthorizationStatus(); -// Check if APNS token is registered -const isRegistered = await push.isRegisteredForRemoteNotifications(); - // Listeners -push.addListener('notificationReceived', (data) => { - switch (data.kind) { - case 'opened': - console.log('opened'); - break; - case 'foreground': - case 'background': - console.log('foreground/background'); - const { uuid } = data; - await push.onFinish(uuid); - break; - } - const { title, body } = data; -}); - -push.addListener('deviceTokenReceived', (token) => {}); -push.addListener('errorReceived', (error) => {}); - -// Remove listeners -push.removeListener('notificationReceived'); -push.removeListener('deviceTokenReceived'); -push.removeListener('errorReceived'); +React.useEffect(() => { + const { NativeEvent, NativeHeadlessTaskKey } = Push.getConstants(); + console.log(NativeEvent, NativeHeadlessTaskKey); + Push.addTokenEventListener(NativeEvent.TOKEN_RECEIVED, (token) => { + console.log('TOKEN_RECEIVED:', token); + }); + Push.addMessageEventListener( + NativeEvent.BACKGROUND_MESSAGE_RECEIVED, + (message, id) => { + console.log('BACKGROUND_MESSAGE_RECEIVED:', message); + if (id !== undefined) { + console.log('Completing notification:', id); + Push.completeNotification(id); + } + } + ); + Push.addErrorListener(NativeEvent.FAILED_TO_REGISTER, (message) => { + console.log('FAILED_TO_REGISTER:', message); + }); + Push.addMessageEventListener(NativeEvent.NOTIFICATION_OPENED, (message) => { + console.log('NOTIFICATION_OPENED:', message); + }); + Push.addMessageEventListener( + NativeEvent.FOREGROUND_MESSAGE_RECEIVED, + (message) => { + console.log('FOREGROUND_MESSAGE_RECEIVED:', message); + } + ); + Push.addMessageEventListener( + NativeEvent.LAUNCH_NOTIFICATION_OPENED, + (message) => { + console.log('LAUNCH_NOTIFICATION_OPENED:', message); + } + ); + return () => { + Push.removeListeners(NativeEvent.TOKEN_RECEIVED); + Push.removeListeners(NativeEvent.BACKGROUND_MESSAGE_RECEIVED); + Push.removeListeners(NativeEvent.NOTIFICATION_OPENED); + Push.removeListeners(NativeEvent.FOREGROUND_MESSAGE_RECEIVED); + Push.removeListeners(NativeEvent.LAUNCH_NOTIFICATION_OPENED); + }; +}, []); ``` ## Testing @@ -123,6 +140,30 @@ This will use the [payload.json](./example/payload.json) file to send a push not Apple also has a new [console](https://developer.apple.com/notifications/push-notifications-console/) to test push notifications. If you print out the token from `deviceTokenReceived` listener, you can use it to send a push notification from the console. +## SNS + +If you're using AWS SNS, you can use the following code to send a push notification + +``` + const message = // apns + os === 'ios' ? JSON.stringify({ APNS: JSON.stringify(payload) }) + : // fcm + JSON.stringify({ + GCM: JSON.stringify({ + data: { + title: title, + body: body, + custom: customData, + data: customData, + priority: '1', + imageUrl: + 'https://logo.png', + targetClass: 'com.yourapp.candle.MainActivity', + }, + }) + }) +``` + ## Contributing We are open to contributions. Please read our [Contributing Guide](CONTRIBUTING.md) for more information. diff --git a/android/src/main/java/com/candlefinance/push/FirebaseMessagingService.kt b/android/src/main/java/com/candlefinance/push/FirebaseMessagingService.kt index a7f4cfb..ffc676a 100644 --- a/android/src/main/java/com/candlefinance/push/FirebaseMessagingService.kt +++ b/android/src/main/java/com/candlefinance/push/FirebaseMessagingService.kt @@ -84,9 +84,15 @@ class FirebaseMessagingService : FirebaseMessagingService() { HeadlessJsTaskService.acquireWakeLockNow(baseContext) } else { Log.e(TAG, "Failed to start headless task") + PushNotificationEventManager.sendEvent( + PushNotificationEventType.BACKGROUND_MESSAGE_RECEIVED, payload.toWritableMap() + ) } } catch (exception: Exception) { Log.e(TAG, "Something went wrong while starting headless task: ${exception.message}") + PushNotificationEventManager.sendEvent( + PushNotificationEventType.BACKGROUND_MESSAGE_RECEIVED, payload.toWritableMap() + ) } } } diff --git a/android/src/main/java/com/candlefinance/push/NotificationUtils.kt b/android/src/main/java/com/candlefinance/push/NotificationUtils.kt index 621fd22..5aa79b2 100644 --- a/android/src/main/java/com/candlefinance/push/NotificationUtils.kt +++ b/android/src/main/java/com/candlefinance/push/NotificationUtils.kt @@ -103,7 +103,7 @@ class PushNotificationsUtils( val notificationContent = payload.rawData notificationBuilder - .setSmallIcon(getResourceIdByName("ic_default_notification", "drawable")) + .setSmallIcon(getResourceIdByName("ic_default_notification_foreground", "drawable")) .setContentTitle(notificationContent[PushNotificationsConstants.TITLE]) .setContentText(notificationContent[PushNotificationsConstants.BODY]) .setSubText(notificationContent[PushNotificationsConstants.SUBTITLE]) @@ -128,6 +128,7 @@ class PushNotificationsUtils( putExtra(PushNotificationsConstants.OPENAPP, true) val json = JSONObject() notificationContent.forEach { (key, value) -> json.put(key, value) } + Log.d(Tag, "SAVE to intent rawData: $json") putExtra("rawData", json.toString()) } @@ -232,9 +233,22 @@ open class NotificationPayload( fun toWritableMap(): WritableMap { val map = Arguments.createMap() - map.putMap("rawData", Arguments.makeNativeMap(rawData)) + if (rawData.containsKey("rawData")) { + val existingRawData = rawData["rawData"]?.let { + JSONObject(it) + } + val toMap = existingRawData?.let { + it.keys().asSequence().associateWith { key -> it.get(key).toString() } + } + toMap?.forEach { (key, value) -> map.putString(key, value) } + map.putMap("rawData", Arguments.makeNativeMap(toMap)) + Log.d(Tag, "TO READABLE existing: $map") + } else { + map.putMap("rawData", Arguments.makeNativeMap(rawData)) + rawData.forEach { (key, value) -> map.putString(key, value) } + Log.d(Tag, "TO READABLE new: $map") + } map.putString("channelId", channelId) - rawData.forEach { (key, value) -> map.putString(key, value) } return map } @@ -252,6 +266,7 @@ open class NotificationPayload( return intent?.extras?.let { val toMap: Map = it.keySet().associateWith { key -> it.get(key)?.toString() } val contentProvider = NotificationContentProvider.FCM(toMap.filterValues { it != null } as Map) + Log.d(Tag, "READING: Notification payload from intent: $toMap") NotificationPayload(contentProvider) } } diff --git a/android/src/main/java/com/candlefinance/push/PushModule.kt b/android/src/main/java/com/candlefinance/push/PushModule.kt index 80d2dc8..4d79aa9 100644 --- a/android/src/main/java/com/candlefinance/push/PushModule.kt +++ b/android/src/main/java/com/candlefinance/push/PushModule.kt @@ -7,6 +7,8 @@ import android.content.Context.MODE_PRIVATE import android.content.Intent import android.content.pm.PackageManager import android.os.Build +import android.os.Handler +import android.os.Looper import android.util.Log import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -57,7 +59,6 @@ class PushModule( const val NAME = "Push" } - private var isAppLaunch: Boolean = true private var launchNotification: WritableMap? = null private val sharedPreferences = reactContext.getSharedPreferences(PREF_FILE_KEY, MODE_PRIVATE) private val scope = CoroutineScope(dispatcher) @@ -181,46 +182,27 @@ class PushModule( */ override fun onHostResume() { Log.d(TAG, "App resumed") - if (isAppLaunch) { - isAppLaunch = false - PushNotificationEventManager.init(reactApplicationContext) - val firebaseInstance = FirebaseMessaging.getInstance() - firebaseInstance.token.addOnCompleteListener(OnCompleteListener { task -> - if (!task.isSuccessful) { - Log.w(TAG, "Fetching FCM registration token failed") - return@OnCompleteListener - } - val params = Arguments.createMap().apply { - putString("token", task.result) - } - Log.d(TAG, "Send device token event") - PushNotificationEventManager.sendEvent( - PushNotificationEventType.TOKEN_RECEIVED, - params - ) - }) - currentActivity?.intent?.let { + PushNotificationEventManager.init(reactApplicationContext) + currentActivity?.intent?.let { val payload = NotificationPayload.fromIntent(it) if (payload != null) { + Log.d(TAG, "Launch notification found in intent waiting 5 seconds") launchNotification = payload.toWritableMap() - // Launch notification opened event is emitted for internal use only - PushNotificationEventManager.sendEvent( - PushNotificationEventType.LAUNCH_NOTIFICATION_OPENED, - payload.toWritableMap() - ) + Handler(Looper.getMainLooper()).postDelayed({ + PushNotificationEventManager.sendEvent( + PushNotificationEventType.LAUNCH_NOTIFICATION_OPENED, + payload.toWritableMap() + ) + }, 3000) } else { Log.d(TAG, "No launch notification found in intent") } } - } else { - // Wipe the launching notification as app was re-opened by some other means - Log.d(TAG, "Wipe launching notification") - launchNotification = null - } } override fun onHostPause() { // noop - only overridden as this class implements LifecycleEventListener + Log.d(TAG, "App paused") } override fun onHostDestroy() {