-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: process push notifications received outside CIO SDK (#38)
Co-authored-by: Shahroz Khan <[email protected]>
- Loading branch information
Showing
10 changed files
with
316 additions
and
0 deletions.
There are no files selected for viewing
33 changes: 33 additions & 0 deletions
33
android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package io.customer.customer_io | ||
|
||
import io.flutter.plugin.common.MethodCall | ||
import io.flutter.plugin.common.MethodChannel | ||
|
||
/** | ||
* Returns the value corresponding to the given key after casting to the generic type provided, or | ||
* null if such key is not present in the map or value cannot be casted to the given type. | ||
*/ | ||
internal inline fun <reified T> Map<String, Any>.getAsTypeOrNull(key: String): T? { | ||
if (containsKey(key)) { | ||
return get(key) as? T | ||
} | ||
return null | ||
} | ||
|
||
/** | ||
* Invokes lambda method that can be used to call matching native method conveniently. The lambda | ||
* expression receives function parameters as arguments and should return the desired result. Any | ||
* exception in the lambda will cause the invoked method to fail with error. | ||
*/ | ||
internal fun <R> MethodCall.invokeNative( | ||
result: MethodChannel.Result, | ||
performAction: (params: Map<String, Any>) -> R, | ||
) { | ||
try { | ||
@Suppress("UNCHECKED_CAST") | ||
val params = this.arguments as? Map<String, Any> ?: emptyMap() | ||
result.success(performAction(params)) | ||
} catch (ex: Exception) { | ||
result.error(this.method, ex.localizedMessage, ex) | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
android/src/main/kotlin/io/customer/customer_io/CustomerIOPluginModule.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package io.customer.customer_io | ||
|
||
import io.flutter.embedding.engine.plugins.FlutterPlugin | ||
|
||
/** | ||
* Module class corresponds to modules concept in native SDKs. Any module added to native SDKs | ||
* should be treated as module in Flutter SDK and should be used to hold all relevant methods at | ||
* single place. | ||
*/ | ||
internal interface CustomerIOPluginModule { | ||
/** | ||
* Unique name of module to identify between other modules | ||
*/ | ||
val moduleName: String | ||
|
||
/** | ||
* Called whenever root FlutterPlugin has been associated with a FlutterEngine instance. | ||
* | ||
* @see [FlutterPlugin.onAttachedToEngine] for more details | ||
*/ | ||
fun onAttachedToEngine() | ||
|
||
/** | ||
* Called whenever root FlutterPlugin has been removed from a FlutterEngine instance. | ||
* | ||
* @see [FlutterPlugin.onDetachedFromEngine] for more details | ||
*/ | ||
fun onDetachedFromEngine() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package io.customer.customer_io.messagingpush | ||
|
||
import android.content.Context | ||
import io.customer.customer_io.CustomerIOPluginModule | ||
import io.customer.customer_io.constant.Keys | ||
import io.customer.customer_io.getAsTypeOrNull | ||
import io.customer.customer_io.invokeNative | ||
import io.customer.messagingpush.CustomerIOFirebaseMessagingService | ||
import io.customer.sdk.CustomerIOShared | ||
import io.customer.sdk.extensions.takeIfNotBlank | ||
import io.customer.sdk.util.Logger | ||
import io.flutter.embedding.engine.plugins.FlutterPlugin | ||
import io.flutter.plugin.common.MethodCall | ||
import io.flutter.plugin.common.MethodChannel | ||
import java.util.* | ||
|
||
/** | ||
* Flutter module implementation for messaging push module in native SDKs. All functionality | ||
* linked with the module should be placed here. | ||
*/ | ||
internal class CustomerIOPushMessaging( | ||
pluginBinding: FlutterPlugin.FlutterPluginBinding, | ||
) : CustomerIOPluginModule, MethodChannel.MethodCallHandler { | ||
override val moduleName: String = "PushMessaging" | ||
private val applicationContext: Context = pluginBinding.applicationContext | ||
private val flutterCommunicationChannel: MethodChannel = | ||
MethodChannel(pluginBinding.binaryMessenger, "customer_io_messaging_push") | ||
private val logger: Logger | ||
get() = CustomerIOShared.instance().diStaticGraph.logger | ||
|
||
override fun onAttachedToEngine() { | ||
flutterCommunicationChannel.setMethodCallHandler(this) | ||
} | ||
|
||
override fun onDetachedFromEngine() { | ||
flutterCommunicationChannel.setMethodCallHandler(null) | ||
} | ||
|
||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { | ||
when (call.method) { | ||
Keys.Methods.ON_MESSAGE_RECEIVED -> { | ||
call.invokeNative(result) { args -> | ||
return@invokeNative onMessageReceived( | ||
message = args.getAsTypeOrNull<Map<String, Any>>("message"), | ||
handleNotificationTrigger = args.getAsTypeOrNull<Boolean>("handleNotificationTrigger") | ||
) | ||
} | ||
} | ||
else -> { | ||
result.notImplemented() | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Handles push notification received. This is helpful in processing push notifications | ||
* received outside the CIO SDK. | ||
* | ||
* @param message push payload received from FCM. | ||
* @param handleNotificationTrigger indicating if the local notification should be triggered. | ||
*/ | ||
private fun onMessageReceived( | ||
message: Map<String, Any>?, | ||
handleNotificationTrigger: Boolean?, | ||
): Boolean { | ||
try { | ||
if (message == null) { | ||
throw IllegalArgumentException("Message cannot be null") | ||
} | ||
|
||
// Generate destination string, see docs on receiver method for more details | ||
val destination = (message["to"] as? String)?.takeIfNotBlank() | ||
?: UUID.randomUUID().toString() | ||
return CustomerIOFirebaseMessagingService.onMessageReceived( | ||
context = applicationContext, | ||
remoteMessage = message.toFCMRemoteMessage(destination = destination), | ||
handleNotificationTrigger = handleNotificationTrigger ?: true, | ||
) | ||
} catch (ex: Throwable) { | ||
logger.error("Unable to handle push notification, reason: ${ex.message}") | ||
throw ex | ||
} | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
android/src/main/kotlin/io/customer/customer_io/messagingpush/Extensions.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package io.customer.customer_io.messagingpush | ||
|
||
import com.google.firebase.messaging.RemoteMessage | ||
import io.customer.customer_io.getAsTypeOrNull | ||
|
||
/** | ||
* Safely transforms any value to string | ||
*/ | ||
private fun Any.toStringOrNull(): String? = try { | ||
toString() | ||
} catch (ex: Exception) { | ||
// We don't need to print any error here as this is expected for some values and doesn't | ||
// break anything | ||
null | ||
} | ||
|
||
/** | ||
* Extension function to build FCM [RemoteMessage] using RN map. This should be independent from | ||
* the sender source and should be able to build a valid [RemoteMessage] for our native SDK. | ||
* | ||
* @param destination receiver of the message. It is mainly required for sending upstream messages, | ||
* since we are using RemoteMessage only for broadcasting messages locally, we can use any non-empty | ||
* string for it. | ||
*/ | ||
internal fun Map<String, Any>.toFCMRemoteMessage(destination: String): RemoteMessage { | ||
val notification = getAsTypeOrNull<Map<String, Any>>("notification") | ||
val data = getAsTypeOrNull<Map<String, Any>>("data") | ||
val messageParams = buildMap { | ||
notification?.let { result -> putAll(result) } | ||
// Adding `data` after `notification` so `data` params take more value as we mainly use | ||
// `data` in rich push | ||
data?.let { result -> putAll(result) } | ||
} | ||
return with(RemoteMessage.Builder(destination)) { | ||
messageParams.let { params -> | ||
val paramsIterator = params.iterator() | ||
while (paramsIterator.hasNext()) { | ||
val (key, value) = paramsIterator.next() | ||
// Some values in notification object can be another object and may not support | ||
// mapping to string values, transforming these values in a try-catch so the code | ||
// doesn't break due to one bad value | ||
value.toStringOrNull()?.let { v -> addData(key, v) } | ||
} | ||
} | ||
getAsTypeOrNull<String>("messageId")?.let { id -> setMessageId(id) } | ||
getAsTypeOrNull<String>("messageType")?.let { type -> setMessageType(type) } | ||
getAsTypeOrNull<String>("collapseKey")?.let { key -> setCollapseKey(key) } | ||
getAsTypeOrNull<Int>("ttl")?.let { time -> ttl = time } | ||
return@with build() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import 'dart:io'; | ||
|
||
import 'package:flutter/foundation.dart'; | ||
import 'package:flutter/services.dart'; | ||
|
||
import '../customer_io_const.dart'; | ||
import 'platform_interface.dart'; | ||
|
||
/// An implementation of [CustomerIOMessagingPushPlatform] that uses method | ||
/// channels. | ||
class CustomerIOMessagingPushMethodChannel | ||
extends CustomerIOMessagingPushPlatform { | ||
/// The method channel used to interact with the native platform. | ||
@visibleForTesting | ||
final methodChannel = const MethodChannel('customer_io_messaging_push'); | ||
|
||
@override | ||
Future<bool> onMessageReceived(Map<String, dynamic> message, | ||
{bool handleNotificationTrigger = true}) { | ||
if (Platform.isIOS) { | ||
/// Since push notifications on iOS work fine with multiple notification | ||
/// SDKs, we don't need to process them on iOS for now. | ||
/// Resolving future to true makes it easier for callers to avoid adding | ||
/// unnecessary platform specific checks. | ||
return Future.value(true); | ||
} | ||
|
||
try { | ||
final arguments = { | ||
TrackingConsts.message: message, | ||
TrackingConsts.handleNotificationTrigger: handleNotificationTrigger, | ||
}; | ||
return methodChannel | ||
.invokeMethod(MethodConsts.onMessageReceived, arguments) | ||
.then((handled) => handled == true); | ||
} on PlatformException catch (exception) { | ||
handleException(exception); | ||
return Future.error( | ||
exception.message ?? "Error handling push notification"); | ||
} | ||
} | ||
|
||
void handleException(PlatformException exception) { | ||
if (kDebugMode) { | ||
print(exception); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; | ||
|
||
import 'method_channel.dart'; | ||
|
||
/// The default instance of [CustomerIOMessagingPushPlatform] to use | ||
/// | ||
/// Platform-specific plugins should override this with their own | ||
/// platform-specific class that extends [CustomerIOMessagingPushPlatform] | ||
/// when they register themselves. | ||
/// | ||
/// Defaults to [CustomerIOMessagingPushMethodChannel] | ||
abstract class CustomerIOMessagingPushPlatform extends PlatformInterface { | ||
CustomerIOMessagingPushPlatform() : super(token: _token); | ||
|
||
static final Object _token = Object(); | ||
|
||
static CustomerIOMessagingPushPlatform _instance = | ||
CustomerIOMessagingPushMethodChannel(); | ||
|
||
static CustomerIOMessagingPushPlatform get instance => _instance; | ||
|
||
static set instance(CustomerIOMessagingPushPlatform instance) { | ||
PlatformInterface.verifyToken(instance, _token); | ||
_instance = instance; | ||
} | ||
|
||
/// Processes push notification received outside the CIO SDK. The method | ||
/// displays notification on device and tracks CIO metrics for push | ||
/// notification. | ||
/// | ||
/// [message] push payload received from FCM. The payload must contain data | ||
/// payload received in push notification. | ||
/// [handleNotificationTrigger] flag to indicate whether it should display the | ||
/// notification or not. | ||
/// `true` (default): The SDK will display the notification and track associated | ||
/// metrics. | ||
/// `false`: The SDK will only process the notification to track metrics but | ||
/// will not display any notification. | ||
/// Returns a [Future] that resolves to boolean indicating if the notification | ||
/// was handled by the SDK or not. | ||
Future<bool> onMessageReceived(Map<String, dynamic> message, | ||
{bool handleNotificationTrigger = true}) { | ||
throw UnimplementedError('onMessageReceived() has not been implemented.'); | ||
} | ||
|
||
/// Handles push notification received when app is background. Since FCM | ||
/// itself displays the notification when app is background, this method makes | ||
/// it easier to determine whether the notification should be displayed or not. | ||
/// | ||
/// @see [onMessageReceived] for more details | ||
Future<bool> onBackgroundMessageReceived(Map<String, dynamic> message) => | ||
onMessageReceived(message, | ||
handleNotificationTrigger: message['notification'] == null); | ||
} |