Skip to content

Commit

Permalink
API expansion to match native SDKs
Browse files Browse the repository at this point in the history
  • Loading branch information
adam1929 committed May 22, 2023
1 parent db89847 commit 986939d
Show file tree
Hide file tree
Showing 20 changed files with 2,102 additions and 1,158 deletions.
165 changes: 165 additions & 0 deletions android/src/main/java/com/exponea/ExponeaModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.exponea.sdk.models.ExponeaProject
import com.exponea.sdk.models.FlushMode
import com.exponea.sdk.models.FlushPeriod
import com.exponea.sdk.models.PropertiesList
import com.exponea.sdk.style.appinbox.StyledAppInboxProvider
import com.exponea.sdk.util.Logger
import com.exponea.style.AppInboxStyleParser
import com.facebook.react.bridge.Promise
Expand Down Expand Up @@ -86,8 +87,11 @@ class ExponeaModule(val reactContext: ReactApplicationContext) : ReactContextBas
private var configuration: ExponeaConfiguration = ExponeaConfiguration()
private var pushOpenedListenerSet = false
private var pushReceivedListenerSet = false
private var inAppActionListenerSet = false
// We have to hold received push data until pushReceivedListener set in JS
private var pendingReceivedPushData: Map<String, Any>? = null
// We have to hold received action data until pushReceivedListener set in JS
private var pendingInAppAction: InAppMessageAction? = null

override fun getName(): String {
return "Exponea"
Expand Down Expand Up @@ -553,4 +557,165 @@ class ExponeaModule(val reactContext: ReactApplicationContext) : ReactContextBas
Exponea.appInboxProvider = ReactNativeAppInboxProvider(style)
promise.resolve(null)
}

@ReactMethod
fun setAutomaticSessionTracking(enabled: Boolean, promise: Promise) = catchAndReject(promise) {
Exponea.isAutomaticSessionTracking = enabled
promise.resolve(null)
}

@ReactMethod
fun setSessionTimeout(timeout: Double, promise: Promise) = catchAndReject(promise) {
Exponea.sessionTimeout = timeout
promise.resolve(null)
}

@ReactMethod
fun setAutoPushNotification(enabled: Boolean, promise: Promise) = catchAndReject(promise) {
Exponea.isAutoPushNotification = enabled
promise.resolve(null)
}

@ReactMethod
fun setCampaignTTL(seconds: Double, promise: Promise) = catchAndReject(promise) {
Exponea.campaignTTL = seconds
promise.resolve(null)
}

internal fun onInAppAction(data: InAppMessageAction) {
if (inAppActionListenerSet) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("inAppAction", Gson().toJson(data))
} else {
pendingInAppAction = data
}
}

@ReactMethod
fun onInAppActionListenerSet(promise: Promise) = catchAndReject(promise) {
inAppActionListenerSet = true
onInAppAction(pendingInAppAction ?: return@catchAndReject)
pendingInAppAction = null
promise.resolve(null)
}

@ReactMethod
fun setInAppMessageActionListener(configMap: ReadableMap, promise: Promise) = catchAndReject(promise) {
Exponea.inAppMessageActionCallback = ReactNativeInAppActionListener(configMap.toHashMapRecursively())
promise.resolve(null)
}

@ReactMethod
fun trackPushToken(token: String, promise: Promise) = catchAndReject(promise) {
Exponea.trackPushToken(token)
promise.resolve(null)
}

@ReactMethod
fun trackHmsPushToken(token: String, promise: Promise) = catchAndReject(promise) {
Exponea.trackHmsPushToken(token)
promise.resolve(null)
}

@ReactMethod
fun trackDeliveredPush(params: ReadableMap, promise: Promise) = catchAndReject(promise) {
val notification = params.toHashMapRecursively().toNotificationData()
val receivedSeconds = params
.toHashMapRecursively().getNullSafely("receivedSeconds") ?: currentTimeSeconds()
Exponea.trackDeliveredPush(notification, receivedSeconds)
promise.resolve(null)
}

@ReactMethod
fun trackDeliveredPushWithoutTrackingConsent(params: ReadableMap, promise: Promise) = catchAndReject(promise) {
val notification = params.toHashMapRecursively().toNotificationData()
val receivedSeconds = params
.toHashMapRecursively().getNullSafely("receivedSeconds") ?: currentTimeSeconds()
Exponea.trackDeliveredPushWithoutTrackingConsent(notification, receivedSeconds)
promise.resolve(null)
}

