Skip to content

skiptools/skip-firebase

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

232 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SkipFirebase

This package provides Firebase support for Skip Swift projects. The Swift side uses the official Firebase iOS SDK directly, with the various SkipFirebase* modules passing the transpiled calls through to the Firebase Android SDK.

For an example of using Firebase in a Skip Fuse app, see the FiresideFuse Sample. For a Skip Lite app, see the Fireside Sample.

Package

The modules in the SkipFirebase framework project mirror the division of the SwiftPM modules in the Firebase iOS SDK (at https://github.com/firebase/firebase-ios-sdk.git), which is also mirrored in the division of the Firebase Kotlin Android gradle modules (at https://github.com/firebase/firebase-android-sdk.git).

See the Package.swift files in the FiresideFuse and Fireside apps for examples of integrating Firebase dependencies.

Module Coverage

skip-firebase exposes Swift API parity with the Firebase iOS SDK for the most common use cases on Android. The Android-side implementation lives in each Sources/SkipFirebase* Swift file (transpiled to Kotlin) and forwards calls to the official Firebase Android SDK. The table below summarizes, per module, the approximate share of the iOS API surface that has been bridged, along with the major gaps.

Module iOS API Coverage Highlights Notable Gaps
SkipFirebaseCore ~80% FirebaseApp.configure (default + named + custom FirebaseOptions), app lifecycle, Timestamp, deep Kotlin↔Swift value conversion helpers Custom FirebaseLogger, advanced data-collection toggles
SkipFirebaseFirestore ~80% Collections, documents, batched writes, queries, Filter.and/.or composition, FieldPath, aggregate queries (count/average/sum), snapshot listeners, FieldValue sentinels, LoadBundleTaskProgress, errors mapped to NSError with FirestoreErrorDomain/FirestoreErrorCode runTransaction (Transaction is currently a passthrough wrapper), Codable document encoding/decoding, GeoPoint, FirestoreSettings/cache configuration, InputStream-based bundle loading
SkipFirebaseAuth ~60% Email/password sign-in, anonymous sign-in, email-link sign-in, interactive OAuth provider sign-in (Activity-based), state-change listener, ID-token retrieval, profile changes, account linking, reauthentication, fetchSignInMethods, ActionCodeSettings, partial AuthErrorCode mapping Phone auth (PhoneAuthProvider, SMS verification), multi-factor (MultiFactor/MultiFactorResolver), applyActionCode/checkActionCode/confirmPasswordReset/verifyPasswordResetCode, custom-token sign-in, GameCenterAuthProvider, language/tenant configuration, updateEmail/updatePassword
SkipFirebaseStorage ~80% Bucket and reference resolution, upload (putFile/putData in both callback and async forms), download (getData/write(toFile:)), StorageMetadata read/write, downloadURL, delete, pause/resume/cancel on uploads, list/listAll with pagination (pageToken), live Progress snapshot observers on uploads and file downloads Full StorageError code mapping, putStream/putString
SkipFirebaseMessaging ~70% FCM token retrieval + auto-refresh, topic subscribe/unsubscribe, MessagingDelegate, MessagingService integrating with iOS-style UNUserNotificationCenterDelegate, intent routing, localized loc_key/loc_args title/body, RemoteMessageUNNotification translation Per-sender FCM token APIs and notification-service-extension helpers (populateNotificationContent, exportDeliveryMetricsToBigQuery) are stubbed out as @available(*, unavailable)
SkipFirebaseAnalytics ~70% logEvent, user properties/ID, consent (setConsent with ConsentType/ConsentStatus), session ID, setSessionTimeoutInterval, setDefaultEventParameters, appInstanceID, all standard event/parameter/user-property name constants, SwiftUI .analyticsScreen modifier initiateOnDeviceConversionMeasurement (email/phone variants), handleEvents(forSession:), deep-link helpers
SkipFirebaseRemoteConfig ~70% fetch/activate/fetchAndActivate, ensureInitialized, value retrieval, keys-by-prefix, RemoteConfigSettings (intervals + timeout), defaults, RemoteConfigValue (string/bool/number/data/json/source) addOnConfigUpdateListener (real-time config updates), setCustomSignals
SkipFirebaseAppCheck ~60% Token retrieval (token(forcingRefresh:), limitedUseToken), token-change listener bridged through NotificationCenter, debug provider factory, AppCheckErrorCode iOS-only DeviceCheckProviderFactory/AppAttestProviderFactory (no Android equivalent), custom AppCheckProvider implementations
SkipFirebaseCrashlytics ~75% log, setCustomValue/setCustomKeysAndValues, setUserID, didCrashDuringPreviousExecution, checkForUnsentReports/sendUnsentReports/deleteUnsentReports, record(error:userInfo:), recordExceptionModel, FIRExceptionModel/FIRStackFrame logWithFormat and checkAndUpdateUnsentReports are marked @available(*, unavailable); no per-FirebaseApp instance accessor
SkipFirebaseFunctions ~45% Default/regional/emulator instance, httpsCallable(_:), completion-based call, automatic Kotlin→Swift result conversion via deepSwift async/await call signatures, streaming RPCs, per-call options (HTTPSCallableOptions), explicit call timeouts
SkipFirebaseDatabase ~5% Database.database() / Database.database(app:) singleton accessors Effectively the entire API: DatabaseReference, child/push/setValue/updateChildValues/removeValue, observe/observeSingleEvent, queries, DataSnapshot, ServerValue, online/offline toggle
SkipFirebaseInstallations ~75% Singleton accessor (installations()/installations(app:)), installationID(), authToken(), authTokenForcingRefresh(_:), delete(), InstallationsAuthTokenResult (authToken, expirationDate) installationIDDidChangeNotification, per-FirebaseApp notification keys
SkipFirebasePerformance ~65% Performance.sharedInstance(), isDataCollectionEnabled, trace(name:), HTTPMetric(url:httpMethod:), full Trace API (start/stop, incrementMetric, valueForMetric, attributes), full HTTPMetric API (start/stop, responseCode, payload sizes, content type, attributes), HTTPMethod enum isInstrumentationEnabled (no Android equivalent, marked unavailable), Performance.startTrace(name:) static convenience, per-FirebaseApp instance accessor

Overall coverage across the fourteen modules: roughly 60% of the iOS API surface. The most production-used modules — Core, Firestore, Auth, Storage, Messaging, Analytics, Crashlytics, and RemoteConfig — sit in the 60–80% range and cover the standard read/write/sign-in/log/notify paths. SkipFirebaseDatabase (Realtime Database) is mostly a stub.

What is working well

  • Firestore is the most complete module: all common CRUD, queries (including Filter combinators and FieldPath predicates), snapshot listeners (with MetadataChanges support), batched writes, aggregate queries, and FieldValue sentinels round-trip correctly between Swift and the Firestore Android SDK. Errors are mapped to NSError with FirestoreErrorDomain/FirestoreErrorCode so error handling looks the same on both platforms.
  • Auth covers the most common sign-in flows (email/password, anonymous, interactive OAuth via the system browser, email-link) and exposes a familiar User API with profile changes, account linking, reauthentication, ID-token retrieval, and a state-change listener.
  • Messaging handles the full FCM lifecycle: token retrieval and auto-refresh, topic subscribe/unsubscribe, RemoteMessageUNNotification translation (including localized loc_key/loc_args substitution from the app's Localizable.strings), and a MessagingService that integrates with iOS-style UNUserNotificationCenterDelegate.
  • Storage supports both data-based and file-based uploads/downloads in callback and async forms, plus full StorageMetadata read/write.
  • Analytics, RemoteConfig, AppCheck, and Crashlytics all expose the workhorse APIs that apps need day-to-day, including a SwiftUI .analyticsScreen modifier and a NotificationCenter-backed AppCheck token-change listener.

What still needs to be implemented

  • Realtime Database (SkipFirebaseDatabase) is currently only a stub. DatabaseReference, observe/observeSingleEvent, queries, and writes have not been bridged. Apps that rely on the Realtime Database should use Firestore instead or contribute the missing wrappers.
  • Phone-number authentication and multi-factor flows in SkipFirebaseAuth are not bridged. Apps requiring SMS verification or MFA need to drop into #if SKIP blocks and call the Android Firebase SDK directly.
  • runTransaction in Firestore is not implemented — the Transaction class is currently a passthrough wrapper with no operations. Multi-document atomic reads-then-writes need to be expressed as WriteBatch commits or as Kotlin-side code today.
  • SkipFirebaseFunctions lacks async/await call signatures, streaming RPCs, and per-call options. Only completion-based callback calls are supported.
  • SkipFirebaseRemoteConfig does not yet expose addOnConfigUpdateListener (real-time config updates) or custom signals.
  • SkipFirebaseAppCheck ships only the Debug provider factory; custom provider implementations are not yet bridgeable.
  • SkipFirebaseInstallations cannot bridge the InstallationIDDidChange notification or the InstallationIDDidChangeAppNameKey userInfo key. The Firebase Android SDK has no equivalent push-notification mechanism for installation ID changes — it exposes no broadcast, callback, or listener that fires when the ID rotates. As a result, any code that observes NotificationCenter.default.publisher(for: .InstallationIDDidChange) on iOS will compile on Android but never receive an event. If your app relies on this to, for example, re-upload the installation ID to your backend after it changes, you will need an alternative strategy on Android (such as fetching the ID on every app foreground, or polling after Firebase SDK upgrades).

Firebase modules not yet wrapped

The following Firebase iOS SDK products do not yet have a SkipFirebase* counterpart in this package: FirebaseABTesting, FirebaseAI / Vertex AI, FirebaseAppDistribution, FirebaseDataConnect, FirebaseDynamicLinks (deprecated upstream), FirebaseInAppMessaging, and FirebaseMLModelDownloader. Pull requests adding any of these are welcome.

Setup

For a Skip app, the simplest way to setup Firebase support is to create a Firebase project at https://console.firebase.google.com/project. Follow the Firebase setup instructions to obtain the GoogleService-Info.plist and google-services.json files and add them to the iOS and Android sides of the project:

  • The GoogleService-Info.plist file should be placed in the Darwin/ folder of the Skip project
  • The google-services.json file should be placed in the Android/app/ folder of the Skip project

In addition, the com.google.gms.google-services plugin will need to be added to the Android app's Android/app/build.gradle.kts file in order to process the google-services.json file for the app, like so:

plugins {
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.android.application)
    id("skip-build-plugin")
    id("com.google.gms.google-services") version "4.4.4" apply true
    id("com.google.firebase.crashlytics") version "3.0.2" apply true # (if using Crashlytics)
}

For concrete examples, see the FireSideFuse Sample project. {: class="callout info"}

Once Firebase has been added to your project, you need to configure the FirebaseApp on app startup. This is typically done in the onInit() callback of the *AppDelegate in your *App.swift file. Here is a snippet from the FireSideFuse sample app:

import SkipFirebaseCore
...

/* SKIP @bridge */public final class FireSideFuseAppDelegate : Sendable {
    /* SKIP @bridge */public static let shared = FireSideFuseAppDelegate()

    ...

    /* SKIP @bridge */public func onInit() {
        logger.debug("onInit")

        FirebaseApp.configure()
        ...
    }

    ...
}

After configuring the FirebaseApp, you will be able to access the singleton type for each of the imported Firebase modules. For example, the following actor uses the Firestore singleton:

// Sources/FireSideFuse/FireSideFuseApp.swift

import SkipFirebaseFirestore

...

public actor Model {
    /// The shared model singleton
    public static let shared = Model()

    private let firestore: Firestore

    private init() {
        self.firestore = Firestore.firestore()
    }
    
    public func queryData() async throws -> [DataModel] { ... }
    public func saveData(model: DataModel) async throws { ... }

    ...
}

Messaging

After setting up your app to use Firebase, enabling push notifications via Firebase Cloud Messaging (FCM) requires a number of additional steps.

  1. Follow Firebase's instructions for creating and uploading your Apple Push Notification Service (APNS) key.

  2. Use Xcode to add the Push capability to your iOS app.

  3. Add Skip's Firebase messaging service and default messaging channel to Android/app/src/main/AndroidManifest.xml:

    ...
    <application ...>
        ...
        <service
            android:name="skip.firebase.messaging.MessagingService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_channel_id"
            android:value="tools.skip.firebase.messaging" />
    </application>
  4. Consider increasing the minSdk version of your Android app. Prior to SDK 33, Android does not provide any control over asking the user for push notification permissions. Rather, the system will prompt the user for permission only after receiving a notification and opening the app. Increasing your minSdk will allow you to decide when to request notification permissions. To do so, edit your Android/app/build.gradle.kts file and change the minSdk value to 33.

  5. Define a delegate to receive notification callbacks. In keeping with Skip's philosophy of transparent adoption, both the iOS and Android sides of your app will receive callbacks via iOS's standard UNUserNotificationCenterDelegate API, as well as the Firebase iOS SDK's MessagingDelegate. Here are example Skip Fuse delegate implementations that works across both platforms:

import SwiftFuseUI
import SkipFirebaseMessaging

final class NotificationDelegate : NSObject, UNUserNotificationCenterDelegate, Sendable {
    public func requestPermission() {
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        Task { @MainActor in
            do {
                if try await UNUserNotificationCenter.current().requestAuthorization(options: authOptions) {
                    logger.info("notification permission granted")
                } else {
                    logger.info("notification permission denied")
                }
            } catch {
                logger.error("notification permission error: \(error)")
            }
        }
    }

    public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
        let content = notification.request.content
        logger.info("willPresentNotification: \(content.title): \(content.body) \(content.userInfo)")
        return [.banner, .sound]
    }

    public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
        let content = response.notification.request.content
        logger.info("didReceiveNotification: \(content.title): \(content.body) \(content.userInfo)")
        #if os(Android) || !os(macOS)
        // Example of using a deep_link key passed in the notification to route to the app's `onOpenURL` handler
        if let deepLink = response.notification.request.content.userInfo["deep_link"] as? String, let url = URL(string: deepLink) {
            Task { @MainActor in
                await UIApplication.shared.open(url)
            }
        }
        #endif
    }
}

