From dca0c818137147a3cf1bd4a47a81e889c25e8402 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 25 Mar 2026 11:26:39 -0400 Subject: [PATCH 1/3] Analytics improvements --- .../SkipFirebaseAnalytics.swift | 103 +++++++++++++++++- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/Sources/SkipFirebaseAnalytics/SkipFirebaseAnalytics.swift b/Sources/SkipFirebaseAnalytics/SkipFirebaseAnalytics.swift index 74f5f2d..5ffe5f9 100644 --- a/Sources/SkipFirebaseAnalytics/SkipFirebaseAnalytics.swift +++ b/Sources/SkipFirebaseAnalytics/SkipFirebaseAnalytics.swift @@ -1,6 +1,7 @@ // SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception #if !SKIP_BRIDGE #if SKIP +import Foundation import SkipFirebaseCore import kotlinx.coroutines.tasks.await @@ -14,14 +15,104 @@ public final class Analytics { self.analytics = analytics } -// public static func analytics() -> Analytics { -// Analytics(analytics: com.google.firebase.analytics.FirebaseAnalytics.getInstance()) -// } + private static var instance: com.google.firebase.analytics.FirebaseAnalytics { + com.google.firebase.analytics.FirebaseAnalytics.getInstance(skip.foundation.ProcessInfo.processInfo.androidContext) + } - public static func logEvent(_ name: String, parameters: [String: Any] = [:]) { + private static func toBundle(_ parameters: [String: Any]?) -> android.os.Bundle? { + guard let parameters = parameters else { return nil } let bundle = android.os.Bundle() - // TODO: add parameters to bundle - com.google.firebase.analytics.FirebaseAnalytics.getInstance(skip.foundation.ProcessInfo.processInfo.androidContext).logEvent(name, bundle) + for (key, value) in parameters { + if let s = value as? String { + bundle.putString(key, s) + } else if let b = value as? Bool { + bundle.putBoolean(key, b) + } else if let i = value as? Int { + bundle.putLong(key, Int64(i)) + } else if let l = value as? Int64 { + bundle.putLong(key, l) + } else if let d = value as? Double { + bundle.putDouble(key, d) + } else if let f = value as? Float { + bundle.putFloat(key, f) + } else { + bundle.putString(key, "\(value)") + } + } + return bundle + } + + public static func logEvent(_ name: String, parameters: [String: Any]? = nil) { + instance.logEvent(name, toBundle(parameters ?? [:])) + } + + public static func setUserProperty(_ value: String?, forName name: String) { + instance.setUserProperty(name, value) + } + + public static func setUserID(_ userID: String?) { + instance.setUserId(userID) + } + + public static func setAnalyticsCollectionEnabled(_ enabled: Bool) { + instance.setAnalyticsCollectionEnabled(enabled) + } + + public static func setDefaultEventParameters(_ parameters: [String: Any]?) { + instance.setDefaultEventParameters(toBundle(parameters)) + } + + public static func resetAnalyticsData() { + instance.resetAnalyticsData() + } + + public static func appInstanceID() async -> String? { + return instance.getAppInstanceId().await() + } + + public static func sessionID() async throws -> Int64 { + let id = instance.getSessionId().await() + return id as Int64 + } + + public static func setSessionTimeoutInterval(_ seconds: TimeInterval) { + instance.setSessionTimeoutDuration(Int64(seconds * 1000.0)) + } + + public static func setConsent(_ consentSettings: [ConsentType: ConsentStatus]) { + let map = java.util.HashMap() + for (type, status) in consentSettings { + map.put(type.platformValue, status.platformValue) + } + instance.setConsent(map) + } +} + +public enum ConsentType { + case adPersonalization + case adStorage + case adUserData + case analyticsStorage + + public var platformValue: com.google.firebase.analytics.FirebaseAnalytics.ConsentType { + switch self { + case .adPersonalization: return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.AD_PERSONALIZATION + case .adStorage: return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.AD_STORAGE + case .adUserData: return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.AD_USER_DATA + case .analyticsStorage: return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE + } + } +} + +public enum ConsentStatus { + case granted + case denied + + public var platformValue: com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus { + switch self { + case .granted: return com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus.GRANTED + case .denied: return com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus.DENIED + } } } From 8332f95e3cf1b277b51e13b14bb2fe8438b3fb1f Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 25 Mar 2026 11:44:46 -0400 Subject: [PATCH 2/3] Add test cases --- .../SkipFirebaseAnalytics.swift | 55 ++++--- .../SkipFirebaseAnalyticsTests.swift | 141 +++++++++++++++++- 2 files changed, 175 insertions(+), 21 deletions(-) diff --git a/Sources/SkipFirebaseAnalytics/SkipFirebaseAnalytics.swift b/Sources/SkipFirebaseAnalytics/SkipFirebaseAnalytics.swift index 5ffe5f9..a813d35 100644 --- a/Sources/SkipFirebaseAnalytics/SkipFirebaseAnalytics.swift +++ b/Sources/SkipFirebaseAnalytics/SkipFirebaseAnalytics.swift @@ -66,13 +66,14 @@ public final class Analytics { instance.resetAnalyticsData() } - public static func appInstanceID() async -> String? { - return instance.getAppInstanceId().await() + public static func appInstanceID() -> String? { + // Android's getAppInstanceId() returns Task; block to match synchronous iOS API + return com.google.android.gms.tasks.Tasks.await(instance.getAppInstanceId()) } - public static func sessionID() async throws -> Int64 { + public static func sessionID() async throws -> Int64? { let id = instance.getSessionId().await() - return id as Int64 + return id as? Int64 } public static func setSessionTimeoutInterval(_ seconds: TimeInterval) { @@ -88,30 +89,44 @@ public final class Analytics { } } -public enum ConsentType { - case adPersonalization - case adStorage - case adUserData - case analyticsStorage +public struct ConsentType: Hashable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public static let adPersonalization = ConsentType(rawValue: "ad_personalization") + public static let adStorage = ConsentType(rawValue: "ad_storage") + public static let adUserData = ConsentType(rawValue: "ad_user_data") + public static let analyticsStorage = ConsentType(rawValue: "analytics_storage") public var platformValue: com.google.firebase.analytics.FirebaseAnalytics.ConsentType { - switch self { - case .adPersonalization: return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.AD_PERSONALIZATION - case .adStorage: return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.AD_STORAGE - case .adUserData: return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.AD_USER_DATA - case .analyticsStorage: return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE + switch rawValue { + case "ad_personalization": return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.AD_PERSONALIZATION + case "ad_storage": return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.AD_STORAGE + case "ad_user_data": return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.AD_USER_DATA + case "analytics_storage": return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE + default: return com.google.firebase.analytics.FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE } } } -public enum ConsentStatus { - case granted - case denied +public struct ConsentStatus: Hashable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public static let granted = ConsentStatus(rawValue: "granted") + public static let denied = ConsentStatus(rawValue: "denied") public var platformValue: com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus { - switch self { - case .granted: return com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus.GRANTED - case .denied: return com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus.DENIED + switch rawValue { + case "granted": return com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus.GRANTED + case "denied": return com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus.DENIED + default: return com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus.DENIED } } } diff --git a/Tests/SkipFirebaseAnalyticsTests/SkipFirebaseAnalyticsTests.swift b/Tests/SkipFirebaseAnalyticsTests/SkipFirebaseAnalyticsTests.swift index f021254..4867465 100644 --- a/Tests/SkipFirebaseAnalyticsTests/SkipFirebaseAnalyticsTests.swift +++ b/Tests/SkipFirebaseAnalyticsTests/SkipFirebaseAnalyticsTests.swift @@ -16,5 +16,144 @@ let logger: Logger = Logger(subsystem: "SkipFirebaseAnalyticsTests", category: " func testSkipFirebaseAnalyticsTests() async throws { Analytics.logEvent("x", parameters: ["a": [1, 2, false]]) } -} + func testLogEventWithNoParameters() { + // Verify logEvent compiles with no parameters + let _: (String, [String: Any]?) -> Void = Analytics.logEvent + } + + func testLogEventWithStringParameters() { + let _: (String, [String: Any]?) -> Void = Analytics.logEvent + // Verify parameter types compile + let params: [String: Any] = [ + AnalyticsParameterItemName: "test_item", + AnalyticsParameterPrice: 9.99, + AnalyticsParameterQuantity: 1, + AnalyticsParameterCurrency: "USD" + ] + _ = params + } + + func testSetUserProperty() { + let _: (String?, String) -> Void = Analytics.setUserProperty(_:forName:) + } + + func testSetUserID() { + let _: (String?) -> Void = Analytics.setUserID + } + + func testSetAnalyticsCollectionEnabled() { + let _: (Bool) -> Void = Analytics.setAnalyticsCollectionEnabled + } + + func testSetDefaultEventParameters() { + let _: ([String: Any]?) -> Void = Analytics.setDefaultEventParameters + } + + func testResetAnalyticsData() { + let _: () -> Void = Analytics.resetAnalyticsData + } + + func testAppInstanceID() { + let _: () -> String? = Analytics.appInstanceID + } + + func testSessionID() { + // sessionID is async throws -> Int64? + let _: () async throws -> Int64? = Analytics.sessionID + } + + func testSetSessionTimeoutInterval() { + let _: (TimeInterval) -> Void = Analytics.setSessionTimeoutInterval + } + + func testConsentTypes() { + // Verify ConsentType static members exist and are the right type + let _: ConsentType = .adPersonalization + let _: ConsentType = .adStorage + let _: ConsentType = .adUserData + let _: ConsentType = .analyticsStorage + } + + func testConsentStatus() { + // Verify ConsentStatus static members exist and are the right type + let _: ConsentStatus = .granted + let _: ConsentStatus = .denied + } + + func testSetConsent() { + let _: ([ConsentType: ConsentStatus]) -> Void = Analytics.setConsent + } + + func testEventNameConstants() { + // Verify event name constants exist and are strings + let events: [String] = [ + AnalyticsEventAdImpression, + AnalyticsEventAddPaymentInfo, + AnalyticsEventAddShippingInfo, + AnalyticsEventAddToCart, + AnalyticsEventAddToWishlist, + AnalyticsEventAppOpen, + AnalyticsEventBeginCheckout, + AnalyticsEventCampaignDetails, + AnalyticsEventEarnVirtualCurrency, + AnalyticsEventGenerateLead, + AnalyticsEventJoinGroup, + AnalyticsEventLevelEnd, + AnalyticsEventLevelStart, + AnalyticsEventLevelUp, + AnalyticsEventLogin, + AnalyticsEventPostScore, + AnalyticsEventPurchase, + AnalyticsEventRefund, + AnalyticsEventRemoveFromCart, + AnalyticsEventScreenView, + AnalyticsEventSearch, + AnalyticsEventSelectContent, + AnalyticsEventSelectItem, + AnalyticsEventSelectPromotion, + AnalyticsEventShare, + AnalyticsEventSignUp, + AnalyticsEventSpendVirtualCurrency, + AnalyticsEventTutorialBegin, + AnalyticsEventTutorialComplete, + AnalyticsEventUnlockAchievement, + AnalyticsEventViewCart, + AnalyticsEventViewItem, + AnalyticsEventViewItemList, + AnalyticsEventViewPromotion, + AnalyticsEventViewSearchResults, + ] + XCTAssertFalse(events.isEmpty) + } + + func testParameterNameConstants() { + // Verify a representative set of parameter constants exist and are strings + let params: [String] = [ + AnalyticsParameterItemName, + AnalyticsParameterItemID, + AnalyticsParameterPrice, + AnalyticsParameterQuantity, + AnalyticsParameterCurrency, + AnalyticsParameterValue, + AnalyticsParameterScreenName, + AnalyticsParameterScreenClass, + AnalyticsParameterSearchTerm, + AnalyticsParameterMethod, + AnalyticsParameterScore, + AnalyticsParameterLevel, + AnalyticsParameterContent, + AnalyticsParameterContentType, + AnalyticsParameterCoupon, + AnalyticsParameterTransactionID, + AnalyticsParameterShipping, + AnalyticsParameterTax, + ] + XCTAssertFalse(params.isEmpty) + } + + func testUserPropertyConstants() { + let _: String = AnalyticsUserPropertyAllowAdPersonalizationSignals + let _: String = AnalyticsUserPropertySignUpMethod + } +} From 15e2beb44223ce7e219cd2fe168ec4725c28c785 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 25 Mar 2026 15:05:03 -0400 Subject: [PATCH 3/3] Update analytics tests --- .../SkipFirebaseAnalyticsTests.swift | 96 ++++++++++++------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/Tests/SkipFirebaseAnalyticsTests/SkipFirebaseAnalyticsTests.swift b/Tests/SkipFirebaseAnalyticsTests/SkipFirebaseAnalyticsTests.swift index 4867465..68c57b9 100644 --- a/Tests/SkipFirebaseAnalyticsTests/SkipFirebaseAnalyticsTests.swift +++ b/Tests/SkipFirebaseAnalyticsTests/SkipFirebaseAnalyticsTests.swift @@ -13,61 +13,76 @@ import SkipFirebaseAnalytics let logger: Logger = Logger(subsystem: "SkipFirebaseAnalyticsTests", category: "Tests") @MainActor final class SkipFirebaseAnalyticsTests: XCTestCase { - func testSkipFirebaseAnalyticsTests() async throws { - Analytics.logEvent("x", parameters: ["a": [1, 2, false]]) + func skipTests() throws { + throw XCTSkip("test intentionally skipped because it exists just for compiler validation") } - func testLogEventWithNoParameters() { - // Verify logEvent compiles with no parameters - let _: (String, [String: Any]?) -> Void = Analytics.logEvent - } + func testLogEvent() throws { + try skipTests() - func testLogEventWithStringParameters() { - let _: (String, [String: Any]?) -> Void = Analytics.logEvent - // Verify parameter types compile let params: [String: Any] = [ AnalyticsParameterItemName: "test_item", AnalyticsParameterPrice: 9.99, AnalyticsParameterQuantity: 1, AnalyticsParameterCurrency: "USD" ] - _ = params + Analytics.logEvent("ABC", parameters: params) } - func testSetUserProperty() { - let _: (String?, String) -> Void = Analytics.setUserProperty(_:forName:) + func testSetUserProperty() throws { + try skipTests() + + Analytics.setUserProperty("X", forName: "Y") + Analytics.setUserProperty(nil, forName: "Y") } - func testSetUserID() { - let _: (String?) -> Void = Analytics.setUserID + func testSetUserID() throws { + try skipTests() + + Analytics.setUserID(nil) + Analytics.setUserID("ABC") } - func testSetAnalyticsCollectionEnabled() { - let _: (Bool) -> Void = Analytics.setAnalyticsCollectionEnabled + func testSetAnalyticsCollectionEnabled() throws { + try skipTests() + + Analytics.setAnalyticsCollectionEnabled(false) } - func testSetDefaultEventParameters() { - let _: ([String: Any]?) -> Void = Analytics.setDefaultEventParameters + func testSetDefaultEventParameters() throws { + try skipTests() + + Analytics.setDefaultEventParameters(nil) + Analytics.setDefaultEventParameters(["x": false]) } - func testResetAnalyticsData() { - let _: () -> Void = Analytics.resetAnalyticsData + func testResetAnalyticsData() throws { + try skipTests() + + Analytics.resetAnalyticsData() } - func testAppInstanceID() { - let _: () -> String? = Analytics.appInstanceID + func testAppInstanceID() throws { + try skipTests() + + let _: String? = Analytics.appInstanceID() } - func testSessionID() { - // sessionID is async throws -> Int64? - let _: () async throws -> Int64? = Analytics.sessionID + func testSessionID() async throws { + try skipTests() + + let _: Int64? = try await Analytics.sessionID() } - func testSetSessionTimeoutInterval() { - let _: (TimeInterval) -> Void = Analytics.setSessionTimeoutInterval + func testSetSessionTimeoutInterval() throws { + try skipTests() + + Analytics.setSessionTimeoutInterval(TimeInterval(100.0)) } - func testConsentTypes() { + func testConsentTypes() throws { + try skipTests() + // Verify ConsentType static members exist and are the right type let _: ConsentType = .adPersonalization let _: ConsentType = .adStorage @@ -75,17 +90,26 @@ let logger: Logger = Logger(subsystem: "SkipFirebaseAnalyticsTests", category: " let _: ConsentType = .analyticsStorage } - func testConsentStatus() { + func testConsentStatus() throws { + try skipTests() + // Verify ConsentStatus static members exist and are the right type let _: ConsentStatus = .granted let _: ConsentStatus = .denied } - func testSetConsent() { - let _: ([ConsentType: ConsentStatus]) -> Void = Analytics.setConsent + func testSetConsent() throws { + try skipTests() + + Analytics.setConsent([ + .analyticsStorage: .granted, + .adPersonalization: .denied + ]) } - func testEventNameConstants() { + func testEventNameConstants() throws { + try skipTests() + // Verify event name constants exist and are strings let events: [String] = [ AnalyticsEventAdImpression, @@ -127,7 +151,9 @@ let logger: Logger = Logger(subsystem: "SkipFirebaseAnalyticsTests", category: " XCTAssertFalse(events.isEmpty) } - func testParameterNameConstants() { + func testParameterNameConstants() throws { + try skipTests() + // Verify a representative set of parameter constants exist and are strings let params: [String] = [ AnalyticsParameterItemName, @@ -152,7 +178,9 @@ let logger: Logger = Logger(subsystem: "SkipFirebaseAnalyticsTests", category: " XCTAssertFalse(params.isEmpty) } - func testUserPropertyConstants() { + func testUserPropertyConstants() throws { + try skipTests() + let _: String = AnalyticsUserPropertyAllowAdPersonalizationSignals let _: String = AnalyticsUserPropertySignUpMethod }