From 399846334f502aad00dfe9329a43a209c6700190 Mon Sep 17 00:00:00 2001 From: Jeroen <76699269+IT-Guy007@users.noreply.github.com> Date: Wed, 13 May 2026 21:36:00 +0200 Subject: [PATCH 1/7] Add Storage list APIs and progress observers Implement Firebase Storage listing and byte-level progress observation: add async list/maxResults/pageToken and listAll on StorageReference, introduce StorageListResult wrapper (items, prefixes, pageToken), and add StorageTaskStatus/StorageProgress/StorageTaskSnapshot types. Implement observe/removeObserver/removeAllObservers for UploadTask and FileDownloadTask (with internal observer tracking) and provide no-op/overrides for other download tasks. Update tests to exercise list APIs and observer types, and update README to reflect increased Storage support; remove related TODO comments. --- README.md | 3 +- .../SkipFirebaseStorage.swift | 195 +++++++++++++++++- .../SkipFirebaseStorageTests.swift | 65 ++++++ 3 files changed, 258 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 00049db..265fcab 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ See the `Package.swift` files in the | `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` | ~65% | 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`, live `Progress` snapshot observers (admitted TODOs), full `StorageError` code mapping, `putStream`/`putString` | +| `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` | @@ -52,7 +52,6 @@ See the `Package.swift` files in the - **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. -- **`SkipFirebaseStorage`** does not yet support `list`/`listAll`, nor live `Progress` snapshot observers — uploads/downloads run to completion but byte-by-byte progress subscription is not exposed. - **`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. diff --git a/Sources/SkipFirebaseStorage/SkipFirebaseStorage.swift b/Sources/SkipFirebaseStorage/SkipFirebaseStorage.swift index 2313a50..6162d59 100644 --- a/Sources/SkipFirebaseStorage/SkipFirebaseStorage.swift +++ b/Sources/SkipFirebaseStorage/SkipFirebaseStorage.swift @@ -269,7 +269,6 @@ public class StorageReference: KotlinConverting StorageMetadata { return try await withCheckedThrowingContinuation { continuation in @@ -301,7 +300,6 @@ public class StorageReference: KotlinConverting StorageMetadata { return try await withCheckedThrowingContinuation { continuation in @@ -366,6 +364,87 @@ public class StorageReference: KotlinConverting StorageListResult { + let result: com.google.firebase.storage.ListResult = platformValue.list(Int(maxResults)).await() + return StorageListResult(platformValue: result) + } + + /// Throws `StorageException` + public func list(maxResults: Int64, pageToken: String) async throws -> StorageListResult { + let result: com.google.firebase.storage.ListResult = platformValue.list(Int(maxResults), pageToken).await() + return StorageListResult(platformValue: result) + } + + /// Throws `StorageException` + public func listAll() async throws -> StorageListResult { + let result: com.google.firebase.storage.ListResult = platformValue.listAll().await() + return StorageListResult(platformValue: result) + } +} + +public class StorageListResult { + public let platformValue: com.google.firebase.storage.ListResult + + public init(platformValue: com.google.firebase.storage.ListResult) { + self.platformValue = platformValue + } + + public var items: [StorageReference] { + var refs: [StorageReference] = [] + for item in platformValue.items { + refs.append(StorageReference(platformValue: item)) + } + return refs + } + + public var prefixes: [StorageReference] { + var refs: [StorageReference] = [] + for prefix in platformValue.prefixes { + refs.append(StorageReference(platformValue: prefix)) + } + return refs + } + + public var pageToken: String? { + platformValue.pageToken + } +} + +public enum StorageTaskStatus: Int { + case unknown = 0 + case resume = 1 + case progress = 2 + case pause = 3 + case success = 4 + case failure = 5 +} + +/// Byte-level progress for a storage transfer, mirroring the fields of `Foundation.Progress` +/// that are relevant to Firebase Storage operations. +public struct StorageProgress { + public let completedUnitCount: Int64 + public let totalUnitCount: Int64 + + public var fractionCompleted: Double { + guard totalUnitCount > 0 else { return 0 } + return Double(completedUnitCount) / Double(totalUnitCount) + } +} + +public class StorageTaskSnapshot { + public let status: StorageTaskStatus + public var progress: StorageProgress? + public var metadata: StorageMetadata? + public var error: Error? + + init(status: StorageTaskStatus, progress: StorageProgress? = nil, metadata: StorageMetadata? = nil, error: Error? = nil) { + self.status = status + self.progress = progress + self.metadata = metadata + self.error = error + } } public class StorageTask { @@ -387,6 +466,7 @@ public class StorageObservableTask : StorageTask { public final class StorageUploadTask : StorageTaskManagement { public let platformValue: com.google.firebase.storage.UploadTask + private var activeObservers: [String: Bool] = [:] init(platformValue: com.google.firebase.storage.UploadTask) { super.init() @@ -407,9 +487,69 @@ public final class StorageUploadTask : StorageTaskManagement { platformValue.resume() return } + + @discardableResult + public func observe(_ status: StorageTaskStatus, handler: @escaping (StorageTaskSnapshot) -> Void) -> String { + let handle = UUID().uuidString + activeObservers[handle] = true + + switch status { + case .progress: + platformValue.addOnProgressListener { taskSnapshot in + guard self.activeObservers[handle] == true else { return } + let p = StorageProgress(completedUnitCount: taskSnapshot.bytesTransferred, totalUnitCount: taskSnapshot.totalByteCount) + handler(StorageTaskSnapshot(status: .progress, progress: p)) + } + case .success: + platformValue.addOnSuccessListener { taskSnapshot in + guard self.activeObservers[handle] == true else { return } + var metadata: StorageMetadata? = nil + if let m = taskSnapshot.metadata { + metadata = StorageMetadata(platformValue: m) + } + handler(StorageTaskSnapshot(status: .success, metadata: metadata)) + } + case .failure: + platformValue.addOnFailureListener { exception in + guard self.activeObservers[handle] == true else { return } + handler(StorageTaskSnapshot(status: .failure, error: ErrorException(exception))) + } + case .pause: + platformValue.addOnPausedListener { _ in + guard self.activeObservers[handle] == true else { return } + handler(StorageTaskSnapshot(status: .pause)) + } + default: + break + } + + return handle + } + + public func removeObserver(withHandle handle: String) { + activeObservers[handle] = false + } + + public func removeAllObservers() { + for key in activeObservers.keys { + activeObservers[key] = false + } + } + + public func removeAllObservers(for status: StorageTaskStatus) { + removeAllObservers() + } } public class StorageDownloadTask { + @discardableResult + open func observe(_ status: StorageTaskStatus, handler: @escaping (StorageTaskSnapshot) -> Void) -> String { + return "" + } + + open func removeObserver(withHandle handle: String) { } + open func removeAllObservers() { } + open func removeAllObservers(for status: StorageTaskStatus) { } } public final class StoragBytesDownloadTask : StorageDownloadTask { @@ -420,8 +560,9 @@ public final class StoragBytesDownloadTask : StorageDownloadTask { } } -public final class StorageFileDownloadTask : StorageDownloadTask, StorageTaskManagement { +public class StorageFileDownloadTask : StorageDownloadTask, StorageTaskManagement { public let platformValue: com.google.firebase.storage.FileDownloadTask + private var activeObservers: [String: Bool] = [:] init(platformValue: com.google.firebase.storage.FileDownloadTask) { super.init() @@ -442,6 +583,54 @@ public final class StorageFileDownloadTask : StorageDownloadTask, StorageTaskMan platformValue.resume() return } + + @discardableResult + override public func observe(_ status: StorageTaskStatus, handler: @escaping (StorageTaskSnapshot) -> Void) -> String { + let handle = UUID().uuidString + activeObservers[handle] = true + + switch status { + case .progress: + platformValue.addOnProgressListener { taskSnapshot in + guard self.activeObservers[handle] == true else { return } + let p = StorageProgress(completedUnitCount: taskSnapshot.bytesTransferred, totalUnitCount: taskSnapshot.totalByteCount) + handler(StorageTaskSnapshot(status: .progress, progress: p)) + } + case .success: + platformValue.addOnSuccessListener { (taskSnapshot: com.google.firebase.storage.FileDownloadTask.TaskSnapshot) in + guard self.activeObservers[handle] == true else { return } + handler(StorageTaskSnapshot(status: .success)) + } + case .failure: + platformValue.addOnFailureListener { exception in + guard self.activeObservers[handle] == true else { return } + handler(StorageTaskSnapshot(status: .failure, error: ErrorException(exception))) + } + case .pause: + platformValue.addOnPausedListener { _ in + guard self.activeObservers[handle] == true else { return } + handler(StorageTaskSnapshot(status: .pause)) + } + default: + break + } + + return handle + } + + override public func removeObserver(withHandle handle: String) { + activeObservers[handle] = false + } + + override public func removeAllObservers() { + for key in activeObservers.keys { + activeObservers[key] = false + } + } + + override public func removeAllObservers(for status: StorageTaskStatus) { + removeAllObservers() + } } #endif diff --git a/Tests/SkipFirebaseStorageTests/SkipFirebaseStorageTests.swift b/Tests/SkipFirebaseStorageTests/SkipFirebaseStorageTests.swift index 50f16ce..020c14e 100644 --- a/Tests/SkipFirebaseStorageTests/SkipFirebaseStorageTests.swift +++ b/Tests/SkipFirebaseStorageTests/SkipFirebaseStorageTests.swift @@ -71,7 +71,72 @@ let logger: Logger = Logger(subsystem: "SkipFirebaseStorageTests", category: "Te let sdt2: StorageDownloadTask = ref.write(toFile: fileURL) let sdt3: StorageDownloadTask = ref.write(toFile: fileURL, completion: { url, error in }) + + // list / listAll + let listResult: StorageListResult = try await ref.listAll() + let _: [StorageReference] = listResult.items + let _: [StorageReference] = listResult.prefixes + let _: String? = listResult.pageToken + + let pagedResult: StorageListResult = try await ref.list(maxResults: 10) + let _: [StorageReference] = pagedResult.items + let pagedResultWithToken: StorageListResult = try await ref.list(maxResults: 10, pageToken: "token") + let _: [StorageReference] = pagedResultWithToken.items + + // progress observers — upload + let handle1: String = sut.observe(.progress) { snapshot in + let _: StorageTaskStatus = snapshot.status + let _ = snapshot.progress + let _: StorageMetadata? = snapshot.metadata + let _: Error? = snapshot.error + } + let _: String = sut.observe(.success) { _ in } + let _: String = sut.observe(.failure) { _ in } + let _: String = sut.observe(.pause) { _ in } + sut.removeObserver(withHandle: handle1) + sut.removeAllObservers() + sut.removeAllObservers(for: .progress) + + // progress observers — file download + let handle2: String = sdt2.observe(.progress) { snapshot in + let _ = snapshot.progress + } + let _: String = sdt2.observe(.success) { _ in } + let _: String = sdt2.observe(.failure) { _ in } + sdt2.removeObserver(withHandle: handle2) + sdt2.removeAllObservers() + sdt2.removeAllObservers(for: .progress) } } + + func testStorageListResultTypes() throws { + #if SKIP + // Validate that list/listAll and StorageListResult properties exist. + // Async function references and typed closures cause Kotlin type-inference issues, + // so we use named variables to preserve type context. + let _items: (StorageListResult) -> [StorageReference] = { result in result.items } + let _prefixes: (StorageListResult) -> [StorageReference] = { result in result.prefixes } + let _pageToken: (StorageListResult) -> String? = { result in result.pageToken } + #endif + } + + func testStorageTaskObserverTypes() throws { + #if SKIP + // Validate StorageTaskStatus, StorageTaskSnapshot, and observer API types compile. + // These are Skip-specific wrappers; the iOS SDK uses StorageTaskState/Progress instead. + let _: StorageTaskStatus = StorageTaskStatus.progress + let _: StorageTaskStatus = StorageTaskStatus.success + let _: StorageTaskStatus = StorageTaskStatus.failure + let _: StorageTaskStatus = StorageTaskStatus.pause + let _: StorageTaskStatus = StorageTaskStatus.resume + let _: StorageTaskStatus = StorageTaskStatus.unknown + + let snapshot = StorageTaskSnapshot(status: StorageTaskStatus.progress) + let _: StorageTaskStatus = snapshot.status + let _: StorageProgress? = snapshot.progress + let _: StorageMetadata? = snapshot.metadata + let _: Error? = snapshot.error + #endif + } } From bb001a3e2a31075e783bd128eba8805bf5042c6c Mon Sep 17 00:00:00 2001 From: Jeroen <76699269+IT-Guy007@users.noreply.github.com> Date: Wed, 13 May 2026 23:23:13 +0200 Subject: [PATCH 2/7] Clamp maxResults before converting to Int Clamp Int64 maxResults to the Swift Int range before casting to Int to prevent overflow when calling platformValue.list(...). Adds a `clamped` variable and uses it in both list(maxResults:) overloads so large or small Int64 values won't cause invalid Int conversion. --- Sources/SkipFirebaseStorage/SkipFirebaseStorage.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/SkipFirebaseStorage/SkipFirebaseStorage.swift b/Sources/SkipFirebaseStorage/SkipFirebaseStorage.swift index 6162d59..71f1f5c 100644 --- a/Sources/SkipFirebaseStorage/SkipFirebaseStorage.swift +++ b/Sources/SkipFirebaseStorage/SkipFirebaseStorage.swift @@ -367,13 +367,15 @@ public class StorageReference: KotlinConverting StorageListResult { - let result: com.google.firebase.storage.ListResult = platformValue.list(Int(maxResults)).await() + let clamped = Int(max(min(maxResults, Int64(Int.max)), Int64(Int.min))) + let result: com.google.firebase.storage.ListResult = platformValue.list(clamped).await() return StorageListResult(platformValue: result) } /// Throws `StorageException` public func list(maxResults: Int64, pageToken: String) async throws -> StorageListResult { - let result: com.google.firebase.storage.ListResult = platformValue.list(Int(maxResults), pageToken).await() + let clamped = Int(max(min(maxResults, Int64(Int.max)), Int64(Int.min))) + let result: com.google.firebase.storage.ListResult = platformValue.list(clamped, pageToken).await() return StorageListResult(platformValue: result) } From 530385cc453f870dc63289b44ab2be2f1a339ac1 Mon Sep 17 00:00:00 2001 From: Jeroen <76699269+IT-Guy007@users.noreply.github.com> Date: Fri, 15 May 2026 16:06:33 +0200 Subject: [PATCH 3/7] Added Firestore Performance --- Package.swift | 10 ++ .../Resources/Localizable.xcstrings | 5 + Sources/SkipFirebasePerformance/Skip/skip.yml | 12 ++ .../SkipFirebasePerformance.swift | 162 ++++++++++++++++++ .../Resources/TestData.json | 3 + .../Skip/skip.yml | 8 + .../SkipFirebasePerformanceTests.swift | 28 +++ 7 files changed, 228 insertions(+) create mode 100644 Sources/SkipFirebasePerformance/Resources/Localizable.xcstrings create mode 100644 Sources/SkipFirebasePerformance/Skip/skip.yml create mode 100644 Sources/SkipFirebasePerformance/SkipFirebasePerformance.swift create mode 100644 Tests/SkipFirebasePerformanceTests/Resources/TestData.json create mode 100644 Tests/SkipFirebasePerformanceTests/Skip/skip.yml create mode 100644 Tests/SkipFirebasePerformanceTests/SkipFirebasePerformanceTests.swift diff --git a/Package.swift b/Package.swift index 4f40285..00fed8b 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,7 @@ let package = Package( .library(name: "SkipFirebaseFunctions", targets: ["SkipFirebaseFunctions"]), .library(name: "SkipFirebaseInstallations", targets: ["SkipFirebaseInstallations"]), .library(name: "SkipFirebaseStorage", targets: ["SkipFirebaseStorage"]), + .library(name: "SkipFirebasePerformance", targets: ["SkipFirebasePerformance"]), ], dependencies: [ .package(url: "https://source.skip.tools/skip.git", from: "1.8.0"), @@ -143,6 +144,15 @@ let package = Package( "SkipFirebaseStorage", .product(name: "SkipTest", package: "skip") ], resources: [.process("Resources")], plugins: skipstone), + + .target(name: "SkipFirebasePerformance", dependencies: [ + "SkipFirebaseCore", + .product(name: "FirebasePerformance", package: "firebase-ios-sdk", condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS, .macCatalyst])), + ], resources: [.process("Resources")], plugins: skipstone), + .testTarget(name: "SkipFirebasePerformanceTests", dependencies: [ + "SkipFirebasePerformance", + .product(name: "SkipTest", package: "skip") + ], resources: [.process("Resources")], plugins: skipstone), ] ) diff --git a/Sources/SkipFirebasePerformance/Resources/Localizable.xcstrings b/Sources/SkipFirebasePerformance/Resources/Localizable.xcstrings new file mode 100644 index 0000000..8a47010 --- /dev/null +++ b/Sources/SkipFirebasePerformance/Resources/Localizable.xcstrings @@ -0,0 +1,5 @@ +{ + "sourceLanguage" : "en", + "strings" : {}, + "version" : "1.0" +} diff --git a/Sources/SkipFirebasePerformance/Skip/skip.yml b/Sources/SkipFirebasePerformance/Skip/skip.yml new file mode 100644 index 0000000..4be96c2 --- /dev/null +++ b/Sources/SkipFirebasePerformance/Skip/skip.yml @@ -0,0 +1,12 @@ +# Configuration file for https://skip.tools project +# +skip: + mode: 'transpiled' + bridging: true + +# Kotlin dependencies and Gradle build options for this module can be configured here +build: + contents: + - block: 'dependencies' + contents: + - 'implementation("com.google.firebase:firebase-perf")' diff --git a/Sources/SkipFirebasePerformance/SkipFirebasePerformance.swift b/Sources/SkipFirebasePerformance/SkipFirebasePerformance.swift new file mode 100644 index 0000000..7e5f205 --- /dev/null +++ b/Sources/SkipFirebasePerformance/SkipFirebasePerformance.swift @@ -0,0 +1,162 @@ +// Copyright 2025–2026 Skip +// SPDX-License-Identifier: MPL-2.0 +#if !SKIP_BRIDGE +#if canImport(FirebasePerformance) +@_exported import FirebasePerformance +#elseif SKIP +import Foundation +import SkipFirebaseCore + +// https://firebase.google.com/docs/reference/swift/firebaseperformance/api/reference/Classes/Performance +// https://firebase.google.com/docs/reference/android/com/google/firebase/perf/FirebasePerformance + +public enum HTTPMethod: String { + case connect = "CONNECT" + case delete = "DELETE" + case get = "GET" + case head = "HEAD" + case options = "OPTIONS" + case patch = "PATCH" + case post = "POST" + case put = "PUT" + case trace = "TRACE" +} + +public final class Performance { + public let platformValue: com.google.firebase.perf.FirebasePerformance + + public init(platformValue: com.google.firebase.perf.FirebasePerformance) { + self.platformValue = platformValue + } + + public static func sharedInstance() -> Performance { + Performance(platformValue: com.google.firebase.perf.FirebasePerformance.getInstance()) + } + + public var isDataCollectionEnabled: Bool { + get { platformValue.isPerformanceCollectionEnabled() } + set { platformValue.setPerformanceCollectionEnabled(newValue) } + } + + public func trace(name: String) -> Trace? { + Trace(platformValue: platformValue.newTrace(name)) + } +} + +public final class Trace { + public let platformValue: com.google.firebase.perf.metrics.Trace + + public init(platformValue: com.google.firebase.perf.metrics.Trace) { + self.platformValue = platformValue + } + + public func start() { + platformValue.start() + } + + public func stop() { + platformValue.stop() + } + + public func incrementMetric(_ name: String, by value: Int64) { + platformValue.incrementMetric(name, value) + } + + public func valueForMetric(_ name: String) -> Int64 { + platformValue.getLongMetric(name) + } + + public func setAttribute(_ value: String, forName name: String) { + platformValue.putAttribute(name, value) + } + + public func valueForAttribute(_ name: String) -> String? { + platformValue.getAttribute(name) + } + + public func removeAttribute(_ name: String) { + platformValue.removeAttribute(name) + } + + public var attributes: [String: String] { + var result: [String: String] = [:] + for (key, value) in platformValue.getAttributes() { + result[key] = value + } + return result + } +} + +public final class HTTPMetric { + public let platformValue: com.google.firebase.perf.metrics.HttpMetric + private var _responseCode: Int = 0 + private var _requestPayloadSize: Int = 0 + private var _responsePayloadSize: Int = 0 + private var _responseContentType: String? = nil + + public init(url: URL, httpMethod: HTTPMethod) { + self.platformValue = com.google.firebase.perf.FirebasePerformance.getInstance().newHttpMetric(url.absoluteString, httpMethod.rawValue) + } + + public func start() { + platformValue.start() + } + + public func stop() { + platformValue.stop() + } + + public var responseCode: Int { + get { _responseCode } + set { + _responseCode = newValue + platformValue.setHttpResponseCode(newValue) + } + } + + public var requestPayloadSize: Int { + get { _requestPayloadSize } + set { + _requestPayloadSize = newValue + platformValue.setRequestPayloadSize(Int64(newValue)) + } + } + + public var responsePayloadSize: Int { + get { _responsePayloadSize } + set { + _responsePayloadSize = newValue + platformValue.setResponsePayloadSize(Int64(newValue)) + } + } + + public var responseContentType: String? { + get { _responseContentType } + set { + _responseContentType = newValue + platformValue.setResponseContentType(newValue) + } + } + + public func setAttribute(_ value: String, forName name: String) { + platformValue.putAttribute(name, value) + } + + public func valueForAttribute(_ name: String) -> String? { + platformValue.getAttribute(name) + } + + public func removeAttribute(_ name: String) { + platformValue.removeAttribute(name) + } + + public var attributes: [String: String] { + var result: [String: String] = [:] + for (key, value) in platformValue.getAttributes() { + result[key] = value + } + return result + } +} +#endif +#endif diff --git a/Tests/SkipFirebasePerformanceTests/Resources/TestData.json b/Tests/SkipFirebasePerformanceTests/Resources/TestData.json new file mode 100644 index 0000000..0e808a3 --- /dev/null +++ b/Tests/SkipFirebasePerformanceTests/Resources/TestData.json @@ -0,0 +1,3 @@ +{ + "testModuleName": "SkipBase" +} diff --git a/Tests/SkipFirebasePerformanceTests/Skip/skip.yml b/Tests/SkipFirebasePerformanceTests/Skip/skip.yml new file mode 100644 index 0000000..b6e8347 --- /dev/null +++ b/Tests/SkipFirebasePerformanceTests/Skip/skip.yml @@ -0,0 +1,8 @@ +# Configuration file for https://skip.tools project +# +# Kotlin dependencies and Gradle build options for this module can be configured here +#build: +# contents: +# - block: 'dependencies' +# contents: +# - 'implementation("androidx.compose.runtime:runtime")' diff --git a/Tests/SkipFirebasePerformanceTests/SkipFirebasePerformanceTests.swift b/Tests/SkipFirebasePerformanceTests/SkipFirebasePerformanceTests.swift new file mode 100644 index 0000000..726af5f --- /dev/null +++ b/Tests/SkipFirebasePerformanceTests/SkipFirebasePerformanceTests.swift @@ -0,0 +1,28 @@ +// Copyright 2025–2026 Skip +// SPDX-License-Identifier: MPL-2.0 +import XCTest +import OSLog +import Foundation +#if !SKIP +import FirebaseCore +#if canImport(FirebasePerformance) +import FirebasePerformance +#endif +#else +import SkipFirebaseCore +import SkipFirebasePerformance +#endif + +let logger: Logger = Logger(subsystem: "SkipFirebasePerformanceTests", category: "Tests") + +@MainActor final class SkipFirebasePerformanceTests: XCTestCase { + func testSkipFirebasePerformanceTests() async throws { + #if canImport(FirebasePerformance) || SKIP + if false { + let _: Performance = Performance.sharedInstance() + let _: Trace? = Performance.sharedInstance().trace(name: "test_trace") + let _: HTTPMetric? = HTTPMetric(url: URL(string: "https://example.com")!, httpMethod: .get) + } + #endif + } +} From b8ef13786a47e85742ff24ac7cc41ed15f5e5547 Mon Sep 17 00:00:00 2001 From: Jeroen <76699269+IT-Guy007@users.noreply.github.com> Date: Fri, 15 May 2026 16:09:59 +0200 Subject: [PATCH 4/7] Updated readme to reflect FirebasePerformance support --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 265fcab..e83b29c 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,9 @@ See the `Package.swift` files in the | `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` | +| `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 twelve 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 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. ### What is working well @@ -58,7 +59,7 @@ See the `Package.swift` files in the ### 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`, `FirebaseMLModelDownloader`, and `FirebasePerformance`. Pull requests adding any of these are welcome. +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.