// Your Firebase MessageDelegate must bridge because we use the Firebase Kotlin API on Android.
/* SKIP @bridge */final class MessageDelegate : NSObject, MessagingDelegate, Sendable {
    /* SKIP @bridge */public func messaging(_ messaging: Messaging, didReceiveRegistrationToken token: String?) {
        logger.info("didReceiveRegistrationToken: \(token ?? "nil")")
    }
}
  1. Wire everything up. This includes assigning your shared delegate, registering for remote notifications, and other necessary steps. Below we build on our previous Firebase setup code to perform these actions. This is taken from our FireSideFuse sample app:
// Sources/FireSideFuse/FireSideFuseApp.swift

import SkipFirebaseCore

...

/* SKIP @bridge */public final class FireSideFuseAppDelegate : Sendable {
    /* SKIP @bridge */public static let shared = FireSideFuseAppDelegate()

    private let notificationDelegate = NotificationDelegate()
    private let messageDelegate = MessageDelegate()

    private init() {
    }

    /* SKIP @bridge */public func onInit() {
        logger.debug("onInit")

        // Configure Firebase and notifications
        FirebaseApp.configure()
        Messaging.messaging().delegate = messageDelegate
        UNUserNotificationCenter.current().delegate = notificationDelegate
    }

    /* SKIP @bridge */public func onLaunch() {
        logger.debug("onLaunch")
        // Ask for permissions at a time appropriate for your app
        notificationDelegate.requestPermission()
    }

    ...
}
// Darwin/Sources/Main.swift