@ReactMethod
fun trackClickedPush(params: ReadableMap, promise: Promise) = catchAndReject(promise) {
val notification = params.toHashMapRecursively().toNotificationData()
val notificationAction = params.toHashMapRecursively().toNotificationAction()
val receivedSeconds = params
.toHashMapRecursively().getNullSafely("receivedSeconds") ?: currentTimeSeconds()
Exponea.trackClickedPush(notification, notificationAction, receivedSeconds)
promise.resolve(null)
}

@ReactMethod
fun trackClickedPushWithoutTrackingConsent(params: ReadableMap, promise: Promise) = catchAndReject(promise) {
val notification = params.toHashMapRecursively().toNotificationData()
val notificationAction = params.toHashMapRecursively().toNotificationAction()
val receivedSeconds = params
.toHashMapRecursively().getNullSafely("receivedSeconds") ?: currentTimeSeconds()
Exponea.trackClickedPushWithoutTrackingConsent(notification, notificationAction, receivedSeconds)
promise.resolve(null)
}

@ReactMethod
fun trackPaymentEvent(params: ReadableMap, promise: Promise) = catchAndReject(promise) {
val receivedSeconds = params
.toHashMapRecursively().getNullSafely("receivedSeconds") ?: currentTimeSeconds()
val paymentItem = params
.toHashMapRecursively().toPurchasedItem()
Exponea.trackPaymentEvent(receivedSeconds, paymentItem)
promise.resolve(null)
}

@ReactMethod
fun isExponeaPushNotification(params: ReadableMap, promise: Promise) = catchAndReject(promise) {
@Suppress("UNCHECKED_CAST")
val notificationData = params
.toHashMapRecursively()
.mapValues { it.value as? String }
.filterValues { it != null } as Map<String, String>
promise.resolve(Exponea.isExponeaPushNotification(notificationData))
}

@ReactMethod
fun trackInAppMessageClick(params: ReadableMap, promise: Promise) = catchAndReject(promise) {
val data = params.toHashMapRecursively().toInAppMessageAction()
if (data == null) {
promise.reject(ExponeaDataException("InApp message data are invalid. See logs"))
return@catchAndReject
}
Exponea.trackInAppMessageClick(data.message, data.button?.text, data.button?.url)
promise.resolve(null)
}

@ReactMethod
fun trackInAppMessageClickWithoutTrackingConsent(params: ReadableMap, promise: Promise) = catchAndReject(promise) {
val data = params.toHashMapRecursively().toInAppMessageAction()
if (data == null) {
promise.reject(ExponeaDataException("InApp message data are invalid. See logs"))
return@catchAndReject
}
Exponea.trackInAppMessageClickWithoutTrackingConsent(data.message, data.button?.text, data.button?.url)
promise.resolve(null)
}

@ReactMethod
fun trackInAppMessageClose(params: ReadableMap, promise: Promise) = catchAndReject(promise) {
val data = params.toHashMapRecursively().toInAppMessage()
if (data == null) {
promise.reject(ExponeaDataException("InApp message data are invalid. See logs"))
return@catchAndReject
}
Exponea.trackInAppMessageClose(data)
promise.resolve(null)
}

@ReactMethod
fun trackInAppMessageCloseWithoutTrackingConsent(params: ReadableMap, promise: Promise) = catchAndReject(promise) {
val data = params.toHashMapRecursively().toInAppMessage()
if (data == null) {
promise.reject(ExponeaDataException("InApp message data are invalid. See logs"))
return@catchAndReject
}
Exponea.trackInAppMessageCloseWithoutTrackingConsent(data)
promise.resolve(null)
}
}
71 changes: 41 additions & 30 deletions android/src/main/java/com/exponea/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import android.graphics.drawable.Drawable
import android.util.TypedValue
import androidx.core.graphics.drawable.DrawableCompat
import com.exponea.sdk.models.MessageItem
import com.exponea.sdk.models.MessageItemAction
import com.exponea.sdk.models.MessageItemAction.Type
import com.exponea.sdk.util.Logger
import com.facebook.react.bridge.Dynamic
import com.facebook.react.bridge.ReadableArray
Expand All @@ -16,9 +14,14 @@ import com.facebook.react.bridge.ReadableType
import com.facebook.react.bridge.ReadableType.Array
import com.facebook.react.bridge.ReadableType.Null
import com.facebook.react.bridge.ReadableType.Number
import java.util.Date
import kotlin.reflect.KClass
import kotlin.text.RegexOption.IGNORE_CASE

