package com.freshworks.freshdesk.reactnative import android.content.pm.PackageManager import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.freshworks.sdk.freshdesk.FreshdeskSDK internal object FreshdeskDiagnostics { fun buildFallbackReport( context: android.content.Context, isInitialized: Boolean, initCallbackReceived: Boolean, configuredHost: String?, normalizedHost: String?, detectedExternalNativeInit: Boolean, nativeSdkReady: Boolean, ): WritableMap { val checks = Arguments.createArray() checks.pushMap( check( id = "runtime.nativeModule", status = "pass", details = "FreshdeskReactNative native module is registered", fixHint = "No action required.", ), ) val sdkInitializedStatus = when { !isInitialized -> "fail" !nativeSdkReady -> "warn" !initCallbackReceived && isInitialized -> "warn" else -> "pass" } checks.pushMap( check( id = "runtime.sdkInitialized", status = sdkInitializedStatus, details = when { !isInitialized -> "FreshdeskSDK.initialize has not completed" !nativeSdkReady -> "Bridge marked initialized but native SDK is not ready" !initCallbackReceived -> "Native SDK is ready but init callback was not received" else -> "FreshdeskSDK.initialize completed successfully" }, fixHint = when { !isInitialized -> "Call FreshdeskSDK.initialize({ token, host, sdkId }) in JavaScript before running diagnostics." !nativeSdkReady -> "Wait for initialize() to resolve. If it timed out, verify token, host, sdkId, and network access." !initCallbackReceived -> "No action required if openSupport works. Otherwise re-run initialize() after confirming credentials." else -> "No action required." }, ), ) if (!configuredHost.isNullOrBlank()) { val bareHost = FreshdeskHostUtils.isBareHost(configuredHost) checks.pushMap( check( id = "config.hostScheme", status = if (bareHost) "warn" else "pass", details = if (bareHost) { "Host was provided without https:// ($configuredHost); normalized to $normalizedHost" } else { "Host includes a URL scheme ($configuredHost)" }, fixHint = if (bareHost) { "Bare domains are auto-normalized. Prefer https://yourcompany.freshdesk.com in .env for clarity." } else { "No action required." }, ), ) } if (detectedExternalNativeInit) { checks.pushMap( check( id = "runtime.doubleInit", status = "warn", details = "Native FreshdeskSDK was initialized outside the React Native bridge", fixHint = "Remove MainApplication.onCreate() native init. Use JS initialize() for in-app support; native init only in FirebaseMessagingService for headless push.", ), ) } if (isInitialized && nativeSdkReady) { checks.pushMap( check( id = "runtime.sdkVersion", status = "pass", details = "SDK version ${FreshdeskSDK.getSDKVersionName()}", fixHint = "No action required.", ), ) } val hasMessagingService = hasFreshdeskMessagingService(context) checks.pushMap( check( id = "push.messagingService", status = if (hasMessagingService) "pass" else "warn", details = if (hasMessagingService) { "A Firebase messaging service is declared in AndroidManifest.xml" } else { "No Freshdesk Firebase messaging service found in AndroidManifest.xml" }, fixHint = if (hasMessagingService) { "No action required." } else { "If you need push notifications, register a FirebaseMessagingService that forwards tokens and messages to FreshdeskSDK (see sample_app/android)." }, ), ) checks.pushMap( check( id = "runtime.diagnostics", status = "skipped", details = "Structured native diagnostics are not available on Android yet", fixHint = "Initialize with debugMode: true and inspect Logcat for Freshdesk SDK logs until Android native diagnostics parity lands.", ), ) val overallStatus = deriveOverallStatus(checks) val prettyPrinted = formatPrettyPrinted(overallStatus, checks) val newArchitecture = isNewArchitecture() return Arguments.createMap().apply { putString("platform", "android") putString("overallStatus", overallStatus) putArray("checks", checks) putString("prettyPrinted", prettyPrinted) putString("architecture", if (newArchitecture) "new" else "old") putBoolean("turboModule", newArchitecture) } } private fun isNewArchitecture(): Boolean = try { Class.forName("com.freshworks.freshdesk.NativeFreshdeskSpec") true } catch (_: Throwable) { false } private fun hasFreshdeskMessagingService(context: android.content.Context): Boolean { return try { val packageInfo = context.packageManager.getPackageInfo( context.packageName, PackageManager.GET_SERVICES, ) packageInfo.services?.any { service -> service.name.contains("MessagingService", ignoreCase = true) || service.name.contains("Freshdesk", ignoreCase = true) } == true } catch (_: Exception) { false } } private fun check( id: String, status: String, details: String, fixHint: String, ): WritableMap { return Arguments.createMap().apply { putString("id", id) putString("status", status) putString("details", details) putString("fixHint", fixHint) } } // WritableArray.getMap() returns a non-null ReadableMap on RN 0.75 but is // nullable on newer RN; the `?: continue` guards keep this compiling on both, // so the "useless elvis" warning is expected in these two loops. @Suppress("USELESS_ELVIS") private fun deriveOverallStatus(checks: WritableArray): String { var hasFail = false var hasWarn = false var hasPass = false for (index in 0 until checks.size()) { val item = checks.getMap(index) ?: continue when (item.getString("status")) { "fail" -> hasFail = true "warn" -> hasWarn = true "pass" -> hasPass = true } } return when { hasFail -> "fail" hasWarn -> "warn" hasPass -> "pass" else -> "skipped" } } @Suppress("USELESS_ELVIS") // see deriveOverallStatus: getMap() nullability varies by RN version private fun formatPrettyPrinted(overallStatus: String, checks: WritableArray): String { val lines = mutableListOf( "Freshdesk diagnostics (android) — overall: $overallStatus", "", ) for (index in 0 until checks.size()) { val item = checks.getMap(index) ?: continue val status = item.getString("status")?.uppercase() ?: "SKIPPED" val id = item.getString("id") ?: "unknown" lines.add("[$status] $id") item.getString("details")?.takeIf { it.isNotEmpty() }?.let { lines.add(" details: $it") } item.getString("fixHint")?.takeIf { it.isNotEmpty() }?.let { lines.add(" fix: $it") } lines.add("") } return lines.joinToString("\n").trimEnd() } }