...

class AppMainDelegate: NSObject, AppMainDelegateBase {
    ...

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        AppDelegate.shared.onLaunch()
        application.registerForRemoteNotifications() // <-- Insert
        return true
    }

    ...
}
// Android/app/src/main/kotlin/.../Main.kt

...

open class MainActivity: AppCompatActivity {
    ...

    override fun onCreate(savedInstanceState: android.os.Bundle?) {
        ...

        setContent {
            ...
        }

        skip.firebase.messaging.Messaging.messaging().onActivityCreated(this) // <-- Insert
        FireSideFuseAppDelegate.shared.onLaunch()

        ...
    }

    ...
}
  1. See Firebase's iOS instructions and Android instructions for additional details and options, including how to send test messages to your apps!

The FiresideFuse and Fireside projects are great references for seeing complete, working Skip Fuse and Skip Lite apps using Firebase push notifications. {: class="callout info"}

Error handling

Firestore

The Firestore API converts com.google.firebase.firestore.FirebaseFirestoreException to NSError so you can handle errors the same way on both platforms:

do {
    try await Firestore.firestore().collection("foo").document("bar").updateData(...)
} catch let error as NSError {
    if error.domain == FirestoreErrorDomain && error.code == FirestoreErrorCode.notFound.rawValue {
        ...
    }
}

