Skip to content
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ See the `Package.swift` files in the
| `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 |
| `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 |
| `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 |
Expand All @@ -52,7 +52,7 @@ See the `Package.swift` files in the
- **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.
- **`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.
- **`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
36 changes: 36 additions & 0 deletions Sources/SkipFirebaseFunctions/SkipFirebaseFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ public typealias Timestamp = SkipFirebaseCore.Timestamp
// https://firebase.google.com/docs/reference/swift/firebasefunctions/api/reference/Classes/Functions
// https://firebase.google.com/docs/reference/android/com/google/firebase/functions/FirebaseFunctions

public final class HTTPSCallableOptions {
public let requireLimitedUseAppCheckTokens: Bool

public init(requireLimitedUseAppCheckTokens: Bool) {
self.requireLimitedUseAppCheckTokens = requireLimitedUseAppCheckTokens
}

var androidOptions: com.google.firebase.functions.HttpsCallableOptions {
let builder = com.google.firebase.functions.HttpsCallableOptions.Builder()
builder.limitedUseAppCheckTokens = requireLimitedUseAppCheckTokens
return builder.build()
}
}

public final class Functions {
public let functions: com.google.firebase.functions.FirebaseFunctions

Expand Down Expand Up @@ -43,6 +57,18 @@ public final class Functions {
public func httpsCallable(_ name: String) -> HTTPSCallable {
HTTPSCallable(functions.getHttpsCallable(name))
}

public func httpsCallable(_ name: String, options: HTTPSCallableOptions) -> HTTPSCallable {
HTTPSCallable(functions.getHttpsCallable(name, options.androidOptions))
}

public func httpsCallable(_ url: URL) -> HTTPSCallable {
HTTPSCallable(functions.getHttpsCallableFromUrl(java.net.URL(url.absoluteString)))
}

public func httpsCallable(_ url: URL, options: HTTPSCallableOptions) -> HTTPSCallable {
HTTPSCallable(functions.getHttpsCallableFromUrl(java.net.URL(url.absoluteString), options.androidOptions))
}
}

public class HTTPSCallable: KotlinConverting<com.google.firebase.functions.HttpsCallableReference> {
Expand All @@ -65,6 +91,16 @@ public class HTTPSCallable: KotlinConverting<com.google.firebase.functions.Https
lhs.platformValue == rhs.platformValue
}

public var timeoutInterval: TimeInterval {
get { Double(platformValue.timeout) / 1000.0 }
set { platformValue.setTimeout(Int64(newValue * 1000), java.util.concurrent.TimeUnit.MILLISECONDS) }
}

public func call(_ data: Any? = nil) async throws -> HTTPSCallableResult {
let task = data == nil ? platformValue.call() : platformValue.call(data!.kotlin())
return HTTPSCallableResult(try await task.await())
}

public func call(_ data: Any? = nil,
completion: @escaping (HTTPSCallableResult?,
Error?) -> Void) {
Expand Down
20 changes: 20 additions & 0 deletions Tests/SkipFirebaseFunctionsTests/SkipFirebaseFunctionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ let logger: Logger = Logger(subsystem: "SkipFirebaseFunctionsTests", category: "
if false {
let _: Functions = Functions.functions()
let _: Functions = Functions.functions(region: "europe-west4")

let options = HTTPSCallableOptions(requireLimitedUseAppCheckTokens: true)
let _: Bool = options.requireLimitedUseAppCheckTokens

let functions = Functions.functions()
let callable: HTTPSCallable = functions.httpsCallable("myFunc")
let callableWithOptions: HTTPSCallable = functions.httpsCallable("myFunc", options: options)
let urlCallable: HTTPSCallable = functions.httpsCallable(URL(string: "https://example.com/myFunc")!)
let urlCallableWithOptions: HTTPSCallable = functions.httpsCallable(URL(string: "https://example.com/myFunc")!, options: options)

callable.timeoutInterval = 30.0
let _: TimeInterval = callable.timeoutInterval

let result: HTTPSCallableResult = try await callable.call()
let resultWithData: HTTPSCallableResult = try await callable.call(["key": "value"])
let _: Any = result.data
let _: HTTPSCallable = callableWithOptions
let _: HTTPSCallable = urlCallable
let _: HTTPSCallable = urlCallableWithOptions
let _: HTTPSCallableResult = resultWithData
}
}
}
Expand Down