diff --git a/README.md b/README.md index d30eac8..6fb07e4 100644 --- a/README.md +++ b/README.md @@ -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` 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 | @@ -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` 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). diff --git a/Sources/SkipFirebaseFunctions/SkipFirebaseFunctions.swift b/Sources/SkipFirebaseFunctions/SkipFirebaseFunctions.swift index e902026..381835c 100644 --- a/Sources/SkipFirebaseFunctions/SkipFirebaseFunctions.swift +++ b/Sources/SkipFirebaseFunctions/SkipFirebaseFunctions.swift @@ -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 @@ -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 { @@ -65,6 +91,16 @@ public class HTTPSCallable: KotlinConverting 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) { diff --git a/Tests/SkipFirebaseFunctionsTests/SkipFirebaseFunctionsTests.swift b/Tests/SkipFirebaseFunctionsTests/SkipFirebaseFunctionsTests.swift index c0d6382..ba26837 100644 --- a/Tests/SkipFirebaseFunctionsTests/SkipFirebaseFunctionsTests.swift +++ b/Tests/SkipFirebaseFunctionsTests/SkipFirebaseFunctionsTests.swift @@ -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 } } }