Catching other errors

Other parts of this library have not been updated to this unified error handling. Instead, you can access the underlying Kotlin exceptions in SKIP blocks according to the documentation:

do {
    try await Storage.storage().reference().child("nonexistent").delete()
} catch {
    #if !SKIP
    let error = error as NSError
    let errorCode = error.domain == StorageError.errorDomain ? error.code : nil
    #else
    let exception = (error as Exception).cause as? com.google.firebase.storage.StorageException
    let errorCode = exception?.code.value()
    #endif
    if errorCode == -13010 { 
        // Object not found
    }
}

Testing

For unit testing, where there isn't a standard place to store the GoogleService-Info.plist and google-services.json configuration files, you can create an configure the app using the SkipFirebaseCore.FirebaseApp API manually from the information provided from the Firebase console, like so:

import SkipFirebaseCore
import SkipFirebaseAuth
import SkipFirebaseStorage
import SkipFirebaseDatabase
import SkipFirebaseAppCheck
import SkipFirebaseFunctions
import SkipFirebaseFirestore
import SkipFirebaseMessaging
import SkipFirebaseCrashlytics
import SkipFirebaseRemoteConfig
import SkipFirebaseInstallations
import SkipFirebasePerformance

let appName = "myapp"
let options = FirebaseOptions(googleAppID: "1:GCM:ios:HASH", gcmSenderID: "GCM")
options.projectID = "some-firebase-projectid"
options.storageBucket = "some-firebase-demo.appspot.com"
options.apiKey = "some-api-key"

FirebaseApp.configure(name: appName, options: options)
guard let app = FirebaseApp.app(name: appName) else {
    fatalError("Cannot load Firebase config")
}

// customize the app here
app.isDataCollectionDefaultEnabled = false

// use the app to create and test services
let auth = Auth.auth(app: app)
let storage = Storage.storage(app: app)
let database = Database.database(app: app)
let appcheck = AppCheck.appCheck(app: app)
let functions = Functions.functions(app: app)
let firestore = Firestore.firestore(app: app)
let crashlytics = Crashlytics.crashlytics(app: app)
let remoteconfig = RemoteConfig.remoteConfig(app: app)
let installations = Installations.installations(app: app)

Common Errors

Error in adb logcat: FirebaseApp: Default FirebaseApp failed to initialize because no default options were found.
This usually means that com.google.gms:google-services was not applied to your gradle project.

The app's com.google.gms:google-services plugin must be applied to the build.gradle.kts file for the app's target.

License

This software is licensed under the Mozilla Public License 2.0.

About

Firebase integration for Skip apps

Resources

License

Contributing

Stars

Watchers

Forks

Sponsor this project

Contributors

Languages