diff --git a/Sources/SkipFirebaseAnalytics/SkipFirebaseAnalytics.swift b/Sources/SkipFirebaseAnalytics/SkipFirebaseAnalytics.swift index 74f5f2d..a813d35 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,119 @@ 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() -> 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? { + 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 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 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 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 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..68c57b9 100644 --- a/Tests/SkipFirebaseAnalyticsTests/SkipFirebaseAnalyticsTests.swift +++ b/Tests/SkipFirebaseAnalyticsTests/SkipFirebaseAnalyticsTests.swift @@ -13,8 +13,175 @@ 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 testLogEvent() throws { + try skipTests() + + let params: [String: Any] = [ + AnalyticsParameterItemName: "test_item", + AnalyticsParameterPrice: 9.99, + AnalyticsParameterQuantity: 1, + AnalyticsParameterCurrency: "USD" + ] + Analytics.logEvent("ABC", parameters: params) + } + + func testSetUserProperty() throws { + try skipTests() + + Analytics.setUserProperty("X", forName: "Y") + Analytics.setUserProperty(nil, forName: "Y") + } + + func testSetUserID() throws { + try skipTests() + + Analytics.setUserID(nil) + Analytics.setUserID("ABC") + } + + func testSetAnalyticsCollectionEnabled() throws { + try skipTests() + + Analytics.setAnalyticsCollectionEnabled(false) + } + + func testSetDefaultEventParameters() throws { + try skipTests() + + Analytics.setDefaultEventParameters(nil) + Analytics.setDefaultEventParameters(["x": false]) + } + + func testResetAnalyticsData() throws { + try skipTests() + + Analytics.resetAnalyticsData() + } + + func testAppInstanceID() throws { + try skipTests() + + let _: String? = Analytics.appInstanceID() + } + + func testSessionID() async throws { + try skipTests() + + let _: Int64? = try await Analytics.sessionID() + } + + func testSetSessionTimeoutInterval() throws { + try skipTests() + + Analytics.setSessionTimeoutInterval(TimeInterval(100.0)) + } + + func testConsentTypes() throws { + try skipTests() + + // Verify ConsentType static members exist and are the right type + let _: ConsentType = .adPersonalization + let _: ConsentType = .adStorage + let _: ConsentType = .adUserData + let _: ConsentType = .analyticsStorage + } + + func testConsentStatus() throws { + try skipTests() + + // Verify ConsentStatus static members exist and are the right type + let _: ConsentStatus = .granted + let _: ConsentStatus = .denied + } + + func testSetConsent() throws { + try skipTests() + + Analytics.setConsent([ + .analyticsStorage: .granted, + .adPersonalization: .denied + ]) + } + + func testEventNameConstants() throws { + try skipTests() + + // 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() throws { + try skipTests() + + // 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() throws { + try skipTests() + + let _: String = AnalyticsUserPropertyAllowAdPersonalizationSignals + let _: String = AnalyticsUserPropertySignUpMethod + } +}