diff --git a/README.md b/README.md index e83b29c..d30eac8 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ See the `Package.swift` files in the | `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` | ~20% | Singleton accessor, `installationID()` | `authTokenForcingRefresh`, `delete()`, `installationIDDidChangeNotification` | +| `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 thirteen 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) and `SkipFirebaseInstallations` are mostly stubs. +**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 @@ -55,7 +55,7 @@ See the `Package.swift` files in the - **`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`** is essentially a no-op beyond fetching the installation ID. +- **`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 diff --git a/Sources/SkipFirebaseInstallations/SkipFirebaseInstallations.swift b/Sources/SkipFirebaseInstallations/SkipFirebaseInstallations.swift index d9291e3..4a99c78 100644 --- a/Sources/SkipFirebaseInstallations/SkipFirebaseInstallations.swift +++ b/Sources/SkipFirebaseInstallations/SkipFirebaseInstallations.swift @@ -4,9 +4,27 @@ #if canImport(FirebaseInstallations) @_exported import FirebaseInstallations #elseif SKIP +import Foundation import SkipFirebaseCore import kotlinx.coroutines.tasks.await +public final class InstallationsAuthTokenResult { + public let platformValue: com.google.firebase.installations.InstallationTokenResult + + public init(platformValue: com.google.firebase.installations.InstallationTokenResult) { + self.platformValue = platformValue + } + + public var authToken: String { + platformValue.token + } + + public var expirationDate: Date { + // tokenExpirationTimestamp is Unix time in seconds + Date(timeIntervalSince1970: Double(platformValue.tokenExpirationTimestamp)) + } +} + public final class Installations { public let installations: com.google.firebase.installations.FirebaseInstallations @@ -25,6 +43,18 @@ public final class Installations { public func installationID() async throws -> String { try await self.installations.getId().await() } + + public func authToken() async throws -> InstallationsAuthTokenResult { + InstallationsAuthTokenResult(platformValue: try await self.installations.getToken(false).await()) + } + + public func authTokenForcingRefresh(_ forceRefresh: Bool) async throws -> InstallationsAuthTokenResult { + InstallationsAuthTokenResult(platformValue: try await self.installations.getToken(forceRefresh).await()) + } + + public func delete() async throws { + try await self.installations.delete().await() + } } #endif #endif diff --git a/Tests/SkipFirebaseInstallationsTests/SkipFirebaseInstallationsTests.swift b/Tests/SkipFirebaseInstallationsTests/SkipFirebaseInstallationsTests.swift index bd1beca..b9150ef 100644 --- a/Tests/SkipFirebaseInstallationsTests/SkipFirebaseInstallationsTests.swift +++ b/Tests/SkipFirebaseInstallationsTests/SkipFirebaseInstallationsTests.swift @@ -18,6 +18,11 @@ let logger: Logger = Logger(subsystem: "SkipFirebaseInstallationsTests", categor if false { let installations: Installations = Installations.installations() let _: String = try await installations.installationID() + let tokenResult: InstallationsAuthTokenResult = try await installations.authToken() + let _: String = tokenResult.authToken + let _: Date = tokenResult.expirationDate + let _: InstallationsAuthTokenResult = try await installations.authTokenForcingRefresh(true) + try await installations.delete() } } }