package io.hackle.reactnative import android.util.Log import com.facebook.react.bridge.ReadableMap import io.hackle.android.HackleConfig import io.hackle.sdk.common.EvaluationMode import io.hackle.sdk.common.Event import io.hackle.sdk.common.HackleSessionPersistCondition import io.hackle.sdk.common.HackleSessionPolicy import io.hackle.sdk.common.HackleSessionTimeoutCondition import io.hackle.sdk.common.PropertyOperation import io.hackle.sdk.common.PropertyOperations import io.hackle.sdk.common.Screen import io.hackle.sdk.common.User import io.hackle.sdk.common.subscription.HackleSubscriptionStatus import io.hackle.sdk.common.subscription.HackleSubscriptionOperations internal fun ReadableMap.toHackleConfig(): HackleConfig { try { val builder = HackleConfig.builder() val wrapperName = this.getString("wrapperName") ?: "" val wrapperVersion = this.getString("wrapperVersion") ?: "" this.getString("sdkUrl")?.let { builder.sdkUri(it) } this.getString("eventUrl")?.let { builder.eventUri(it) } this.getString("monitoringUrl")?.let { builder.monitoringUri(it) } this.getIntOrNull("sessionTimeoutMillis")?.let { builder.sessionTimeoutMillis(it) } this.getIntOrNull("pollingIntervalMillis")?.let { builder.pollingIntervalMillis(it) } this.getIntOrNull("eventFlushIntervalMillis")?.let { builder.eventFlushIntervalMillis(it) } this.getIntOrNull("eventFlushThreshold")?.let { builder.eventFlushThreshold(it) } this.getIntOrNull("exposureEventDedupIntervalMillis")?.let { builder.exposureEventDedupIntervalMillis(it) } builder.add("\$wrapper_name", wrapperName) builder.add("\$wrapper_version", wrapperVersion) this.getBooleanOrNull("debug")?.takeIf { it }?.let { builder.eventFlushIntervalMillis(1_000) builder.logLevel(Log.DEBUG) } this.getBooleanOrNull("automaticAppLifecycleTracking")?.let { builder.automaticAppLifecycleTracking(it) } this.getBooleanOrNull("automaticScreenTracking")?.let { builder.automaticScreenTracking(it) } this.getBooleanOrNull("enableMonitoring")?.let { builder.enableMonitoring(it) } this.getBooleanOrNull("optOutTracking")?.let { builder.optOutTracking(it) } this.getString("evaluationMode")?.let { when (it) { "local" -> builder.evaluationMode(EvaluationMode.LOCAL) "remote" -> builder.evaluationMode(EvaluationMode.REMOTE) else -> Log.w("HackleReactNativeSdk", "Unknown evaluationMode: $it") } } this.getMap("sessionPolicy")?.let { policy -> val sessionPolicyBuilder = HackleSessionPolicy.builder() policy.getString("persistCondition")?.let { when (it) { "alwaysNewSession" -> sessionPolicyBuilder.persistCondition(HackleSessionPersistCondition.ALWAYS_NEW_SESSION) "nullToUserId" -> sessionPolicyBuilder.persistCondition(HackleSessionPersistCondition.NULL_TO_USER_ID) else -> Log.w("HackleReactNativeSdk", "Unknown persistCondition: $it") } } policy.getMap("timeoutCondition")?.let { timeout -> val timeoutBuilder = HackleSessionTimeoutCondition.builder() timeout.getIntOrNull("timeoutIntervalMillis")?.let { timeoutBuilder.millis(it.toLong()) } timeout.getBooleanOrNull("onForeground")?.let { timeoutBuilder.onForeground(it) } timeout.getBooleanOrNull("onBackground")?.let { timeoutBuilder.onBackground(it) } timeout.getBooleanOrNull("onApplicationStateChange")?.let { timeoutBuilder.onApplicationStateChange(it) } sessionPolicyBuilder.timeoutCondition(timeoutBuilder.build()) } builder.sessionPolicy(sessionPolicyBuilder.build()) } return builder.build() } catch (_: Throwable) { val builder = HackleConfig.builder() val wrapperName = this.getString("wrapperName") ?: "" val wrapperVersion = this.getString("wrapperVersion") ?: "" builder.add("\$wrapper_name", wrapperName) builder.add("\$wrapper_version", wrapperVersion) return builder.build() } } internal val ReadableMap.properties: ReadableMap? get() = if (this.hasKey("properties")) { this.getMap("properties") } else { null } internal val ReadableMap.identifiers: ReadableMap? get() = if (this.hasKey("identifiers")) { this.getMap("identifiers") } else { null } internal fun ReadableMap.getBooleanOrNull(key: String): Boolean? = if (this.hasKey(key) && !this.isNull(key)) { this.getBoolean(key) } else { null } internal fun ReadableMap.getIntOrNull(key: String): Int? = if (this.hasKey(key) && !this.isNull(key)) { this.getInt(key) } else { null } internal fun ReadableMap.getDoubleOrNull(key: String): Double? = if (this.hasKey(key) && !this.isNull(key)) { this.getDouble(key) } else { null } internal fun ReadableMap.toMap(): Map { return this.toHashMap().toValidatedMap() } internal fun ReadableMap.toUser(): User { val userBuilder: User.Builder = this.getString("id")?.let { User.builder(it) } ?: User.builder() this.getString("userId")?.let { userBuilder.userId(it) } this.getString("deviceId")?.let { userBuilder.deviceId(it) } val identifiers = this.identifiers val properties = this.properties if (identifiers != null) { userBuilder.identifiers(identifiers.toMap().convertToStringHashMap()); } if (properties != null) { userBuilder.properties(properties.toMap()); } return userBuilder.build() } internal fun ReadableMap.toEvent(): Event { val key = requireNotNull(this.getString("key")) { "event key ust be required" } val properties = this.properties val eventBuilder = Event.builder(key) this.getDoubleOrNull("value")?.let { eventBuilder.value(it) } if (properties != null) { eventBuilder.properties(properties.toMap()) } return eventBuilder.build() } internal fun ReadableMap.toPropertyOperations(): PropertyOperations { val builder = PropertyOperations.Builder() this.toMap().entries.forEach { it -> val propertyOperation = PropertyOperation.from(it.key) @Suppress("UNCHECKED_CAST") val value = it.value as? Map when (propertyOperation) { PropertyOperation.SET -> value?.entries?.forEach { inner -> builder.set(inner.key, inner.value) } PropertyOperation.SET_ONCE -> value?.entries?.forEach { inner -> builder.setOnce(inner.key, inner.value) } PropertyOperation.UNSET -> value?.entries?.forEach { inner -> builder.unset(inner.key) } PropertyOperation.INCREMENT -> value?.entries?.forEach { inner -> builder.increment(inner.key, inner.value) } PropertyOperation.APPEND -> value?.entries?.forEach { inner -> builder.append(inner.key, inner.value) } PropertyOperation.APPEND_ONCE -> value?.entries?.forEach { inner -> builder.appendOnce(inner.key, inner.value) } PropertyOperation.PREPEND -> value?.entries?.forEach { inner -> builder.prepend(inner.key, inner.value) } PropertyOperation.PREPEND_ONCE -> value?.entries?.forEach { inner -> builder.prependOnce(inner.key, inner.value) } PropertyOperation.REMOVE -> value?.entries?.forEach { inner -> builder.remove(inner.key, inner.value) } PropertyOperation.CLEAR_ALL -> builder.clearAll() } } return builder.build() } internal fun ReadableMap.toHackleSubscriptionOperations(): HackleSubscriptionOperations { val builder = HackleSubscriptionOperations.Builder() this.toMap().entries.forEach { it -> if (it.value is String) { val status = HackleSubscriptionStatus.valueOf(it.value as String) builder.custom(it.key, status) } } return builder.build() } internal fun ReadableMap.toScreen(): Screen { val name = this.getString("name") ?: "" val screenClass = this.getString("screenClass") ?: "" val properties = this.properties val screenBuilder = Screen.builder(name, screenClass) properties?.let { screenBuilder.properties(it.toMap()) } return screenBuilder.build() } private fun Map.convertToStringHashMap(): Map { return this.filterValues { it is String } .mapValues { it.value as String } } private fun Map.toValidatedMap(): Map { return this .filterValues { it != null } .mapValues { (_, value) -> if (value is Number && value.isWholeNumber()) { value.toLong() } else if (value is Map<*, *>) { @Suppress("UNCHECKED_CAST") (value as? Map)?.toValidatedMap() ?: value } else { value!! } } } private fun Number.isWholeNumber(): Boolean { if (this.toDouble() % 1 != 0.0) { return false } val longValue = this.toLong() return longValue >= JavaScriptNumber.MIN_SAFE_INTEGER && longValue <= JavaScriptNumber.MAX_SAFE_INTEGER }