package io.hackle.reactnative import android.util.Log import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule import java.util.concurrent.Semaphore import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import io.hackle.android.Hackle import io.hackle.android.HackleConfig import io.hackle.android.app import io.hackle.android.initialize import io.hackle.sdk.common.ParameterConfig import io.hackle.sdk.common.User import io.hackle.sdk.common.decision.Decision import io.hackle.sdk.common.decision.DecisionReason import io.hackle.sdk.common.decision.FeatureFlagDecision import io.hackle.sdk.common.HackleInAppMessage import io.hackle.sdk.common.HackleInAppMessageAction import io.hackle.sdk.common.HackleInAppMessageActionType import io.hackle.sdk.common.HackleInAppMessageListener import io.hackle.sdk.common.HackleInAppMessageView import io.hackle.sdk.common.subscription.HackleSubscriptionOperations class HackleReactNativeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { private val logTag = "Hackle" private var hasEventListener: Boolean = false private var inAppMessageView: HackleInAppMessageView? = null // onInAppMessageClick 임계영역 private val inAppMessageClickSemaphore = Semaphore(1) private var inAppMessageClickResolveBlock: ((Boolean) -> Unit)? = null override fun getName(): String { return "HackleReactNativeSdk" } @ReactMethod fun initialize(sdkKey: String, config: ReadableMap?, user: ReadableMap?, promise: Promise) { val hackleConfig = config?.toHackleConfig() ?: HackleConfig.builder().build() val hackleUser = user?.toUser() if (hackleUser != null) { Hackle.initialize(reactApplicationContext, sdkKey, hackleUser, hackleConfig) { promise.resolve(null) } } else { Hackle.initialize(reactApplicationContext, sdkKey, hackleConfig) { promise.resolve(null) } } internalSetInAppMessageListener() } @ReactMethod(isBlockingSynchronousMethod = true) fun variationDetailSync(experimentKey: Double): WritableMap { return try { Hackle.app.variationDetail(experimentKey.toLong()).toMap() } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) getDefaultDecisionMap() } } @ReactMethod fun variationDetail(experimentKey: Double, promise: Promise) { promise.resolve(variationDetailSync(experimentKey)) } @ReactMethod(isBlockingSynchronousMethod = true) fun featureFlagDetailSync(featureKey: Double): WritableMap { return try { Hackle.app.featureFlagDetail(featureKey.toLong()).toMap() } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) getDefaultFeatureFlagDecisionMap() } } @ReactMethod fun featureFlagDetail(featureKey: Double, promise: Promise) { promise.resolve(featureFlagDetailSync(featureKey)) } @ReactMethod fun track(eventMap: ReadableMap) { try { Hackle.app.track(eventMap.toEvent()) } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) } } @ReactMethod(isBlockingSynchronousMethod = true) fun getRemoteConfigStringSync(parameterKey: String, defaultValue: String): String { return try { Hackle.app.remoteConfig().getString(parameterKey, defaultValue) } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) defaultValue } } @ReactMethod fun getRemoteConfigString(parameterKey: String, defaultValue: String, promise: Promise) { promise.resolve(getRemoteConfigStringSync(parameterKey, defaultValue)) } @ReactMethod(isBlockingSynchronousMethod = true) fun getRemoteConfigDoubleSync(parameterKey: String, defaultValue: Double): Double { return try { Hackle.app.remoteConfig().getDouble(parameterKey, defaultValue) } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) defaultValue } } @ReactMethod fun getRemoteConfigDouble(parameterKey: String, defaultValue: Double, promise: Promise) { promise.resolve(getRemoteConfigDoubleSync(parameterKey, defaultValue)) } @ReactMethod(isBlockingSynchronousMethod = true) fun getRemoteConfigBooleanSync(parameterKey: String, defaultValue: Boolean): Boolean { return try { Hackle.app.remoteConfig().getBoolean(parameterKey, defaultValue) } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) defaultValue } } @ReactMethod fun getRemoteConfigBoolean(parameterKey: String, defaultValue: Boolean, promise: Promise) { promise.resolve(getRemoteConfigBooleanSync(parameterKey, defaultValue)) } @ReactMethod(isBlockingSynchronousMethod = true) fun getDeviceId(): String { return Hackle.app.deviceId } @ReactMethod fun getUser(promise: Promise) { promise.resolve(Hackle.app.user.toMap()) } @ReactMethod fun setUser(user: ReadableMap, promise: Promise) { Hackle.app.setUser(user.toUser()) { promise.resolve(null) } } @ReactMethod fun setUserId(userId: String?, promise: Promise) { Hackle.app.setUserId(userId) { promise.resolve(null) } } @ReactMethod fun setDeviceId(deviceId: String, promise: Promise) { Hackle.app.setDeviceId(deviceId) { promise.resolve(null) } } @ReactMethod fun updateUserProperties(operations: ReadableMap, promise: Promise) { val propertyOperations = operations.toPropertyOperations() Hackle.app.updateUserProperties(propertyOperations) { promise.resolve(null) } } @ReactMethod fun updatePushSubscriptions(operations: ReadableMap, promise: Promise) { val hackleSubscriptionOperations = operations.toHackleSubscriptionOperations() Hackle.app.updatePushSubscriptions(hackleSubscriptionOperations) promise.resolve(null) } @ReactMethod fun updateSmsSubscriptions(operations: ReadableMap, promise: Promise) { val hackleSubscriptionOperations = operations.toHackleSubscriptionOperations() Hackle.app.updateSmsSubscriptions(hackleSubscriptionOperations) promise.resolve(null) } @ReactMethod fun updateKakaoSubscriptions(operations: ReadableMap, promise: Promise) { val hackleSubscriptionOperations = operations.toHackleSubscriptionOperations() Hackle.app.updateKakaoSubscriptions(hackleSubscriptionOperations) promise.resolve(null) } @ReactMethod fun resetUser(promise: Promise) { Hackle.app.resetUser { promise.resolve(null) } } @ReactMethod fun setPhoneNumber(phoneNumber: String, promise: Promise) { Hackle.app.setPhoneNumber(phoneNumber) { promise.resolve(null) } } @ReactMethod fun unsetPhoneNumber(promise: Promise) { Hackle.app.unsetPhoneNumber { promise.resolve(null) } } @ReactMethod fun showUserExplorer(promise: Promise) { Hackle.app.showUserExplorer() promise.resolve(null) } @ReactMethod fun hideUserExplorer(promise: Promise) { Hackle.app.hideUserExplorer() promise.resolve(null) } @ReactMethod fun fetch(promise: Promise) { Hackle.app.fetch { promise.resolve(null) } } @ReactMethod fun isInvocableString(command: String, promise: Promise) { val result = Hackle.app.invocator().isInvocableString(command) promise.resolve(result) } @ReactMethod fun bridgeInvoke(command: String, promise: Promise) { val result = Hackle.app.invocator().invoke(command) promise.resolve(result) } @ReactMethod fun setCurrentScreen(screen: ReadableMap, promise: Promise) { try { val screenObject = screen.toScreen() Hackle.app.setCurrentScreen(screenObject) promise.resolve(null) } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) promise.resolve(null) } } @ReactMethod fun setOptOutTracking(optOut: Boolean, promise: Promise) { try { Hackle.app.setOptOutTracking(optOut) promise.resolve(null) } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) promise.resolve(null) } } @ReactMethod fun isOptOutTracking(promise: Promise) { try { promise.resolve(Hackle.app.isOptOutTracking) } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) promise.resolve(false) } } @ReactMethod fun beginListening(promise: Promise) { hasEventListener = true promise.resolve(null) } @ReactMethod fun endListening(promise: Promise) { hasEventListener = false promise.resolve(null) } @ReactMethod fun resolveInAppMessageClick(result: Boolean, promise: Promise) { inAppMessageClickResolveBlock?.invoke(result) promise.resolve(null) } @ReactMethod fun closeInAppMessageView(promise: Promise) { inAppMessageView?.close() inAppMessageView = null promise.resolve(null) } @ReactMethod fun dismissDisplayedInAppMessageView(promise: Promise) { try { Hackle.app.displayedInAppMessageView?.close() promise.resolve(null) } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) promise.resolve(null) } } @ReactMethod fun setBackButtonDismissesInAppMessageView(dismisses: Boolean, promise: Promise) { try { Hackle.app.setBackButtonDismissesInAppMessageView(dismisses) promise.resolve(null) } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) promise.resolve(null) } } @ReactMethod fun addListener(eventName: String) { // nothing to do } @ReactMethod fun removeListeners(count: Int) { // nothing to do } private fun internalSetInAppMessageListener() { Hackle.app.setInAppMessageListener(object : HackleInAppMessageListener { override fun beforeInAppMessageOpen(inAppMessage: HackleInAppMessage) { if (!hasEventListener) { return } val map = Arguments.createMap() map.putMap("inAppMessage", inAppMessage.toMap()) reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(InAppMessageEvents.beforeInAppMessageOpenEvent.name, map) } override fun afterInAppMessageOpen(inAppMessage: HackleInAppMessage) { if (!hasEventListener) { return } val map = Arguments.createMap() map.putMap("inAppMessage", inAppMessage.toMap()) reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(InAppMessageEvents.afterInAppMessageOpenEvent.name, map) } override fun beforeInAppMessageClose(inAppMessage: HackleInAppMessage) { if (!hasEventListener) { return } val map = Arguments.createMap() map.putMap("inAppMessage", inAppMessage.toMap()) reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(InAppMessageEvents.beforeInAppMessageCloseEvent.name, map) } override fun afterInAppMessageClose(inAppMessage: HackleInAppMessage) { if (!hasEventListener) { return } val map = Arguments.createMap() map.putMap("inAppMessage", inAppMessage.toMap()) reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(InAppMessageEvents.afterInAppMessageCloseEvent.name, map) } override fun onInAppMessageClick( inAppMessage: HackleInAppMessage, view: HackleInAppMessageView, action: HackleInAppMessageAction ): Boolean { if (!hasEventListener) { return false } if(!inAppMessageClickSemaphore.tryAcquire()) { Log.i(logTag, "InAppMessage click event blocked - concurrent access detected") return true } return try { inAppMessageView = view var clickResult = false val countDownLatch = CountDownLatch(1) inAppMessageClickResolveBlock = { result -> clickResult = result countDownLatch.countDown() } val map = Arguments.createMap() map.putMap("inAppMessage", inAppMessage.toMap()) map.putMap("action", action.toMap()) reactApplicationContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(InAppMessageEvents.onInAppMessageClickEvent.name, map) countDownLatch.await(100, TimeUnit.MILLISECONDS) clickResult } catch (tr: Throwable) { Log.e(logTag, "Unexpected error", tr) false } finally { inAppMessageView = null inAppMessageClickResolveBlock = null inAppMessageClickSemaphore.release() } } }) } private fun getDefaultDecisionMap(): WritableMap { val map = Arguments.createMap() map.putString("variation", "A") map.putString("reason", DecisionReason.EXCEPTION.name) map.putMap("config", Arguments.createMap()) return map } private fun getDefaultFeatureFlagDecisionMap(): WritableMap { val map = Arguments.createMap() map.putBoolean("isOn", false) map.putString("reason", DecisionReason.EXCEPTION.name) map.putMap("config", Arguments.createMap()) return map } } private fun User.toMap(): WritableMap { val map = Arguments.createMap() val properties = Arguments.makeNativeMap(this.properties) val identifiers = Arguments.makeNativeMap(this.identifiers) map.putString("id", this.id) map.putString("userId", this.userId) map.putString("deviceId", this.deviceId) map.putMap("properties", properties) map.putMap("identifiers", identifiers) return map } private fun ParameterConfig.toMap(): WritableMap { val map = Arguments.makeNativeMap(this.parameters) return map } private fun Decision.toMap(): WritableMap { val map = Arguments.createMap() map.putString("variation", this.variation.name) map.putString("reason", this.reason.name) map.putMap("parameters", this.config.toMap()) return map } private fun FeatureFlagDecision.toMap(): WritableMap { val map = Arguments.createMap() map.putBoolean("isOn", this.isOn) map.putString("reason", this.reason.name) map.putMap("parameters", this.config.toMap()) return map } private fun HackleInAppMessage.toMap(): WritableMap { val map = Arguments.createMap() map.putDouble("key", this.key.toDouble()) return map } private fun HackleInAppMessageAction.toMap(): WritableMap { val map = Arguments.createMap() map.putString("type", this.type.name) if (this.type == HackleInAppMessageActionType.CLOSE) { val closeMap = Arguments.createMap() closeMap.putDouble("hideDurationMillis", this.close?.hideDurationMillis?.toDouble() ?: 0.0) map.putMap("close", closeMap) } else if (this.type == HackleInAppMessageActionType.LINK) { val linkMap = Arguments.createMap() linkMap.putString("url", this.link?.url ?: "") linkMap.putBoolean("shouldCloseAfterLink", this.link?.shouldCloseAfterLink ?: false) map.putMap("link", linkMap) } return map }