Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ let skipstone = [Target.PluginUsage.plugin(name: "skipstone", package: "skip")]
let package = Package(
name: "skip-firebase",
defaultLocalization: "en",
platforms: [.iOS(.v17), .macOS(.v14), .tvOS(.v17), .watchOS(.v10), .macCatalyst(.v17)],
platforms: [.iOS(.v16), .macOS(.v13), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v16)],
products: [
.library(name: "SkipFirebaseCore", targets: ["SkipFirebaseCore"]),
.library(name: "SkipFirebaseFirestore", targets: ["SkipFirebaseFirestore"]),
Expand Down
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ See the `Package.swift` files in the
| `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, `RemoteMessage` → `UNNotification` 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` |
| `SkipFirebaseRemoteConfig` | ~75% | `fetch`/`activate`/`fetchAndActivate`, `ensureInitialized`, real-time config updates (`addOnConfigUpdateListener`), value retrieval, keys-by-prefix, `RemoteConfigSettings` (intervals + timeout), defaults, `RemoteConfigValue` (string/bool/number/data/json/source) | `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` | ~75% | Default/regional/emulator instance, `httpsCallable(_:)` and `httpsCallable(_:options:)` (name and URL-based), `HTTPSCallableOptions` with `requireLimitedUseAppCheckTokens`, `async`/`await` `call`, completion-based `call`, `timeoutInterval`, automatic Kotlin→Swift result conversion via `deepSwift` | Streaming RPCs (`stream`), `Callable<Request, Response>` Codable wrapper, `customDomain`-based factory |
Expand All @@ -54,7 +54,7 @@ See the `Package.swift` files in the
- **`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.
- **`Codable` decoding API differs by platform** — on Android use `snapshot.decoded()` with explicit type annotation (`let m: MyModel = try snapshot.decoded()`); on iOS use `FirebaseFirestoreSwift`'s `snapshot.data(as: MyModel.self)`. The `setData(from:)` encoding API works identically on both platforms.
- **`SkipFirebaseFunctions`** does not support streaming RPCs (`stream`). The Firebase Android SDK has no SSE/streaming equivalent to the iOS `AsyncThrowingStream`-based `stream` API — Android Functions calls are always one-shot request/response. Any code that iterates over `.stream(...)` on iOS will not compile on Android. The `Callable<Request, Response>` Codable convenience wrapper is also not bridged; use `call(_ data: Any?)` directly and decode the result manually on Android.
- **`SkipFirebaseRemoteConfig`** does not yet expose `addOnConfigUpdateListener` (real-time config updates) or custom signals.
- **`SkipFirebaseRemoteConfig`** does not yet expose 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).

Expand Down Expand Up @@ -193,6 +193,27 @@ public actor Model {
}
```

## Remote Config

`SkipFirebaseRemoteConfig` supports fetching values manually and listening for real-time Remote Config updates. Realtime updates are delivered through `addOnConfigUpdateListener`; activate the fetched values in the callback when your UI can safely apply the change.

```swift
import SkipFirebaseRemoteConfig

let remoteConfig = RemoteConfig.remoteConfig()
let registration = remoteConfig.addOnConfigUpdateListener { update, error in
guard error == nil else { return }

Task {
_ = try? await remoteConfig.activate()
// Read updated values with configValue(forKey:) here.
}
}

// Later, when the listener is no longer needed:
registration.remove()
```

## Messaging

After [setting up](#setup) your app to use Firebase, enabling push notifications via Firebase Cloud Messaging (FCM) requires a number of additional steps.
Expand Down
64 changes: 64 additions & 0 deletions Sources/SkipFirebaseRemoteConfig/SkipFirebaseRemoteConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ public final class RemoteConfig {
let _: com.google.firebase.remoteconfig.FirebaseRemoteConfigInfo = remoteconfig.ensureInitialized().await()
}

/// Adds a listener that receives real-time Remote Config updates.
/// https://firebase.google.com/docs/reference/swift/firebaseremoteconfig/api/reference/Classes/RemoteConfig#addonconfigupdatelistener(remoteconfigupdatecompletion:)
public func addOnConfigUpdateListener(
remoteConfigUpdateCompletion listener: @escaping (RemoteConfigUpdate?, Error?) -> Void
) -> ConfigUpdateListenerRegistration {
let androidListener = RemoteConfigUpdateListener(listener: listener)
let registration = remoteconfig.addOnConfigUpdateListener(androidListener)
return ConfigUpdateListenerRegistration(registration: registration)
}

// MARK: - Get values

public func configValue(forKey key: String?) -> RemoteConfigValue {
Expand Down Expand Up @@ -139,6 +149,60 @@ public final class RemoteConfig {
}
}

// MARK: - Real-time Updates

public final class RemoteConfigUpdate {
public let platformValue: com.google.firebase.remoteconfig.ConfigUpdate

public init(platformValue: com.google.firebase.remoteconfig.ConfigUpdate) {
self.platformValue = platformValue
}

/// The parameter keys that changed in this update.
public var updatedKeys: Set<String> {
var result = Set<String>()
for key in platformValue.getUpdatedKeys() {
result.insert(key)
}
return result
}
}

public class ConfigUpdateListenerRegistration: KotlinConverting<com.google.firebase.remoteconfig.ConfigUpdateListenerRegistration> {
public let registration: com.google.firebase.remoteconfig.ConfigUpdateListenerRegistration

public init(registration: com.google.firebase.remoteconfig.ConfigUpdateListenerRegistration) {
self.registration = registration
}

// SKIP @nooverride
public override func kotlin(nocopy: Bool = false) -> com.google.firebase.remoteconfig.ConfigUpdateListenerRegistration {
registration
}

/// Stops receiving real-time Remote Config updates for this listener.
public func remove() {
registration.remove()
}
}

// SKIP @nobridge
private final class RemoteConfigUpdateListener: com.google.firebase.remoteconfig.ConfigUpdateListener {
private let listener: (RemoteConfigUpdate?, Error?) -> Void

init(listener: @escaping (RemoteConfigUpdate?, Error?) -> Void) {
self.listener = listener
}

public override func onUpdate(configUpdate: com.google.firebase.remoteconfig.ConfigUpdate) {
listener(RemoteConfigUpdate(platformValue: configUpdate), nil)
}

public override func onError(error: com.google.firebase.remoteconfig.FirebaseRemoteConfigException) {
listener(nil, ErrorException(error))
}
}

// MARK: - RemoteConfigValue

public final class RemoteConfigValue {
Expand Down
Loading