package com.blux import android.content.Context import android.os.Handler import android.os.Looper import android.util.Log import com.blux_android_sdk.BluxClient import com.blux_android_sdk.BluxNotification import com.blux_android_sdk.HttpUrlOpenTarget import com.blux_android_sdk.InAppUrlOpenOptions import com.blux_android_sdk.NotificationReceivedEvent import com.blux_android_sdk.NotificationUrlOpenOptions import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableType import com.facebook.react.bridge.Arguments import com.facebook.react.modules.core.DeviceEventManagerModule import org.json.JSONObject class BluxModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener { private val TAG: String by lazy { this::class.simpleName ?: "BluxModule" } private val NOTIFICATION_CLICKED_EVENT_KEY = "notification-clicked" private val NOTIFICATION_FOREGROUND_WILL_DISPLAY_EVENT_KEY = "notification-foreground-will-display" private val INAPP_CLICKED_EVENT_KEY = "inapp-clicked" private val INAPP_CUSTOM_ACTION_EVENT_KEY = "inapp-custom-action" private var inAppCustomActionUnsubscribe: Runnable? = null private val appContext: Context get() = reactApplicationContext.applicationContext private val mainHandler = Handler(Looper.getMainLooper()) override fun getName(): String = NAME @ReactMethod fun addListener(eventName: String) { /* no-op */ } @ReactMethod fun removeListeners(count: Int) { /* no-op */ } override fun initialize() { super.initialize() reactApplicationContext.addLifecycleEventListener(this) } override fun invalidate() { reactApplicationContext.removeLifecycleEventListener(this) super.invalidate() } override fun onHostResume() { val act = reactApplicationContext.currentActivity if (act != null) { mainHandler.post { BluxClient.setCurrentActivity(act) } } } override fun onHostPause() { /* no-op */ } override fun onHostDestroy() { /* no-op */ } @ReactMethod fun initialize( bluxApplicationId: String, bluxAPIKey: String, requestPermissionOnLaunch: Boolean, customDeviceId: String?, promise: Promise ) { mainHandler.post { try { BluxClient.initialize( appContext, bluxApplicationId, bluxAPIKey, requestPermissionOnLaunch, customDeviceId, object : BluxClient.CompletionCallback { override fun onSuccess() { Log.d(TAG, "BluxClient.initialize succeeded") // 초기화 직후에도 activity 있으면 한번 주입 reactApplicationContext.currentActivity?.let { BluxClient.setCurrentActivity(it) } promise.resolve(null) } override fun onFailure(e: Exception) { Log.e(TAG, "BluxClient.initialize failed", e) promise.reject("BLUX_INIT_FAILED", e.message, e) } }) } catch (e: Exception) { Log.e(TAG, "BluxClient.initialize failed", e) promise.reject("BLUX_INIT_FAILED", e.message, e) } } } private fun convertLogLevelToInt(level: String): Int? { return when (level) { "error" -> Log.ERROR "verbose" -> Log.VERBOSE else -> null } } @ReactMethod fun setLogLevel(logLevel: String, promise: Promise) { val logLevelAsInt = convertLogLevelToInt(logLevel) ?: return BluxClient.setLogLevel(logLevelAsInt) promise.resolve(null) } @ReactMethod fun signIn(userId: String, promise: Promise) { BluxClient.signIn(appContext, userId, null) promise.resolve(null) } @ReactMethod fun signOut(promise: Promise) { BluxClient.signOut(appContext) promise.resolve(null) } private fun ReadableMap.toJSONObject(): JSONObject { val jsonObject = JSONObject() val iterator = this.keySetIterator() while (iterator.hasNextKey()) { val key = iterator.nextKey() try { when (this.getType(key)) { ReadableType.Null -> jsonObject.put(key, JSONObject.NULL) ReadableType.Boolean -> jsonObject.put(key, this.getBoolean(key)) ReadableType.Number -> jsonObject.put(key, this.getDouble(key)) ReadableType.String -> jsonObject.put(key, this.getString(key)) ReadableType.Map -> { val map = this.getMap(key) if (map != null) { jsonObject.put(key, map.toJSONObject()) } else { jsonObject.put(key, JSONObject.NULL) } } ReadableType.Array -> { val array = this.getArray(key) if (array != null) { jsonObject.put(key, array.toJSONArray()) } else { jsonObject.put(key, JSONObject.NULL) } } } } catch (e: Exception) { Log.e(TAG, "Error processing key '$key'", e) jsonObject.put(key, JSONObject.NULL) } } return jsonObject } private fun ReadableArray.toJSONArray(): org.json.JSONArray { val jsonArray = org.json.JSONArray() for (i in 0 until this.size()) { try { when (this.getType(i)) { ReadableType.Null -> jsonArray.put(JSONObject.NULL) ReadableType.Boolean -> jsonArray.put(this.getBoolean(i)) ReadableType.Number -> jsonArray.put(this.getDouble(i)) ReadableType.String -> jsonArray.put(this.getString(i)) ReadableType.Map -> { val map = this.getMap(i) if (map != null) { jsonArray.put(map.toJSONObject()) } else { jsonArray.put(JSONObject.NULL) } } ReadableType.Array -> { val array = this.getArray(i) if (array != null) { jsonArray.put(array.toJSONArray()) } else { jsonArray.put(JSONObject.NULL) } } } } catch (e: Exception) { Log.e(TAG, "Error processing array element at index $i", e) jsonArray.put(JSONObject.NULL) } } return jsonArray } @ReactMethod fun sendRequest(events: ReadableArray, promise: Promise) { try { // ReadableArray를 JSONObject 리스트로 직접 변환 val jsonObjectList = mutableListOf() for (i in 0 until events.size()) { try { val type = events.getType(i) if (type == ReadableType.Map) { val readableMap = events.getMap(i) if (readableMap != null) { val jsonObject = readableMap.toJSONObject() if (jsonObject != null) { jsonObjectList.add(jsonObject) } else { Log.w(TAG, "toJSONObject() returned null at index $i") } } else { Log.w(TAG, "ReadableMap at index $i is null") } } else { Log.w(TAG, "Item at index $i is not a Map type, it's $type") } } catch (e: Exception) { Log.e(TAG, "Error processing item at index $i", e) } } BluxClient.sendRequestData(appContext, jsonObjectList) promise.resolve(null) } catch (e: Exception) { Log.e(TAG, "Error in sendRequest", e) promise.reject("SEND_REQUEST_ERROR", e.message, e) } } @ReactMethod fun setUserProperties(userProperties: ReadableMap, promise: Promise) { BluxClient.setUserPropertiesData(appContext, userProperties.toJSONObject()) promise.resolve(null) } @ReactMethod fun setCustomUserProperties(customUserProperties: ReadableMap, promise: Promise) { BluxClient.setCustomUserProperties(appContext, customUserProperties.toJSONObject()) promise.resolve(null) } @ReactMethod fun setNotificationUrlOpenOptions(options: ReadableMap, promise: Promise) { val raw = if (options.hasKey("httpUrlOpenTarget")) options.getString("httpUrlOpenTarget") else null val target = parseHttpUrlOpenTarget(raw) ?: run { promise.reject("INVALID_ARGUMENT", "Invalid httpUrlOpenTarget: $raw") return } BluxClient.setNotificationUrlOpenOptions( appContext, NotificationUrlOpenOptions(httpUrlOpenTarget = target) ) promise.resolve(null) } private fun parseHttpUrlOpenTarget(raw: String?): HttpUrlOpenTarget? = when (raw) { "internalWebView", null -> HttpUrlOpenTarget.INTERNAL_WEBVIEW "externalBrowser" -> HttpUrlOpenTarget.EXTERNAL_BROWSER "none" -> HttpUrlOpenTarget.NONE else -> null } @ReactMethod fun startNotificationForegroundWillDisplayHandler(promise: Promise) { BluxClient.setNotificationForegroundWillDisplayHandler { event -> emit(NOTIFICATION_FOREGROUND_WILL_DISPLAY_EVENT_KEY, Arguments.makeNativeMap(event.toHashMap())) } promise.resolve(null) } @ReactMethod fun displayNotification(notification: ReadableMap, promise: Promise) { val id = notification.getString("id") ?: run { promise.reject("INVALID_ARGUMENT", "notification.id is required") return } val body = notification.getString("body") ?: "" val dataJson = if (notification.hasKey("data") && notification.getType("data") == ReadableType.Map ) notification.getMap("data")?.toJSONObject()?.toString() else null val bluxNotification = BluxNotification( id = id, body = body, data = dataJson, title = notification.getString("title"), url = notification.getString("url"), imageUrl = notification.getString("imageUrl") ) NotificationReceivedEvent(appContext, bluxNotification).display() promise.resolve(null) } @ReactMethod fun startNotificationClickedHandler(promise: Promise) { BluxClient.setNotificationClickedHandler { notification -> emit(NOTIFICATION_CLICKED_EVENT_KEY, Arguments.makeNativeMap(notification.toHashMap())) } promise.resolve(null) } @ReactMethod fun setInAppUrlOpenOptions(options: ReadableMap, promise: Promise) { val raw = if (options.hasKey("httpUrlOpenTarget")) options.getString("httpUrlOpenTarget") else null val target = parseHttpUrlOpenTarget(raw) ?: run { promise.reject("INVALID_ARGUMENT", "Invalid httpUrlOpenTarget: $raw") return } BluxClient.setInAppUrlOpenOptions( appContext, InAppUrlOpenOptions(httpUrlOpenTarget = target) ) promise.resolve(null) } @ReactMethod fun startInAppClickedHandler(promise: Promise) { BluxClient.setInAppClickedHandler { inApp -> emit(INAPP_CLICKED_EVENT_KEY, Arguments.makeNativeMap(inApp.toHashMap())) } promise.resolve(null) } @ReactMethod fun startInAppCustomActionHandler() { if (inAppCustomActionUnsubscribe != null) { return } inAppCustomActionUnsubscribe = BluxClient.addInAppCustomActionHandler { actionId, data -> val params = Arguments.createMap().apply { putString("actionId", actionId) val dataMap = Arguments.createMap() val keys = data.keys() while (keys.hasNext()) { val key = keys.next() when (val value = data.opt(key)) { is String -> dataMap.putString(key, value) is Boolean -> dataMap.putBoolean(key, value) is Int -> dataMap.putInt(key, value) is Double -> dataMap.putDouble(key, value) is Long -> dataMap.putDouble(key, value.toDouble()) null -> dataMap.putNull(key) else -> dataMap.putString(key, value.toString()) } } putMap("data", dataMap) } emit(INAPP_CUSTOM_ACTION_EVENT_KEY, params) } } @ReactMethod fun dismissInApp() { BluxClient.dismissInApp() } private fun emit(eventName: String, params: Any?) { reactApplicationContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) } companion object { const val NAME = "Blux" } }