diff --git a/android/gradle.properties b/android/gradle.properties index 781b120d..db3db310 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -3,4 +3,4 @@ Airship_minSdkVersion=21 Airship_targetSdkVersion=34 Airship_compileSdkVersion=34 Airship_ndkversion=26.1.10909125 -Airship_airshipProxyVersion=8.3.0 +Airship_airshipProxyVersion=9.1.3 diff --git a/android/src/main/java/com/urbanairship/reactnative/AirshipExtender.kt b/android/src/main/java/com/urbanairship/reactnative/AirshipExtender.kt index 7e5cc367..0ee2ed2f 100644 --- a/android/src/main/java/com/urbanairship/reactnative/AirshipExtender.kt +++ b/android/src/main/java/com/urbanairship/reactnative/AirshipExtender.kt @@ -10,6 +10,7 @@ import com.urbanairship.UAirship * Register the extender fully qualified class name in the manifest under the key * `com.urbanairship.reactnative.AIRSHIP_EXTENDER`. */ -interface AirshipExtender { +@Deprecated("Use com.urbanairship.android.framework.proxy.AirshipPluginExtender instead and register it under the manifest key `com.urbanairship.plugin.extender`") +interface AirshipExtender { fun onAirshipReady(context: Context, airship: UAirship) } \ No newline at end of file diff --git a/android/src/main/java/com/urbanairship/reactnative/AirshipModule.kt b/android/src/main/java/com/urbanairship/reactnative/AirshipModule.kt index e333864f..5a55566f 100644 --- a/android/src/main/java/com/urbanairship/reactnative/AirshipModule.kt +++ b/android/src/main/java/com/urbanairship/reactnative/AirshipModule.kt @@ -8,10 +8,13 @@ import com.urbanairship.PendingResult import com.urbanairship.actions.ActionResult import com.urbanairship.actions.ActionValue import com.urbanairship.android.framework.proxy.EventType +import com.urbanairship.android.framework.proxy.NotificationConfig import com.urbanairship.android.framework.proxy.ProxyLogger import com.urbanairship.android.framework.proxy.events.EventEmitter import com.urbanairship.android.framework.proxy.proxies.AirshipProxy +import com.urbanairship.android.framework.proxy.proxies.EnableUserNotificationsArgs import com.urbanairship.android.framework.proxy.proxies.FeatureFlagProxy +import com.urbanairship.android.framework.proxy.proxies.LiveUpdateRequest import com.urbanairship.android.framework.proxy.proxies.SuspendingPredicate import com.urbanairship.json.JsonMap import com.urbanairship.json.JsonSerializable @@ -238,9 +241,12 @@ class AirshipModule internal constructor(val context: ReactApplicationContext) : } @ReactMethod - override fun pushEnableUserNotifications(promise: Promise) { + override fun pushEnableUserNotifications(options: ReadableMap?, promise: Promise) { promise.resolveSuspending(scope) { - proxy.push.enableUserPushNotifications() + val args = options?.let { + EnableUserNotificationsArgs.fromJson(Utils.convertMap(it).toJsonValue()) + } + proxy.push.enableUserPushNotifications(args = args) } } @@ -693,6 +699,12 @@ class AirshipModule internal constructor(val context: ReactApplicationContext) : } } + override fun liveActivityListAll(promise: Promise) { + promise.resolveResult { + throw IllegalStateException("Not supported on Android") + } + } + override fun liveActivityList(request: ReadableMap?, promise: Promise) { promise.resolveResult { throw IllegalStateException("Not supported on Android") @@ -717,6 +729,48 @@ class AirshipModule internal constructor(val context: ReactApplicationContext) : } } + override fun liveUpdateListAll(promise: Promise) { + promise.resolveSuspending(scope) { + proxy.liveUpdateManager.listAll().let { + JsonValue.wrapOpt(it) + } + } + } + + override fun liveUpdateList(request: ReadableMap?, promise: Promise) { + promise.resolveSuspending(scope) { + proxy.liveUpdateManager.list( + LiveUpdateRequest.List.fromJson(Utils.convertMap(requireNotNull(request)).toJsonValue()) + ).let { + JsonValue.wrapOpt(it) + } + } + } + + override fun liveUpdateCreate(request: ReadableMap?, promise: Promise) { + promise.resolveSuspending(scope) { + proxy.liveUpdateManager.create( + LiveUpdateRequest.Create.fromJson(Utils.convertMap(requireNotNull(request)).toJsonValue()) + ) + } + } + + override fun liveUpdateUpdate(request: ReadableMap?, promise: Promise) { + promise.resolveSuspending(scope) { + proxy.liveUpdateManager.update( + LiveUpdateRequest.Update.fromJson(Utils.convertMap(requireNotNull(request)).toJsonValue()) + ) + } + } + + override fun liveUpdateEnd(request: ReadableMap?, promise: Promise) { + promise.resolveSuspending(scope) { + proxy.liveUpdateManager.end( + LiveUpdateRequest.End.fromJson(Utils.convertMap(requireNotNull(request)).toJsonValue()) + ) + } + } + private fun notifyPending() { if (context.hasActiveReactInstance()) { val appEventEmitter = context.getJSModule(RCTNativeAppEventEmitter::class.java) diff --git a/android/src/main/java/com/urbanairship/reactnative/ReactAutopilot.kt b/android/src/main/java/com/urbanairship/reactnative/ReactAutopilot.kt index 194ac959..a6868e3a 100644 --- a/android/src/main/java/com/urbanairship/reactnative/ReactAutopilot.kt +++ b/android/src/main/java/com/urbanairship/reactnative/ReactAutopilot.kt @@ -17,7 +17,9 @@ import com.urbanairship.embedded.AirshipEmbeddedInfo import com.urbanairship.embedded.AirshipEmbeddedObserver import com.urbanairship.json.JsonMap import com.urbanairship.json.jsonMapOf -import kotlinx.coroutines.MainScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -26,14 +28,12 @@ import kotlinx.coroutines.launch */ class ReactAutopilot : BaseAutopilot() { - override fun onAirshipReady(airship: UAirship) { - super.onAirshipReady(airship) + private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + override fun onReady(context: Context, airship: UAirship) { ProxyLogger.info("Airship React Native version: %s, SDK version: %s", BuildConfig.AIRSHIP_MODULE_VERSION, UAirship.getVersion()) - val context = UAirship.getApplicationContext() - - MainScope().launch { + scope.launch { EventEmitter.shared().pendingEventListener .filter { !it.type.isForeground() } .collect { @@ -41,18 +41,19 @@ class ReactAutopilot : BaseAutopilot() { } } - MainScope().launch { + scope.launch { AirshipEmbeddedObserver(filter = { true }).embeddedViewInfoFlow.collect { EventEmitter.shared().addEvent(PendingEmbeddedUpdated(it)) } } - // Set our custom notification providerr + // Set our custom notification provider val notificationProvider = ReactNotificationProvider(context, airship.airshipConfigOptions) airship.pushManager.notificationProvider = notificationProvider airship.analytics.registerSDKExtension(Extension.REACT_NATIVE, BuildConfig.AIRSHIP_MODULE_VERSION) + // Legacy extender val extender = createExtender(context) extender?.onAirshipReady(context, airship) } @@ -61,6 +62,7 @@ class ReactAutopilot : BaseAutopilot() { DataMigrator(context).migrateData(proxyStore) } + @Suppress("deprecation") private fun createExtender(context: Context): AirshipExtender? { val ai: ApplicationInfo try { @@ -77,7 +79,7 @@ class ReactAutopilot : BaseAutopilot() { try { val extenderClass = Class.forName(classname) - return extenderClass.newInstance() as AirshipExtender + return extenderClass.getDeclaredConstructor().newInstance() as AirshipExtender } catch (e: Exception) { ProxyLogger.error(e, "Unable to create extender: $classname") } diff --git a/android/src/oldarch/java/com/urbanairship/reactnative/AirshipSpec.kt b/android/src/oldarch/java/com/urbanairship/reactnative/AirshipSpec.kt index cd8d914d..27a0fd42 100644 --- a/android/src/oldarch/java/com/urbanairship/reactnative/AirshipSpec.kt +++ b/android/src/oldarch/java/com/urbanairship/reactnative/AirshipSpec.kt @@ -98,7 +98,7 @@ abstract class AirshipSpec internal constructor(context: ReactApplicationContext @ReactMethod @com.facebook.proguard.annotations.DoNotStrip - abstract fun pushEnableUserNotifications(promise: Promise) + abstract fun pushEnableUserNotifications(options: ReadableMap?, promise: Promise) @ReactMethod @com.facebook.proguard.annotations.DoNotStrip @@ -405,6 +405,10 @@ abstract class AirshipSpec internal constructor(context: ReactApplicationContext @com.facebook.proguard.annotations.DoNotStrip abstract fun featureFlagManagerTrackInteraction(flag: ReadableMap?, promise: Promise) + @ReactMethod + @com.facebook.proguard.annotations.DoNotStrip + abstract fun liveActivityListAll(promise: Promise) + @ReactMethod @com.facebook.proguard.annotations.DoNotStrip abstract fun liveActivityList(request: ReadableMap?, promise: Promise) @@ -420,6 +424,24 @@ abstract class AirshipSpec internal constructor(context: ReactApplicationContext @ReactMethod @com.facebook.proguard.annotations.DoNotStrip abstract fun liveActivityEnd(request: ReadableMap?, promise: Promise) -} + @ReactMethod + @com.facebook.proguard.annotations.DoNotStrip + abstract fun liveUpdateListAll(promise: Promise) + + @ReactMethod + @com.facebook.proguard.annotations.DoNotStrip + abstract fun liveUpdateList(request: ReadableMap?, promise: Promise) + + @ReactMethod + @com.facebook.proguard.annotations.DoNotStrip + abstract fun liveUpdateCreate(request: ReadableMap?, promise: Promise) + @ReactMethod + @com.facebook.proguard.annotations.DoNotStrip + abstract fun liveUpdateUpdate(request: ReadableMap?, promise: Promise) + + @ReactMethod + @com.facebook.proguard.annotations.DoNotStrip + abstract fun liveUpdateEnd(request: ReadableMap?, promise: Promise) +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 4122f36a..0ab88c50 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -3,23 +3,28 @@ - - - - - - + android:roundIcon="@mipmap/ic_launcher_round" + android:theme="@style/AppTheme"> + + + + + + + + + diff --git a/example/android/app/src/main/java/com/urbanairship/sample/AirshipExtender.kt b/example/android/app/src/main/java/com/urbanairship/sample/AirshipExtender.kt new file mode 100644 index 00000000..269ebb57 --- /dev/null +++ b/example/android/app/src/main/java/com/urbanairship/sample/AirshipExtender.kt @@ -0,0 +1,69 @@ +package com.urbanairship.sample + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.annotation.Keep +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.urbanairship.UAirship +import com.urbanairship.android.framework.proxy.AirshipPluginExtender +import com.urbanairship.json.requireField +import com.urbanairship.liveupdate.LiveUpdate +import com.urbanairship.liveupdate.LiveUpdateEvent +import com.urbanairship.liveupdate.LiveUpdateManager +import com.urbanairship.liveupdate.LiveUpdateResult +import com.urbanairship.liveupdate.SuspendLiveUpdateNotificationHandler + + +@Keep +public final class AirshipExtender: AirshipPluginExtender { + override fun onAirshipReady(context: Context, airship: UAirship) { + LiveUpdateManager.shared().register("Example", ExampleLiveUpdateHandler()) + } +} + +public final class ExampleLiveUpdateHandler: SuspendLiveUpdateNotificationHandler() { + override suspend fun onUpdate( + context: Context, + event: LiveUpdateEvent, + update: LiveUpdate + ): LiveUpdateResult { + + if (event == LiveUpdateEvent.END) { + // Dismiss the live update on END. The default behavior will leave the Live Update + // in the notification tray until the dismissal time is reached or the user dismisses it. + return LiveUpdateResult.cancel() + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel("emoji-example", "Emoji example", importance) + channel.description = "Emoji example" + NotificationManagerCompat.from(context).createNotificationChannel(channel) + } + + val launchIntent = context.packageManager + .getLaunchIntentForPackage(context.packageName) + ?.addCategory(update.name) + ?.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) + ?.setPackage(null) + + val contentIntent = PendingIntent.getActivity( + context, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE + ) + + val notification = NotificationCompat.Builder(context, "emoji-example") + .setSmallIcon(R.drawable.ic_notification) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_EVENT) + .setContentTitle("Example Live Update") + .setContentText(update.content.requireField("emoji")) + .setContentIntent(contentIntent) + + return LiveUpdateResult.ok(notification) + } +} \ No newline at end of file diff --git a/example/android/app/src/main/res/drawable-anydpi-v24/ic_notification.xml b/example/android/app/src/main/res/drawable-anydpi-v24/ic_notification.xml new file mode 100644 index 00000000..6f92d40c --- /dev/null +++ b/example/android/app/src/main/res/drawable-anydpi-v24/ic_notification.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/example/android/app/src/main/res/drawable-hdpi/ic_notification.png b/example/android/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 00000000..8e46a11c Binary files /dev/null and b/example/android/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/example/android/app/src/main/res/drawable-mdpi/ic_notification.png b/example/android/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 00000000..32067e0c Binary files /dev/null and b/example/android/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/example/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/example/android/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 00000000..bc21cccd Binary files /dev/null and b/example/android/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/example/android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/example/android/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 00000000..a0753ae4 Binary files /dev/null and b/example/android/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 203fd248..aba700cc 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -17,7 +17,7 @@ PODS: - Airship/Core - Airship/PreferenceCenter (18.9.2): - Airship/Core - - AirshipFrameworkProxy (8.3.0): + - AirshipFrameworkProxy (9.1.3): - Airship (= 18.9.2) - AirshipServiceExtension (18.9.2) - boost (1.83.0) @@ -908,7 +908,7 @@ PODS: - glog - React-debug - react-native-airship (19.3.2): - - AirshipFrameworkProxy (= 8.3.0) + - AirshipFrameworkProxy (= 9.1.3) - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1280,7 +1280,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Airship: 7f891aa9bb142d02f35aaef5ebdb09c2b5730a6d - AirshipFrameworkProxy: 48208d21ca1376d9bc111efd31981af84bfcf8f3 + AirshipFrameworkProxy: 9a983b72a47ce10d8eda32b446ea553ef7bcc8f2 AirshipServiceExtension: 0ed795b521a76f8391e13896fbe1dee6ce9196ca boost: d3f49c53809116a5d38da093a8aa78bf551aed09 DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 @@ -1311,7 +1311,7 @@ SPEC CHECKSUMS: React-jsinspector: 9ac353eccf6ab54d1e0a33862ba91221d1e88460 React-logger: 0a57b68dd2aec7ff738195f081f0520724b35dab React-Mapbuffer: 63913773ed7f96b814a2521e13e6d010282096ad - react-native-airship: c68835c32f7b5a100b25bf5f2ecc6e4fae6ddce4 + react-native-airship: 6afeeef72fa57a06a716afaca0ababb8adfaee81 react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b React-nativeconfig: d7af5bae6da70fa15ce44f045621cf99ed24087c React-NativeModulesApple: 0123905d5699853ac68519607555a9a4f5c7b3ac diff --git a/example/package-lock.json b/example/package-lock.json index 8e0b163d..67c2988d 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -11,7 +11,8 @@ "babel-plugin-module-resolver": "^5.0.0", "moment": "^2.30.1", "react": "18.2.0", - "react-native": "0.73.4" + "react-native": "0.73.4", + "react-native-uuid": "^2.0.2" }, "devDependencies": { "@babel/core": "^7.20.0", @@ -12184,6 +12185,15 @@ "react-native": "*" } }, + "node_modules/react-native-uuid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-native-uuid/-/react-native-uuid-2.0.2.tgz", + "integrity": "sha512-5ypj/hV58P+6VREdjkW0EudSibsH3WdqDERoHKnD9syFWjF+NfRWWrJb2sa3LIwI5zpzMvUiabs+DX40WHpEMw==", + "engines": { + "node": ">=10.0.0", + "npm": ">=6.0.0" + } + }, "node_modules/react-native-vector-icons": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.0.3.tgz", diff --git a/example/package.json b/example/package.json index 007c27a1..91027d3f 100644 --- a/example/package.json +++ b/example/package.json @@ -12,7 +12,8 @@ "babel-plugin-module-resolver": "^5.0.0", "moment": "^2.30.1", "react": "18.2.0", - "react-native": "0.73.4" + "react-native": "0.73.4", + "react-native-uuid": "^2.0.2" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/example/src/screens/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index 835cf737..d0ae546d 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -6,6 +6,7 @@ */ import React, { useState, useEffect, useCallback } from 'react'; +import uuid from 'react-native-uuid'; import { View, Text, @@ -82,49 +83,28 @@ export default function HomeScreen() { [refreshTags] ); - const handleNotificationsEnabled = useCallback((enabled: boolean) => { - Airship.push.setUserNotificationsEnabled(enabled); - setNotificationsEnabled(enabled); + const handleNotificationsEnabled = useCallback(async (enabled: boolean) => { + if (enabled) { + await Airship.push.enableUserNotifications({ + fallback: 'systemSettings', + }); + } else { + Airship.push.setUserNotificationsEnabled(false); + } }, []); useEffect(() => { - // Add takeOff here - - Airship.push - .getNotificationStatus() - .then((id) => { - console.log(id); - }) - .catch((error) => { - console.error('Error getting notification status:', error); - }); - setEmbeddedReady(Airship.inApp.isEmbeddedReady('test')); - Airship.push.iOS - .getAuthorizedNotificationSettings() - .then((id) => { - console.log(id); - }) - .catch((error) => { - console.error('Error getting notification settings:', error); - }); - - Airship.push.iOS.getAuthorizedNotificationStatus().then((id) => { - console.log(id); - }); - - Airship.push.getNotificationStatus().then((id) => { - console.log(id); - }); - Airship.channel.getChannelId().then((id) => { if (id) { setChannelId(id); } }); - Airship.push.isUserNotificationsEnabled().then(setNotificationsEnabled); + Airship.push + .getNotificationStatus() + .then((status) => setNotificationsEnabled(status.isUserOptedIn)); const fetchTags = async () => { const fetchedTags = await Airship.channel.getTags(); @@ -140,20 +120,32 @@ export default function HomeScreen() { fetchNamedUser(); - let subscription = Airship.addListener( + let channelListener = Airship.addListener( EventType.ChannelCreated, (event) => { setChannelId(event.channelId); } ); - Airship.inApp.addEmbeddedReadyListener('test', (isReady) => { - console.log('Test ' + isReady); - setEmbeddedReady(isReady); - }); + let embeddedListener = Airship.inApp.addEmbeddedReadyListener( + 'test', + (isReady) => { + setEmbeddedReady(isReady); + } + ); + + let optInListener = Airship.addListener( + EventType.PushNotificationStatusChangedStatus, + (event) => { + console.log('Event', event); + setNotificationsEnabled(event.status.isUserOptedIn); + } + ); return () => { - subscription.remove(); + channelListener.remove(); + embeddedListener.remove(); + optInListener.remove(); }; }, []); @@ -183,10 +175,20 @@ export default function HomeScreen() { )} - {Platform.OS === 'ios' ? ( - -