mobiler 0.44.0

Build mobile apps in Rust — one core, native UI on Android, iOS, and the web (CLI)
import FirebaseAnalytics
import FirebaseCore
import FirebaseCrashlytics
import Foundation
import SharedTypes

// analytics (free, bundled, EXPERIMENTAL). Product analytics + automatic crash capture via Firebase
// Analytics + Crashlytics. Fire-and-forget request/response (no stream). Crash capture is AUTOMATIC once
// Firebase is configured. Ops (input is JSON unless noted):
//   logEvent {name, params?}   setUserId <id>   setUserProperty {name, value}   setEnabled "true"|"false"
//   log <message>              recordError {message, domain?}                    testCrash ""  (dev only)
//
// `AnalyticsPlugin.bootstrap()` runs at launch (App.swift `// mobiler:app-launch`) and configures
// Firebase IF a GoogleService-Info.plist is bundled. Without it, the app does NOT crash — every op
// short-circuits to ok:false ("Firebase not configured") so the plugin is safe to add before config.
enum AnalyticsPlugin {
    // Launch hook — configure Firebase early so Crashlytics catches startup crashes. Guarded twice:
    // skip if already configured (push-firebase-only may have), and skip if no config file is present.
    static func bootstrap() {
        guard FirebaseApp.app() == nil else { return }
        guard Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist") != nil else { return }
        FirebaseApp.configure()
    }

    static func handle(op: String, input: String) async -> PluginResponse {
        guard FirebaseApp.app() != nil else {
            return PluginResponse(ok: false, output: "Firebase not configured — add GoogleService-Info.plist")
        }
        switch op {
        case "logEvent": return logEvent(input)
        case "setUserId":
            Analytics.setUserID(input.isEmpty ? nil : input)
            return PluginResponse(ok: true, output: "")
        case "setUserProperty": return setUserProperty(input)
        case "setEnabled": return setEnabled(input)
        case "log":
            Crashlytics.crashlytics().log(input)
            return PluginResponse(ok: true, output: "")
        case "recordError": return recordError(input)
        case "testCrash":
            // Deliberate crash to verify Crashlytics wiring — the report uploads on the NEXT launch.
            fatalError("analytics testCrash")
        default:
            return PluginResponse(ok: false, output: "unknown op '\(op)'")
        }
    }

    private static func logEvent(_ input: String) -> PluginResponse {
        guard let obj = json(input), let name = obj["name"] as? String else {
            return PluginResponse(ok: false, output: "expected {name, params?}")
        }
        let params = obj["params"] as? [String: Any]
        Analytics.logEvent(name, parameters: params)
        return PluginResponse(ok: true, output: "")
    }

    private static func setUserProperty(_ input: String) -> PluginResponse {
        guard let obj = json(input), let name = obj["name"] as? String else {
            return PluginResponse(ok: false, output: "expected {name, value}")
        }
        Analytics.setUserProperty(obj["value"] as? String, forName: name)
        return PluginResponse(ok: true, output: "")
    }

    private static func setEnabled(_ input: String) -> PluginResponse {
        let on = (input == "true")
        Analytics.setAnalyticsCollectionEnabled(on)
        Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(on)
        return PluginResponse(ok: true, output: "")
    }

    private static func recordError(_ input: String) -> PluginResponse {
        let obj = json(input)
        let message = (obj?["message"] as? String) ?? "error"
        let domain = (obj?["domain"] as? String) ?? "mobiler.analytics"
        let error = NSError(domain: domain, code: 0, userInfo: [NSLocalizedDescriptionKey: message])
        Crashlytics.crashlytics().record(error: error)
        return PluginResponse(ok: true, output: "")
    }

    private static func json(_ input: String) -> [String: Any]? {
        guard let data = input.data(using: .utf8),
              let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
        else { return nil }
        return obj
    }
}