internal inline fun <reified T : Any> Map<String, Any?>.getRequired(key: String): T {
return getSafely(key, T::class)
}

internal fun <T : Any> Map<String, Any?>.getSafely(key: String, type: KClass<T>): T {
val value = this[key] ?: throw ExponeaModule.ExponeaDataException("Property '$key' cannot be null.")
if (value::class == type) {
Expand Down Expand Up @@ -63,40 +66,46 @@ internal fun MessageItem.toMap(): Map<String, Any?> {
)
}

internal fun Map<String, Any?>.toMessageItem(): MessageItem? {
val id = this.getNullSafely("id", String::class)
val rawType = this.getNullSafely("type", String::class)
if (id.isNullOrEmpty() || rawType.isNullOrEmpty()) {
return null
}
val read = this.getNullSafely("is_read", Boolean::class)
val receivedTime = this.getNullSafely("create_time", Double::class)
val rawContent = this.getNullSafely<HashMap<String, Any>>("content")
return MessageItem(
id = id,
rawType = rawType,
read = read,
receivedTime = receivedTime,
rawContent = rawContent
)
internal inline fun <reified T : Any> Map<String, Any?>.getNullSafelyMap(key: String, defaultValue: Map<String, T>? = null): Map<String, T>? {
return getNullSafelyMap(key, T::class, defaultValue)
}

internal fun MessageItemAction.toMap(): Map<String, Any?> {
return mapOf(
"type" to type.value,
"title" to title,
"url" to url
internal inline fun <reified T : Any> Map<String, Any?>.getNullSafelyMap(key: String, type: KClass<T>, defaultValue: Map<String, T>? = null): Map<String, T>? {
val value = this[key] ?: return defaultValue
@Suppress("UNCHECKED_CAST")
val mapOfAny = value as? Map<String, Any?> ?: throw ExponeaModule.ExponeaDataException(
"Non-map type for key '$key'. Got ${value::class.simpleName}"
)
return mapOfAny.filterValueIsInstance(type.java)
}

internal fun Map<String, Any?>.toMessageItemAction(): MessageItemAction? {
val source = this
val sourceType = Type.find(source.getNullSafely("type")) ?: return null
return MessageItemAction().apply {
type = sourceType
title = source.getNullSafely("title")
url = source.getNullSafely("url")
/**
* Returns a map containing all key-value pairs with values are instances of specified class.
*
* The returned map preserves the entry iteration order of the original map.
*/
internal fun <K, V, R> Map<out K, V>.filterValueIsInstance(klass: Class<R>): Map<K, R> {
val result = LinkedHashMap<K, R>()
for (entry in this) {
if (klass.isInstance(entry.value)) {
@Suppress("UNCHECKED_CAST")
((entry.value as R).also { result[entry.key] = it })
}
}
return result
}

internal inline fun <reified T : Any> Map<String, Any?>.getNullSafelyArray(key: String, defaultValue: List<T>? = null): List<T>? {
return getNullSafelyArray(key, T::class, defaultValue)
}

internal inline fun <reified T : Any> Map<String, Any?>.getNullSafelyArray(key: String, type: KClass<T>, defaultValue: List<T>? = null): List<T>? {
val value = this[key] ?: return defaultValue
val arrayOfAny = value as? List<Any?> ?: throw ExponeaModule.ExponeaDataException(
"Non-array type for key '$key'. Got ${value::class.simpleName}"
)
return arrayOfAny
.filterIsInstance(type.java)
}

internal inline fun <reified T : Any> Map<String, Any?>.getNullSafely(key: String, defaultValue: T? = null): T? {
Expand Down Expand Up @@ -454,3 +463,5 @@ internal fun Int.asColorString(): String {
val alpha = Color.alpha(this)
return String.format("#%02x%02x%02x%02x", red, green, blue, alpha)
}

fun currentTimeSeconds() = Date().time / 1000.0
10 changes: 10 additions & 0 deletions android/src/main/java/com/exponea/InAppMessageAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.exponea

import com.exponea.sdk.models.InAppMessage
import com.exponea.sdk.models.InAppMessageButton

data class InAppMessageAction(
var message: InAppMessage,
var button: InAppMessageButton?,
var interaction: Boolean
)
Loading

0 comments on commit 986939d

Please sign in to comment.