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
118 changes: 112 additions & 6 deletions Sources/SkipFirebaseAnalytics/SkipFirebaseAnalytics.swift
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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<String>; 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<com.google.firebase.analytics.FirebaseAnalytics.ConsentType, com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus>()
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
}
}
}

Expand Down
173 changes: 170 additions & 3 deletions Tests/SkipFirebaseAnalyticsTests/SkipFirebaseAnalyticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}