package com.bigcrunch.ads.react import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule import com.bigcrunch.ads.* import com.bigcrunch.ads.models.* import kotlinx.coroutines.* /** * React Native bridge module for BigCrunch Ads SDK */ class BigCrunchAdsModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private val interstitialCache = mutableMapOf() // Placeholder for interstitial instances private val rewardedCache = mutableMapOf() // Placeholder for rewarded instances override fun getName(): String = "BigCrunchAdsModule" override fun onCatalystInstanceDestroy() { super.onCatalystInstanceDestroy() scope.cancel() } /** * Initialize the SDK */ @ReactMethod fun initialize(options: ReadableMap, promise: Promise) { try { val propertyId = options.getString("propertyId") ?: throw IllegalArgumentException("propertyId is required") val environment = options.getString("environment") ?: "production" val debug = if (options.hasKey("debug")) options.getBoolean("debug") else false val useMockConfig = if (options.hasKey("useMockConfig")) options.getBoolean("useMockConfig") else false val useTestAds = if (options.hasKey("useTestAds")) options.getBoolean("useTestAds") else false // Initialize the native SDK val context = reactApplicationContext.applicationContext scope.launch { try { android.util.Log.d("BigCrunchRNBridge", "initialize() called: propertyId=$propertyId env=$environment mock=$useMockConfig testAds=$useTestAds") BigCrunchAds.initialize( context = context, propertyId = propertyId, env = if (environment == "sandbox" || environment == "staging") BigCrunchAds.Environment.Staging else BigCrunchAds.Environment.Prod, useMockConfig = useMockConfig, useTestAds = useTestAds ) android.util.Log.d("BigCrunchRNBridge", "native initialize() returned, isInitialized=${BigCrunchAds.isInitialized()}") if (debug) { BigCrunchAds.setDebugMode(true) } // Wait for config to be loaded before resolving the promise // This ensures placements are available when the app starts loading ads android.util.Log.d("BigCrunchRNBridge", "waiting for config...") val configLoaded = BigCrunchAds.waitForConfig() android.util.Log.d("BigCrunchRNBridge", "waitForConfig returned: $configLoaded, isConfigReady=${BigCrunchAds.isConfigReady()}") if (!configLoaded) { android.util.Log.e("BigCrunchRNBridge", "config failed to load, rejecting promise") sendEvent("BigCrunchConfigFailed", Arguments.createMap().apply { putString("errorMessage", "Failed to load configuration") }) promise.reject("INIT_ERROR", "Failed to load configuration", null) return@launch } android.util.Log.d("BigCrunchRNBridge", "init complete, resolving promise") sendEvent("BigCrunchConfigUpdated", Arguments.createMap()) sendEvent("BigCrunchSessionStarted", Arguments.createMap()) promise.resolve(null) } catch (e: Exception) { promise.reject("INIT_ERROR", "Failed to initialize SDK: ${e.message}", e) } } } catch (e: Exception) { promise.reject("INIT_ERROR", "Failed to initialize SDK: ${e.message}", e) } } /** * Check if SDK is initialized */ @ReactMethod fun isInitialized(promise: Promise) { promise.resolve(BigCrunchAds.isInitialized()) } /** * Track screen view */ @ReactMethod fun trackScreenView(screenName: String, options: ReadableMap?, promise: Promise) { scope.launch { try { var screenViewOptions: ScreenViewOptions? = null if (options != null) { var pageMeta: PageMetaData? = null if (options.hasKey("pageMeta")) { val metaMap = options.getMap("pageMeta") if (metaMap != null) { pageMeta = PageMetaData( url = metaMap.getString("url"), author = metaMap.getString("author"), title = metaMap.getString("title"), thumbnailUrl = metaMap.getString("thumbnailUrl"), articleSection = metaMap.getString("articleSection"), keywords = metaMap.getString("keywords"), dateCreated = metaMap.getString("dateCreated"), dateModified = metaMap.getString("dateModified"), datePublished = metaMap.getString("datePublished") ) } } var customDimensions: Map? = null if (options.hasKey("customDimensions")) { val dimMap = options.getMap("customDimensions") if (dimMap != null) { val dims = mutableMapOf() val iterator = dimMap.keySetIterator() while (iterator.hasNextKey()) { val key = iterator.nextKey() dimMap.getString(key)?.let { dims[key] = it } } customDimensions = dims } } screenViewOptions = ScreenViewOptions( pageUrl = if (options.hasKey("pageUrl")) options.getString("pageUrl") else null, pageMeta = pageMeta, customDimensions = customDimensions ) } BigCrunchAds.trackScreenView(screenName, screenViewOptions) promise.resolve(null) } catch (e: Exception) { promise.reject("TRACK_ERROR", "Failed to track screen view: ${e.message}", e) } } } /** * Get app configuration */ @ReactMethod fun getAppConfig(promise: Promise) { scope.launch { try { val config = BigCrunchAds.getAppConfig() if (config != null) { promise.resolve(convertAppConfigToMap(config)) } else { promise.resolve(null) } } catch (e: Exception) { promise.reject("CONFIG_ERROR", "Failed to get app config: ${e.message}", e) } } } /** * Refresh configuration */ @ReactMethod fun refreshConfig(promise: Promise) { scope.launch { try { BigCrunchAds.refreshConfig() sendEvent("BigCrunchConfigUpdated", Arguments.createMap()) promise.resolve(null) } catch (e: Exception) { sendEvent("BigCrunchConfigFailed", Arguments.createMap().apply { putString("errorMessage", e.message ?: "Failed to refresh config") }) promise.reject("CONFIG_ERROR", "Failed to refresh config: ${e.message}", e) } } } /** * Load interstitial ad */ @ReactMethod fun loadInterstitial(options: ReadableMap, promise: Promise) { try { val placementId = options.getString("placementId") ?: throw IllegalArgumentException("placementId is required") scope.launch { try { // Create ad request options val customTargeting = options.getMap("customTargeting")?.let { convertMapToStringMap(it) } // Set up callbacks to emit events to JavaScript val context = reactApplicationContext.applicationContext BigCrunchInterstitial.preload( context = context, placementId = placementId, callback = object : BigCrunchInterstitial.PreloadCallback { override fun onAdLoaded() { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) sendEvent("BigCrunchInterstitialAdLoaded", eventParams) // Store the loaded ad in cache (for tracking) interstitialCache[placementId] = true } override fun onAdFailedToLoad(error: String) { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) eventParams.putString("errorCode", "LOAD_FAILED") eventParams.putString("errorMessage", error) sendEvent("BigCrunchInterstitialAdFailedToLoad", eventParams) } } ) // Apply custom targeting if provided (will be implemented in AdOrchestrator later) if (customTargeting != null) { // TODO: Pass custom targeting to AdOrchestrator } promise.resolve(null) } catch (e: Exception) { promise.reject("LOAD_ERROR", "Failed to load interstitial: ${e.message}", e) } } } catch (e: Exception) { promise.reject("LOAD_ERROR", "Failed to load interstitial: ${e.message}", e) } } /** * Show interstitial ad */ @ReactMethod fun showInterstitial(placementId: String, promise: Promise) { val activity = currentActivity if (activity == null) { promise.reject("NO_ACTIVITY", "No current activity available") return } scope.launch { try { val callback = object : BigCrunchInterstitial.ShowCallback { override fun onAdShowed() { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) sendEvent("BigCrunchInterstitialAdOpened", eventParams) // Also emit impression event sendEvent("BigCrunchInterstitialAdImpression", eventParams) } override fun onAdDismissed() { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) sendEvent("BigCrunchInterstitialAdClosed", eventParams) // Clear from cache on dismissal interstitialCache.remove(placementId) } override fun onAdClicked() { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) sendEvent("BigCrunchInterstitialAdClicked", eventParams) } override fun onAdFailedToShow(error: String) { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) eventParams.putString("errorCode", "SHOW_FAILED") eventParams.putString("errorMessage", error) sendEvent("BigCrunchInterstitialAdFailedToShow", eventParams) } } BigCrunchInterstitial.show(activity, placementId, callback) promise.resolve(null) } catch (e: Exception) { promise.reject("SHOW_ERROR", "Failed to show interstitial: ${e.message}", e) } } } /** * Check if interstitial is loaded */ @ReactMethod fun isInterstitialLoaded(placementId: String, promise: Promise) { promise.resolve(BigCrunchInterstitial.isLoaded(placementId)) } /** * Destroy interstitial */ @ReactMethod fun destroyInterstitial(placementId: String, promise: Promise) { try { BigCrunchInterstitial.destroy(placementId) interstitialCache.remove(placementId) promise.resolve(null) } catch (e: Exception) { promise.reject("DESTROY_ERROR", "Failed to destroy interstitial: ${e.message}", e) } } /** * Load rewarded ad */ @ReactMethod fun loadRewarded(options: ReadableMap, promise: Promise) { try { val placementId = options.getString("placementId") ?: throw IllegalArgumentException("placementId is required") scope.launch { try { val customTargeting = options.getMap("customTargeting")?.let { convertMapToStringMap(it) } // Set up callbacks to emit events to JavaScript val context = reactApplicationContext.applicationContext BigCrunchRewarded.preload( context = context, placementId = placementId, callback = object : BigCrunchRewarded.PreloadCallback { override fun onAdLoaded() { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) sendEvent("BigCrunchRewardedAdLoaded", eventParams) // Store the loaded ad in cache (for tracking) rewardedCache[placementId] = true } override fun onAdFailedToLoad(error: String) { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) eventParams.putString("errorCode", "LOAD_FAILED") eventParams.putString("errorMessage", error) sendEvent("BigCrunchRewardedAdFailedToLoad", eventParams) } } ) // Apply custom targeting if provided (will be implemented in AdOrchestrator later) if (customTargeting != null) { // TODO: Pass custom targeting to AdOrchestrator } promise.resolve(null) } catch (e: Exception) { promise.reject("LOAD_ERROR", "Failed to load rewarded: ${e.message}", e) } } } catch (e: Exception) { promise.reject("LOAD_ERROR", "Failed to load rewarded: ${e.message}", e) } } /** * Show rewarded ad */ @ReactMethod fun showRewarded(placementId: String, promise: Promise) { val activity = currentActivity if (activity == null) { promise.reject("NO_ACTIVITY", "No current activity available") return } scope.launch { try { val callback = object : BigCrunchRewarded.ShowCallback { override fun onAdShowed() { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) sendEvent("BigCrunchRewardedAdOpened", eventParams) // Also emit impression event sendEvent("BigCrunchRewardedAdImpression", eventParams) } override fun onUserEarnedReward(type: String, amount: Int) { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) eventParams.putString("type", type) eventParams.putInt("amount", amount) sendEvent("BigCrunchRewardedAdEarned", eventParams) } override fun onAdDismissed() { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) sendEvent("BigCrunchRewardedAdClosed", eventParams) // Clear from cache on dismissal rewardedCache.remove(placementId) } override fun onAdClicked() { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) sendEvent("BigCrunchRewardedAdClicked", eventParams) } override fun onAdFailedToShow(error: String) { val eventParams = Arguments.createMap() eventParams.putString("placementId", placementId) eventParams.putString("errorCode", "SHOW_FAILED") eventParams.putString("errorMessage", error) sendEvent("BigCrunchRewardedAdFailedToShow", eventParams) } } BigCrunchRewarded.show(activity, placementId, callback) promise.resolve(null) } catch (e: Exception) { promise.reject("SHOW_ERROR", "Failed to show rewarded: ${e.message}", e) } } } /** * Check if rewarded is loaded */ @ReactMethod fun isRewardedLoaded(placementId: String, promise: Promise) { promise.resolve(BigCrunchRewarded.isLoaded(placementId)) } /** * Destroy rewarded */ @ReactMethod fun destroyRewarded(placementId: String, promise: Promise) { try { BigCrunchRewarded.destroy(placementId) rewardedCache.remove(placementId) promise.resolve(null) } catch (e: Exception) { promise.reject("DESTROY_ERROR", "Failed to destroy rewarded: ${e.message}", e) } } /** * Get session info */ @ReactMethod fun getSessionInfo(promise: Promise) { scope.launch { try { val sessionInfo = BigCrunchAds.getSessionInfo() promise.resolve(convertSessionInfoToMap(sessionInfo)) } catch (e: Exception) { promise.reject("SESSION_ERROR", "Failed to get session info: ${e.message}", e) } } } /** * Start new session */ @ReactMethod fun startNewSession(promise: Promise) { scope.launch { try { BigCrunchAds.startNewSession() sendEvent("BigCrunchSessionStarted", Arguments.createMap()) promise.resolve(null) } catch (e: Exception) { promise.reject("SESSION_ERROR", "Failed to start new session: ${e.message}", e) } } } /** * Set UTM attribution parameters */ @ReactMethod fun setUTMParameters(params: ReadableMap, promise: Promise) { try { BigCrunchAds.setUTMParameters( source = params.getString("source"), medium = params.getString("medium"), campaign = params.getString("campaign"), term = params.getString("term"), content = params.getString("content") ) promise.resolve(null) } catch (e: Exception) { promise.reject("UTM_ERROR", "Failed to set UTM parameters: ${e.message}", e) } } /** * Clear UTM attribution parameters */ @ReactMethod fun clearUTMParameters(promise: Promise) { try { BigCrunchAds.clearUTMParameters() promise.resolve(null) } catch (e: Exception) { promise.reject("UTM_ERROR", "Failed to clear UTM parameters: ${e.message}", e) } } /** * Get device context */ @ReactMethod fun getDeviceContext(promise: Promise) { scope.launch { try { val deviceData = BigCrunchAds.getDeviceData() if (deviceData != null) { promise.resolve(convertDeviceDataToMap(deviceData)) } else { promise.reject("DEVICE_ERROR", "SDK not initialized or device data unavailable", null) } } catch (e: Exception) { promise.reject("DEVICE_ERROR", "Failed to get device data: ${e.message}", e) } } } /** * Set GDPR consent */ @ReactMethod fun setGdprConsent(consent: String, promise: Promise) { try { BigCrunchAds.setGdprConsent(consent) promise.resolve(null) } catch (e: Exception) { promise.reject("PRIVACY_ERROR", "Failed to set GDPR consent: ${e.message}", e) } } /** * Set CCPA string */ @ReactMethod fun setCcpaString(ccpaString: String, promise: Promise) { try { BigCrunchAds.setCcpaString(ccpaString) promise.resolve(null) } catch (e: Exception) { promise.reject("PRIVACY_ERROR", "Failed to set CCPA string: ${e.message}", e) } } /** * Set COPPA compliant */ @ReactMethod fun setCoppaCompliant(isCompliant: Boolean, promise: Promise) { try { BigCrunchAds.setCoppaCompliant(isCompliant) promise.resolve(null) } catch (e: Exception) { promise.reject("PRIVACY_ERROR", "Failed to set COPPA compliance: ${e.message}", e) } } /** * Set account type */ @ReactMethod fun setAccountType(accountType: String, promise: Promise) { try { BigCrunchAds.setAccountType(accountType) promise.resolve(null) } catch (e: Exception) { promise.reject("ACCOUNT_ERROR", "Failed to set account type: ${e.message}", e) } } /** * Set debug mode */ @ReactMethod fun setDebugMode(enabled: Boolean, promise: Promise) { try { BigCrunchAds.setDebugMode(enabled) promise.resolve(null) } catch (e: Exception) { promise.reject("DEBUG_ERROR", "Failed to set debug mode: ${e.message}", e) } } /** * Add test device */ @ReactMethod fun addTestDevice(deviceId: String, promise: Promise) { try { BigCrunchAds.addTestDevice(deviceId) promise.resolve(null) } catch (e: Exception) { promise.reject("TEST_ERROR", "Failed to add test device: ${e.message}", e) } } /** * Remove test device */ @ReactMethod fun removeTestDevice(deviceId: String, promise: Promise) { try { BigCrunchAds.removeTestDevice(deviceId) promise.resolve(null) } catch (e: Exception) { promise.reject("TEST_ERROR", "Failed to remove test device: ${e.message}", e) } } /** * Get test devices */ @ReactMethod fun getTestDevices(promise: Promise) { try { val devices = BigCrunchAds.getTestDevices() promise.resolve(Arguments.fromList(devices)) } catch (e: Exception) { promise.reject("TEST_ERROR", "Failed to get test devices: ${e.message}", e) } } /** * Add event listener (required for NativeEventEmitter) */ @ReactMethod fun addListener(eventName: String) { // Keep track of listeners if needed // This is required for NativeEventEmitter to work properly } /** * Remove event listeners (required for NativeEventEmitter) */ @ReactMethod fun removeListeners(count: Int) { // Remove listeners if needed // This is required for NativeEventEmitter to work properly } /** * Send event to JavaScript */ private fun sendEvent(eventName: String, params: WritableMap?) { reactApplicationContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) } // Helper conversion methods private fun convertAppConfigToMap(config: AppConfig): WritableMap { val map = Arguments.createMap() map.putString("propertyId", config.propertyId) map.putString("appName", config.appName) val placements = Arguments.createArray() config.placements.forEach { placement -> placements.pushMap(convertPlacementConfigToMap(placement)) } map.putArray("placements", placements) return map } private fun convertPlacementConfigToMap(config: PlacementConfig): WritableMap { val map = Arguments.createMap() map.putString("placementId", config.placementId) map.putString("format", config.format) map.putString("gamAdUnit", config.gamAdUnit) // Add sizes array if available config.sizes?.let { sizes -> val sizesArray = Arguments.createArray() for (size in sizes) { val sizeMap = Arguments.createMap() sizeMap.putInt("width", size.width) sizeMap.putInt("height", size.height) sizesArray.pushMap(sizeMap) } map.putArray("sizes", sizesArray) } // Add refresh config if available config.refresh?.let { refresh -> val refreshMap = Arguments.createMap() refreshMap.putBoolean("enabled", refresh.enabled) refreshMap.putInt("intervalMs", refresh.intervalMs) refresh.maxRefreshes?.let { refreshMap.putInt("maxRefreshes", it) } map.putMap("refresh", refreshMap) } return map } private fun convertSessionInfoToMap(info: SessionInfo): WritableMap { val map = Arguments.createMap() map.putString("sessionId", info.sessionId) map.putString("userId", info.userId) map.putString("startTime", info.startTime) // Changed from toDouble() - now ISO 8601 string map.putBoolean("isNewUser", info.isNewUser) map.putInt("sessionDepth", info.sessionDepth) map.putInt("screenViewCount", info.screenViewCount) map.putInt("adRequestCount", info.adRequestCount) map.putInt("adImpressionCount", info.adImpressionCount) map.putDouble("totalRevenueMicros", info.totalRevenueMicros.toDouble()) return map } private fun convertDeviceDataToMap(data: DeviceData): WritableMap { val map = Arguments.createMap() map.putString("deviceId", data.deviceId) map.putString("deviceModel", data.deviceModel) map.putString("osVersion", data.osVersion) map.putString("appVersion", data.appVersion) map.putInt("screenWidth", data.screenWidth) map.putInt("screenHeight", data.screenHeight) map.putString("language", data.language) map.putString("country", data.country) map.putBoolean("isTablet", data.isTablet) data.carrier?.let { map.putString("carrier", it) } data.networkType?.let { map.putString("networkType", it) } return map } private fun convertMapToStringMap(map: ReadableMap): Map { val result = mutableMapOf() val iterator = map.keySetIterator() while (iterator.hasNextKey()) { val key = iterator.nextKey() map.getString(key)?.let { value -> result[key] = value } } return result } }