From dfb82b875b510667ac65aa3eacca31cf53695078 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 5 Nov 2024 16:35:38 +0100 Subject: [PATCH 01/17] add CustomerCenterEvent impression --- RevenueCat.xcodeproj/project.pbxproj | 16 +++ .../Events/CustomerCenterEvent.swift | 103 ++++++++++++++++++ .../Events/EventsRequest+CustomerCenter.swift | 100 +++++++++++++++++ Sources/Events/StoredEvent.swift | 1 + 4 files changed, 220 insertions(+) create mode 100644 Sources/CustomerCenter/Events/CustomerCenterEvent.swift create mode 100644 Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index cf1c0b00e7..89f13f0f2f 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -315,6 +315,8 @@ 35F249CC2C493DCC0058993A /* CustomerCenterPurchasesType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F249CB2C493DCC0058993A /* CustomerCenterPurchasesType.swift */; }; 35F249CE2C493E3D0058993A /* CustomerCenterPurchases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F249CD2C493E3D0058993A /* CustomerCenterPurchases.swift */; }; 35F38B482C30104E00CD29FD /* BackendGetCustomerCenterConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F38B472C30104E00CD29FD /* BackendGetCustomerCenterConfigTests.swift */; }; + 35F7884B2CC68F5300080F32 /* CustomerCenterEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F7884A2CC68F4C00080F32 /* CustomerCenterEvent.swift */; }; + 35F788F62CC6B62600080F32 /* EventsRequest+CustomerCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F788F52CC6B62400080F32 /* EventsRequest+CustomerCenter.swift */; }; 35F82BAB26A84E130051DF03 /* Dictionary+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BAA26A84E130051DF03 /* Dictionary+Extensions.swift */; }; 35F82BB226A98EC50051DF03 /* AttributionDataMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BB126A98EC50051DF03 /* AttributionDataMigratorTests.swift */; }; 35F82BB426A9A74D0051DF03 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F82BB326A9A74D0051DF03 /* HTTPClient.swift */; }; @@ -1571,6 +1573,8 @@ 35F249CB2C493DCC0058993A /* CustomerCenterPurchasesType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterPurchasesType.swift; sourceTree = ""; }; 35F249CD2C493E3D0058993A /* CustomerCenterPurchases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterPurchases.swift; sourceTree = ""; }; 35F38B472C30104E00CD29FD /* BackendGetCustomerCenterConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendGetCustomerCenterConfigTests.swift; sourceTree = ""; }; + 35F7884A2CC68F4C00080F32 /* CustomerCenterEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterEvent.swift; sourceTree = ""; }; + 35F788F52CC6B62400080F32 /* EventsRequest+CustomerCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventsRequest+CustomerCenter.swift"; sourceTree = ""; }; 35F82BAA26A84E130051DF03 /* Dictionary+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Extensions.swift"; sourceTree = ""; }; 35F82BB126A98EC50051DF03 /* AttributionDataMigratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributionDataMigratorTests.swift; sourceTree = ""; }; 35F82BB326A9A74D0051DF03 /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; @@ -3685,6 +3689,7 @@ 3592E8892C2ED54A00D7F91D /* CustomerCenter */ = { isa = PBXGroup; children = ( + 35F7884C2CC68F5F00080F32 /* Events */, 3592E8882C2ED54A00D7F91D /* CustomerCenterConfigData.swift */, ); path = CustomerCenter; @@ -3764,6 +3769,15 @@ path = Locale; sourceTree = ""; }; + 35F7884C2CC68F5F00080F32 /* Events */ = { + isa = PBXGroup; + children = ( + 35F788F52CC6B62400080F32 /* EventsRequest+CustomerCenter.swift */, + 35F7884A2CC68F4C00080F32 /* CustomerCenterEvent.swift */, + ); + path = Events; + sourceTree = ""; + }; 35F82BBB26A9BFA60051DF03 /* FoundationExtensions */ = { isa = PBXGroup; children = ( @@ -5747,6 +5761,7 @@ B34605C5279A6E380031CA74 /* CustomerInfoResponseHandler.swift in Sources */, 5796A3A927D7C43500653165 /* Deprecations.swift in Sources */, 4F0201C42A13C85500091612 /* Assertions.swift in Sources */, + 35F7884B2CC68F5300080F32 /* CustomerCenterEvent.swift in Sources */, 5753ED8E294A662400CBAB54 /* DateFormatter+Extensions.swift in Sources */, 4F174F472B07EA7E00FE538E /* StorefrontProvider.swift in Sources */, B3AA6238268B926F00894871 /* SystemInfo.swift in Sources */, @@ -5931,6 +5946,7 @@ B34D2AA0269606E400D88C3A /* IntroEligibility.swift in Sources */, F516BD29282434070083480B /* StoreKit2StorefrontListener.swift in Sources */, 4D6ABB102AF13FBD00BB2A08 /* SK2AppTransaction.swift in Sources */, + 35F788F62CC6B62600080F32 /* EventsRequest+CustomerCenter.swift in Sources */, 5766AAB0283D8CDC00FA6091 /* CacheFetchPolicy.swift in Sources */, 4FD368B42AA7CFED00F63354 /* PaywallEventStore.swift in Sources */, B302206E2728B798008F1A0D /* BackendErrorStrings.swift in Sources */, diff --git a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift new file mode 100644 index 0000000000..b07bc2a16a --- /dev/null +++ b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift @@ -0,0 +1,103 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// CustomerCenterEvent.swift +// +// Created by Cesar de la Vega on 21/10/24. + +import Foundation + +/// An event to be sent by the `RevenueCatUI` SDK. +public enum CustomerCenterEvent { + + // swiftlint:disable type_name + + /// An identifier that represents a customer center event. + public typealias ID = UUID + + // swiftlint:enable type_name + + /// An identifier that represents a paywall session. + public typealias SessionID = UUID + + /// A `CustomerCenterView` was displayed. + case impression(CreationData, Data) + +} + +extension CustomerCenterEvent { + + /// The creation data of a ``CustomerCenterEvent``. + public struct CreationData { + + // swiftlint:disable missing_docs + public var id: ID + public var date: Date + + public init( + id: ID = .init(), + date: Date = .init() + ) { + self.id = id + self.date = date + } + + } + +} + +extension CustomerCenterEvent { + + /// The content of a ``CustomerCenterEvent``. + public struct Data { + + // swiftlint:disable missing_docs + public var sessionIdentifier: SessionID + public var localeIdentifier: String + public var darkMode: Bool + + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + public init( + sessionID: SessionID, + locale: Locale, + darkMode: Bool + ) { + self.sessionIdentifier = sessionID + self.localeIdentifier = locale.identifier + self.darkMode = darkMode + } + // swiftlint:enable missing_docs + + } + +} + +extension CustomerCenterEvent { + + /// - Returns: the underlying ``CustomerCenterEvent/CreationData-swift.struct`` for this event. + public var creationData: CreationData { + switch self { + case let .impression(creationData, _): return creationData + } + } + + /// - Returns: the underlying ``CustomerCenterEvent/Data-swift.struct`` for this event. + public var data: Data { + switch self { + case let .impression(_, data): return data + } + } + +} + +// MARK: - + +extension CustomerCenterEvent.CreationData: Equatable, Codable, Sendable {} +extension CustomerCenterEvent.Data: Equatable, Codable, Sendable {} +extension CustomerCenterEvent: Equatable, Codable, Sendable {} diff --git a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift new file mode 100644 index 0000000000..63d127113c --- /dev/null +++ b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift @@ -0,0 +1,100 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Untitled.swift +// +// Created by Cesar de la Vega on 21/10/24. + +import Foundation + +extension EventsRequest { + + struct CustomerCenterEvent: FeatureEvent { + + let id: String? + let version: Int + var type: EventType + var appUserID: String + var sessionID: String + var timestamp: UInt64 + var darkMode: Bool + var localeIdentifier: String + + } + +} + +extension EventsRequest.CustomerCenterEvent { + + enum EventType: String { + + case impression = "customer_center_impression" + + } + + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + init?(storedEvent: StoredEvent) { + guard let eventData = storedEvent.encodedEvent.value as? [String: Any] else { + return nil + } + do { + let customerCenterEvent: CustomerCenterEvent = try JSONDecoder.default.decode(dictionary: eventData) + let creationData = customerCenterEvent.creationData + let data = customerCenterEvent.data + + self.init( + id: creationData.id.uuidString, + version: Self.version, + type: customerCenterEvent.eventType, + appUserID: storedEvent.userID, + sessionID: data.sessionIdentifier.uuidString, + timestamp: creationData.date.millisecondsSince1970, + darkMode: data.darkMode, + localeIdentifier: data.localeIdentifier + ) + } catch { + return nil + } + } + + private static let version: Int = 1 + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension CustomerCenterEvent { + + var eventType: EventsRequest.CustomerCenterEvent.EventType { + switch self { + case .impression: return .impression + } + + } + +} + +// MARK: - Codable + +extension EventsRequest.CustomerCenterEvent.EventType: Encodable {} +extension EventsRequest.CustomerCenterEvent: Encodable { + + private enum CodingKeys: String, CodingKey { + + case id + case version + case type + case appUserID = "appUserId" + case sessionID = "sessionId" + case timestamp + case darkMode + case localeIdentifier = "locale" + + } + +} diff --git a/Sources/Events/StoredEvent.swift b/Sources/Events/StoredEvent.swift index b6dbac60ec..7423fc6871 100644 --- a/Sources/Events/StoredEvent.swift +++ b/Sources/Events/StoredEvent.swift @@ -35,6 +35,7 @@ struct StoredEvent { enum Feature: String, Codable { case paywalls + case customerCenter } From b9ae356f3b231da36804cc7afc1d7140faf97103 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 5 Nov 2024 18:20:51 +0100 Subject: [PATCH 02/17] missing case in switch --- Sources/Events/Networking/EventsRequest.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/Events/Networking/EventsRequest.swift b/Sources/Events/Networking/EventsRequest.swift index 85ee28c50a..5b434c39f5 100644 --- a/Sources/Events/Networking/EventsRequest.swift +++ b/Sources/Events/Networking/EventsRequest.swift @@ -31,6 +31,11 @@ struct EventsRequest { return nil } return AnyEncodable(event) + case .customerCenter: + guard let event = CustomerCenterEvent(storedEvent: storedEvent) else { + return nil + } + return AnyEncodable(event) } }) } From a7106912209af7e475375307f4397455574d1825 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 20 Nov 2024 09:24:52 +0100 Subject: [PATCH 03/17] add trackImpression --- RevenueCat.xcodeproj/project.pbxproj | 4 ++++ .../ManageSubscriptionsPurchaseType.swift | 3 +++ .../ViewModels/CustomerCenterViewModel.swift | 5 +++++ .../ManageSubscriptionsViewModel.swift | 4 ++++ .../Views/CustomerCenterView.swift | 7 +++++++ .../Events/CustomerCenterEvent.swift | 18 ++++++++++++++++-- .../Events/EventsRequest+CustomerCenter.swift | 6 +++++- Sources/Events/FeatureEvent.swift | 18 ++++++++++++++++++ Sources/Events/Networking/EventsRequest.swift | 9 --------- .../Networking/EventsRequest+Paywall.swift | 2 +- Sources/Paywalls/Events/PaywallEvent.swift | 6 +++++- .../Paywalls/Events/PaywallEventsManager.swift | 8 ++++---- Sources/Purchasing/Purchases/Purchases.swift | 7 ++++++- 13 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 Sources/Events/FeatureEvent.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 89f13f0f2f..4cdc8e7797 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -234,6 +234,7 @@ 351B51C126D450E800BD2BD7 /* OfferingsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F575859126C08E3F00C12B97 /* OfferingsManagerTests.swift */; }; 351B51C226D450E800BD2BD7 /* ProductRequestDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E350E57B0A393455A72B40 /* ProductRequestDataTests.swift */; }; 351B51C326D450F200BD2BD7 /* InMemoryCachedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35E3250FBBB03D92E06EC /* InMemoryCachedObjectTests.swift */; }; + 352137322CDBA2AA00FE961B /* FeatureEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352137312CDBA2A700FE961B /* FeatureEvent.swift */; }; 3525D8A42C4AB3D600C21D99 /* CustomerCenterEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3525D8A32C4AB3D500C21D99 /* CustomerCenterEnvironment.swift */; }; 35272E1B26D0029300F22C3B /* DeviceCacheSubscriberAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35C4A795A0F056381A1B3 /* DeviceCacheSubscriberAttributesTests.swift */; }; 35272E2226D0048D00F22C3B /* HTTPClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E353CBE9CF2572A72A347F /* HTTPClientTests.swift */; }; @@ -1500,6 +1501,7 @@ 351B517126D44EF300BD2BD7 /* MockInMemoryCachedOfferings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockInMemoryCachedOfferings.swift; sourceTree = ""; }; 351B517326D44F4B00BD2BD7 /* MockPaymentDiscount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPaymentDiscount.swift; sourceTree = ""; }; 351B517926D44FF000BD2BD7 /* MockRequestFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRequestFetcher.swift; sourceTree = ""; }; + 352137312CDBA2A700FE961B /* FeatureEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureEvent.swift; sourceTree = ""; }; 3525D8A32C4AB3D500C21D99 /* CustomerCenterEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterEnvironment.swift; sourceTree = ""; }; 352B7D7827BD919B002A47DD /* DangerousSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DangerousSettings.swift; sourceTree = ""; }; 3530C18822653E8F00D6DF52 /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; @@ -3552,6 +3554,7 @@ 353DE0042CCA4EA600A8F632 /* Events */ = { isa = PBXGroup; children = ( + 352137312CDBA2A700FE961B /* FeatureEvent.swift */, 4FE6FEE42AA940B700780B45 /* StoredEvent.swift */, 4FD368B52AA7D09C00F63354 /* StoredEventSerializer.swift */, 353DE0052CCA4EAE00A8F632 /* Networking */, @@ -5735,6 +5738,7 @@ 2C0B98CD2797070B00C5874F /* PromotionalOffer.swift in Sources */, 57DC9F4627CC2E4900DA6AF9 /* HTTPRequest.swift in Sources */, 2DC5623024EC63730031F69B /* OperationDispatcher.swift in Sources */, + 352137322CDBA2AA00FE961B /* FeatureEvent.swift in Sources */, 35D159CB2BC4396F004D8061 /* DiagnosticsPostOperation.swift in Sources */, 575642B62910116900719219 /* EligibilityStrings.swift in Sources */, 4F87610F2A5C9E490006FA14 /* PaywallData.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift b/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift index 212f6ca568..928eecfadd 100644 --- a/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift +++ b/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift @@ -34,4 +34,7 @@ protocol ManageSubscriptionsPurchaseType: Sendable { @Sendable func beginRefundRequest(forProduct productID: String) async throws -> RefundRequestStatus + @Sendable + func trackImpression(_ eventData: CustomerCenterEvent.Data) async throws + } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index ea88b48edd..dd0f367841 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -139,6 +139,11 @@ import RevenueCat } } + func trackImpression() { + // swiftlint:disable:next todo + // TODO: implement tracking impression + } + } fileprivate extension String { diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index a11ccbd362..838111d305 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -259,6 +259,10 @@ private final class ManageSubscriptionPurchases: ManageSubscriptionsPurchaseType await Purchases.shared.products(productIdentifiers) } + func trackImpression(_ eventData: CustomerCenterEvent.Data) async throws { + await Purchases.shared.track(customerCenterEvent: CustomerCenterEvent.impression(.init(), eventData)) + } + } #endif diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 52db688907..c182ff6ead 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -63,6 +63,9 @@ public struct CustomerCenterView: View { .task { await loadInformationIfNeeded() } + .task { + trackImpression() + } .environmentObject(self.viewModel) } @@ -118,6 +121,10 @@ private extension CustomerCenterView { .applyIf(accentColor != nil, apply: { $0.tint(accentColor) }) } + func trackImpression() { + viewModel.trackImpression() + } + } #if DEBUG diff --git a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift index b07bc2a16a..2a4a35c883 100644 --- a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift +++ b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift @@ -14,7 +14,7 @@ import Foundation /// An event to be sent by the `RevenueCatUI` SDK. -public enum CustomerCenterEvent { +public enum CustomerCenterEvent: FeatureEvent { // swiftlint:disable type_name @@ -26,9 +26,19 @@ public enum CustomerCenterEvent { /// An identifier that represents a paywall session. public typealias SessionID = UUID - /// A `CustomerCenterView` was displayed. + var feature: Feature { + return .customerCenter + } + + /// A ``CustomerCenterView`` was displayed. case impression(CreationData, Data) + /// A feedback survey was completed with a particular option. + case surveyCompleted(CreationData, Data) + + /// A ``CustomerCenterView`` was closed. + case close(CreationData, Data) + } extension CustomerCenterEvent { @@ -84,6 +94,8 @@ extension CustomerCenterEvent { public var creationData: CreationData { switch self { case let .impression(creationData, _): return creationData + case let .surveyCompleted(creationData, _): return creationData + case let .close(creationData, _): return creationData } } @@ -91,6 +103,8 @@ extension CustomerCenterEvent { public var data: Data { switch self { case let .impression(_, data): return data + case let .surveyCompleted(_, data): return data + case let .close(_, data): return data } } diff --git a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift index 63d127113c..4e54d7fb12 100644 --- a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift +++ b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift @@ -15,7 +15,7 @@ import Foundation extension EventsRequest { - struct CustomerCenterEvent: FeatureEvent { + struct CustomerCenterEvent { let id: String? let version: Int @@ -35,6 +35,8 @@ extension EventsRequest.CustomerCenterEvent { enum EventType: String { case impression = "customer_center_impression" + case close = "customer_center_close" + case surveyCompleted = "customer_center_survey_completed" } @@ -73,6 +75,8 @@ private extension CustomerCenterEvent { var eventType: EventsRequest.CustomerCenterEvent.EventType { switch self { case .impression: return .impression + case .close: return .close + case .surveyCompleted: return .surveyCompleted } } diff --git a/Sources/Events/FeatureEvent.swift b/Sources/Events/FeatureEvent.swift new file mode 100644 index 0000000000..c941b45a36 --- /dev/null +++ b/Sources/Events/FeatureEvent.swift @@ -0,0 +1,18 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// FeatureEvent.swift +// +// Created by Cesar de la Vega on 6/11/24. + +protocol FeatureEvent: Encodable, Sendable { + + var feature: Feature { get } + +} diff --git a/Sources/Events/Networking/EventsRequest.swift b/Sources/Events/Networking/EventsRequest.swift index 5b434c39f5..7ca78c03e1 100644 --- a/Sources/Events/Networking/EventsRequest.swift +++ b/Sources/Events/Networking/EventsRequest.swift @@ -42,13 +42,4 @@ struct EventsRequest { } -protocol FeatureEvent: Encodable { - - var id: String? { get } - var version: Int { get } - var appUserID: String { get } - var sessionID: String { get } - -} - extension EventsRequest: HTTPRequestBody {} diff --git a/Sources/Paywalls/Events/Networking/EventsRequest+Paywall.swift b/Sources/Paywalls/Events/Networking/EventsRequest+Paywall.swift index ac243e9c43..cb7ec9048e 100644 --- a/Sources/Paywalls/Events/Networking/EventsRequest+Paywall.swift +++ b/Sources/Paywalls/Events/Networking/EventsRequest+Paywall.swift @@ -15,7 +15,7 @@ import Foundation extension EventsRequest { - struct PaywallEvent: FeatureEvent { + struct PaywallEvent { let id: String? let version: Int diff --git a/Sources/Paywalls/Events/PaywallEvent.swift b/Sources/Paywalls/Events/PaywallEvent.swift index 33d891788e..b865585428 100644 --- a/Sources/Paywalls/Events/PaywallEvent.swift +++ b/Sources/Paywalls/Events/PaywallEvent.swift @@ -14,7 +14,7 @@ import Foundation /// An event to be sent by the `RevenueCatUI` SDK. -public enum PaywallEvent { +public enum PaywallEvent: FeatureEvent { // swiftlint:disable type_name @@ -26,6 +26,10 @@ public enum PaywallEvent { /// An identifier that represents a paywall session. public typealias SessionID = UUID + var feature: Feature { + return .paywalls + } + /// A `PaywallView` was displayed. case impression(CreationData, Data) diff --git a/Sources/Paywalls/Events/PaywallEventsManager.swift b/Sources/Paywalls/Events/PaywallEventsManager.swift index 45d5bc7486..767dc17ded 100644 --- a/Sources/Paywalls/Events/PaywallEventsManager.swift +++ b/Sources/Paywalls/Events/PaywallEventsManager.swift @@ -16,7 +16,7 @@ import Foundation protocol PaywallEventsManagerType { @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) - func track(paywallEvent: PaywallEvent) async + func track(featureEvent: FeatureEvent) async /// - Throws: if posting events fails /// - Returns: the number of events posted @@ -44,10 +44,10 @@ actor PaywallEventsManager: PaywallEventsManagerType { self.store = store } - func track(paywallEvent: PaywallEvent) async { - guard let event: StoredEvent = .init(event: paywallEvent, + func track(featureEvent: FeatureEvent) async { + guard let event: StoredEvent = .init(event: featureEvent, userID: self.userProvider.currentAppUserID, - feature: .paywalls) else { + feature: featureEvent.feature) else { Logger.error(Strings.paywalls.event_cannot_serialize) return } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 7acbaaea52..d25071fc8d 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1257,7 +1257,12 @@ public extension Purchases { /// Used by `RevenueCatUI` to keep track of ``PaywallEvent``s. func track(paywallEvent: PaywallEvent) async { self.purchasesOrchestrator.track(paywallEvent: paywallEvent) - await self.paywallEventsManager?.track(paywallEvent: paywallEvent) + await self.paywallEventsManager?.track(featureEvent: paywallEvent) + } + + /// Used by `RevenueCatUI` to keep track of ``CustomerCenterEvent``s. + func track(customerCenterEvent: CustomerCenterEvent) async { + await self.paywallEventsManager?.track(featureEvent: customerCenterEvent) } /// Used by `RevenueCatUI` to download customer center data From 6a7d6e83ed6a70dba85cc07b3ea3deea39afc066 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 27 Nov 2024 15:20:18 +0100 Subject: [PATCH 04/17] cleanup --- .../CustomerCenterPurchasesType.swift | 3 +++ .../ManageSubscriptionsPurchaseType.swift | 3 --- .../Data/CustomerCenterPurchases.swift | 5 ++++ .../ViewModels/CustomerCenterViewModel.swift | 23 +++++++++---------- .../ManageSubscriptionsViewModel.swift | 4 ---- .../Views/CustomerCenterView.swift | 5 +++- .../Events/CustomerCenterEvent.swift | 11 ++++----- 7 files changed, 28 insertions(+), 26 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift index 21e155ed48..1f730a3b4b 100644 --- a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift +++ b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift @@ -34,4 +34,7 @@ protocol CustomerCenterPurchasesType: Sendable { promotionalOffer: PromotionalOffer ) async throws -> PurchaseResultData + @Sendable + func track(customerCenterEvent: CustomerCenterEvent) async throws + } diff --git a/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift b/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift index 928eecfadd..212f6ca568 100644 --- a/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift +++ b/RevenueCatUI/CustomerCenter/Abstractions/ManageSubscriptionsPurchaseType.swift @@ -34,7 +34,4 @@ protocol ManageSubscriptionsPurchaseType: Sendable { @Sendable func beginRefundRequest(forProduct productID: String) async throws -> RefundRequestStatus - @Sendable - func trackImpression(_ eventData: CustomerCenterEvent.Data) async throws - } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift index c2a43ae59b..e4ed4144de 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift @@ -43,4 +43,9 @@ final class CustomerCenterPurchases: CustomerCenterPurchasesType { promotionalOffer: promotionalOffer ) } + + func track(customerCenterEvent: CustomerCenterEvent) async throws { + await Purchases.shared.track(customerCenterEvent: customerCenterEvent) + } + } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index dd0f367841..92cb0508f5 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -26,7 +26,6 @@ import RevenueCat // We fail open. private static let defaultAppIsLatestVersion = true - typealias CustomerInfoFetcher = @Sendable () async throws -> CustomerInfo typealias CurrentVersionFetcher = () -> String? private lazy var currentAppVersion: String? = currentVersionFetcher() @@ -37,6 +36,7 @@ import RevenueCat private(set) var hasAppleEntitlement: Bool = false @Published private(set) var appIsLatestVersion: Bool = defaultAppIsLatestVersion + private(set) var purchasesProvider: CustomerCenterPurchasesType // @PublicForExternalTesting @Published @@ -68,7 +68,6 @@ import RevenueCat return state != .notLoaded && configuration != nil } - private var customerInfoFetcher: CustomerInfoFetcher private let currentVersionFetcher: CurrentVersionFetcher internal let customerCenterActionHandler: CustomerCenterActionHandler? @@ -76,18 +75,15 @@ import RevenueCat init( customerCenterActionHandler: CustomerCenterActionHandler?, - customerInfoFetcher: @escaping CustomerInfoFetcher = { - guard Purchases.isConfigured else { throw PaywallError.purchasesNotConfigured } - return try await Purchases.shared.customerInfo() - }, currentVersionFetcher: @escaping CurrentVersionFetcher = { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - } + }, + purchasesProvider: CustomerCenterPurchasesType = CustomerCenterPurchases() ) { self.state = .notLoaded - self.customerInfoFetcher = customerInfoFetcher self.currentVersionFetcher = currentVersionFetcher self.customerCenterActionHandler = customerCenterActionHandler + self.purchasesProvider = purchasesProvider } #if DEBUG @@ -106,7 +102,7 @@ import RevenueCat func loadHasActivePurchases() async { do { - let customerInfo = try await self.customerInfoFetcher() + let customerInfo = try await purchasesProvider.customerInfo() self.hasActiveProducts = customerInfo.activeSubscriptions.count > 0 || customerInfo.nonSubscriptions.count > 0 self.hasAppleEntitlement = customerInfo.entitlements.active.contains { entitlement in @@ -139,9 +135,12 @@ import RevenueCat } } - func trackImpression() { - // swiftlint:disable:next todo - // TODO: implement tracking impression + func trackImpression(_ eventData: CustomerCenterEvent.Data) { + let event = CustomerCenterEvent.impression(CustomerCenterEvent.CreationData(), eventData) + + Task.detached(priority: .background) { [purchasesProvider = self.purchasesProvider] in + try await purchasesProvider.track(customerCenterEvent: event) + } } } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 838111d305..a11ccbd362 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -259,10 +259,6 @@ private final class ManageSubscriptionPurchases: ManageSubscriptionsPurchaseType await Purchases.shared.products(productIdentifiers) } - func trackImpression(_ eventData: CustomerCenterEvent.Data) async throws { - await Purchases.shared.track(customerCenterEvent: CustomerCenterEvent.impression(.init(), eventData)) - } - } #endif diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index c182ff6ead..78d1397f01 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -122,7 +122,10 @@ private extension CustomerCenterView { } func trackImpression() { - viewModel.trackImpression() + let eventData = CustomerCenterEvent.Data(sessionID: CustomerCenterEvent.SessionID(), + locale: .current, + darkMode: self.colorScheme == .dark) + viewModel.trackImpression(eventData) } } diff --git a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift index 2a4a35c883..935f9ad365 100644 --- a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift +++ b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift @@ -47,8 +47,8 @@ extension CustomerCenterEvent { public struct CreationData { // swiftlint:disable missing_docs - public var id: ID - public var date: Date + var id: ID + var date: Date public init( id: ID = .init(), @@ -68,11 +68,10 @@ extension CustomerCenterEvent { public struct Data { // swiftlint:disable missing_docs - public var sessionIdentifier: SessionID - public var localeIdentifier: String - public var darkMode: Bool + var sessionIdentifier: SessionID + var localeIdentifier: String + var darkMode: Bool - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) public init( sessionID: SessionID, locale: Locale, From febe8cb29b2c61582d9c6e2ab2f17652a4ce43d0 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 27 Nov 2024 15:30:11 +0100 Subject: [PATCH 05/17] fix decoding --- .../Events/EventsRequest+CustomerCenter.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift index 4e54d7fb12..19ca8c29ac 100644 --- a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift +++ b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift @@ -42,11 +42,13 @@ extension EventsRequest.CustomerCenterEvent { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) init?(storedEvent: StoredEvent) { - guard let eventData = storedEvent.encodedEvent.value as? [String: Any] else { + guard let jsonData = storedEvent.encodedEvent.data(using: .utf8) else { + Logger.error(Strings.paywalls.event_cannot_get_encoded_event) return nil } + do { - let customerCenterEvent: CustomerCenterEvent = try JSONDecoder.default.decode(dictionary: eventData) + let customerCenterEvent = try JSONDecoder.default.decode(CustomerCenterEvent.self, from: jsonData) let creationData = customerCenterEvent.creationData let data = customerCenterEvent.data From 95ab252cbfa598a9744824f49ec7cd7d0ffbe0dd Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 27 Nov 2024 16:58:57 +0100 Subject: [PATCH 06/17] adds more properties --- RevenueCat.xcodeproj/project.pbxproj | 4 ++ .../CustomerCenterPurchasesType.swift | 4 ++ .../Data/CustomerCenterPurchases.swift | 10 +++++ .../View+PresentCustomerCenter.swift | 20 ---------- .../ViewModels/CustomerCenterViewModel.swift | 9 ++++- .../Views/CustomerCenterView.swift | 16 +++++--- .../CustomerCenterPresentationMode.swift | 40 +++++++++++++++++++ .../Events/CustomerCenterEvent.swift | 14 +++++-- .../Events/EventsRequest+CustomerCenter.swift | 18 ++++++--- 9 files changed, 98 insertions(+), 37 deletions(-) create mode 100644 Sources/CustomerCenter/CustomerCenterPresentationMode.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 4cdc8e7797..3ef766b2e7 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -276,6 +276,7 @@ 354895D6267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */; }; 3551E39D2C4A6A1400D27C25 /* TintedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3551E39C2C4A6A1400D27C25 /* TintedProgressView.swift */; }; 35549323269E298B005F9AE9 /* OfferingsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35549322269E298B005F9AE9 /* OfferingsFactory.swift */; }; + 356523A82CF7719C00B6E3EA /* CustomerCenterPresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356523A72CF7719B00B6E3EA /* CustomerCenterPresentationMode.swift */; }; 356979E02CCFDAA100EE6A9E /* CustomerInfoFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356979DF2CCFDA9C00EE6A9E /* CustomerInfoFixtures.swift */; }; 356E2DE82CD3CF930055AABB /* StoredEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356E2DE72CD3CF8F0055AABB /* StoredEventTests.swift */; }; 357349012C3BEB5C000EEB86 /* CustomerCenterConfigDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */; }; @@ -1534,6 +1535,7 @@ 354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReservedSubscriberAttributes.swift; sourceTree = ""; }; 3551E39C2C4A6A1400D27C25 /* TintedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TintedProgressView.swift; sourceTree = ""; }; 35549322269E298B005F9AE9 /* OfferingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsFactory.swift; sourceTree = ""; }; + 356523A72CF7719B00B6E3EA /* CustomerCenterPresentationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterPresentationMode.swift; sourceTree = ""; }; 356979DF2CCFDA9C00EE6A9E /* CustomerInfoFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerInfoFixtures.swift; sourceTree = ""; }; 356E2DE72CD3CF8F0055AABB /* StoredEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredEventTests.swift; sourceTree = ""; }; 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigDataTests.swift; sourceTree = ""; }; @@ -3692,6 +3694,7 @@ 3592E8892C2ED54A00D7F91D /* CustomerCenter */ = { isa = PBXGroup; children = ( + 356523A72CF7719B00B6E3EA /* CustomerCenterPresentationMode.swift */, 35F7884C2CC68F5F00080F32 /* Events */, 3592E8882C2ED54A00D7F91D /* CustomerCenterConfigData.swift */, ); @@ -5789,6 +5792,7 @@ FDAC7B532CD3D67600DFC0D9 /* WinBackOfferEligibilityCalculatorType.swift in Sources */, B34605C0279A6E380031CA74 /* CustomerInfoCallback.swift in Sources */, B33CEAA0268CDCC9008A3144 /* ISOPeriodFormatter.swift in Sources */, + 356523A82CF7719C00B6E3EA /* CustomerCenterPresentationMode.swift in Sources */, 4FBBC5682A61E42F0077281F /* NonEmptyStringDecodable.swift in Sources */, 2DDF41A324F6F331005BC22D /* PurchasesReceiptParser.swift in Sources */, 2CB8CF9327BF538F00C34DE3 /* PlatformInfo.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift index 1f730a3b4b..37b8897e2a 100644 --- a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift +++ b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift @@ -37,4 +37,8 @@ protocol CustomerCenterPurchasesType: Sendable { @Sendable func track(customerCenterEvent: CustomerCenterEvent) async throws + func isSandbox() -> Bool + + func appSessionID() -> UUID + } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift index e4ed4144de..25aa256791 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift @@ -48,4 +48,14 @@ final class CustomerCenterPurchases: CustomerCenterPurchasesType { await Purchases.shared.track(customerCenterEvent: customerCenterEvent) } + func isSandbox() -> Bool { + // TODO: implement + return true + } + + func appSessionID() -> UUID { + // TODO: implement + return UUID() + } + } diff --git a/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift b/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift index 1f5daaaf0e..2f39fb6563 100644 --- a/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift +++ b/RevenueCatUI/CustomerCenter/View+PresentCustomerCenter.swift @@ -16,26 +16,6 @@ import SwiftUI #if os(iOS) -/// Warning: This is currently in beta and subject to change. -/// -/// Presentation options to use with the [presentCustomerCenter](x-source-tag://presentCustomerCenter) View modifiers. -public enum CustomerCenterPresentationMode { - - /// Customer center presented using SwiftUI's `.sheet`. - case sheet - - /// Customer center presented using SwiftUI's `.fullScreenCover`. - case fullScreen - -} - -extension CustomerCenterPresentationMode { - - // swiftlint:disable:next missing_docs - public static let `default`: Self = .sheet - -} - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable, message: "RevenueCatUI does not support macOS yet") @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 92cb0508f5..64fab24920 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -135,7 +135,14 @@ import RevenueCat } } - func trackImpression(_ eventData: CustomerCenterEvent.Data) { + func trackImpression(darkMode: Bool, displayMode: CustomerCenterPresentationMode) { + let isSandbox = purchasesProvider.isSandbox() + let appSessionID = purchasesProvider.appSessionID() + let eventData = CustomerCenterEvent.Data(appSessionID: appSessionID, + locale: .current, + darkMode: darkMode, + isSandbox: isSandbox, + displayMode: displayMode) let event = CustomerCenterEvent.impression(CustomerCenterEvent.CreationData(), eventData) Task.detached(priority: .background) { [purchasesProvider = self.purchasesProvider] in diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 78d1397f01..9594676d56 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -33,17 +33,23 @@ public struct CustomerCenterView: View { @Environment(\.colorScheme) private var colorScheme + private let mode: CustomerCenterPresentationMode + /// Create a view to handle common customer support tasks /// - Parameters: /// - customerCenterActionHandler: An optional `CustomerCenterActionHandler` to handle actions /// from the customer center. - public init(customerCenterActionHandler: CustomerCenterActionHandler? = nil) { + public init(customerCenterActionHandler: CustomerCenterActionHandler? = nil, + mode: CustomerCenterPresentationMode = CustomerCenterPresentationMode.default) { self._viewModel = .init(wrappedValue: CustomerCenterViewModel(customerCenterActionHandler: customerCenterActionHandler)) + self.mode = mode } - fileprivate init(viewModel: CustomerCenterViewModel) { + fileprivate init(viewModel: CustomerCenterViewModel, + mode: CustomerCenterPresentationMode = CustomerCenterPresentationMode.default) { self._viewModel = .init(wrappedValue: viewModel) + self.mode = mode } // swiftlint:disable:next missing_docs @@ -122,10 +128,8 @@ private extension CustomerCenterView { } func trackImpression() { - let eventData = CustomerCenterEvent.Data(sessionID: CustomerCenterEvent.SessionID(), - locale: .current, - darkMode: self.colorScheme == .dark) - viewModel.trackImpression(eventData) + viewModel.trackImpression(darkMode: self.colorScheme == .dark, + displayMode: self.mode) } } diff --git a/Sources/CustomerCenter/CustomerCenterPresentationMode.swift b/Sources/CustomerCenter/CustomerCenterPresentationMode.swift new file mode 100644 index 0000000000..cc21c81925 --- /dev/null +++ b/Sources/CustomerCenter/CustomerCenterPresentationMode.swift @@ -0,0 +1,40 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// CustomerCenterPresentationMode.swift +// +// Created by Cesar de la Vega on 27/11/24. + +import Foundation + +#if os(iOS) + +/// Warning: This is currently in beta and subject to change. +/// +/// Presentation options to use with the [presentCustomerCenter](x-source-tag://presentCustomerCenter) View modifiers. +public enum CustomerCenterPresentationMode { + + /// Customer center presented using SwiftUI's `.sheet`. + case sheet + + /// Customer center presented using SwiftUI's `.fullScreenCover`. + case fullScreen + +} + +extension CustomerCenterPresentationMode { + + // swiftlint:disable:next missing_docs + public static let `default`: Self = .sheet + +} + +extension CustomerCenterPresentationMode: Equatable, Codable, Sendable {} + +#endif diff --git a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift index 935f9ad365..82a2a23e97 100644 --- a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift +++ b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift @@ -68,18 +68,24 @@ extension CustomerCenterEvent { public struct Data { // swiftlint:disable missing_docs - var sessionIdentifier: SessionID + var appSessionID: SessionID var localeIdentifier: String var darkMode: Bool + var isSandbox: Bool + var displayMode: CustomerCenterPresentationMode public init( - sessionID: SessionID, + appSessionID: SessionID, locale: Locale, - darkMode: Bool + darkMode: Bool, + isSandbox: Bool, + displayMode: CustomerCenterPresentationMode ) { - self.sessionIdentifier = sessionID + self.appSessionID = appSessionID self.localeIdentifier = locale.identifier self.darkMode = darkMode + self.isSandbox = isSandbox + self.displayMode = displayMode } // swiftlint:enable missing_docs diff --git a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift index 19ca8c29ac..d4c4bbeb90 100644 --- a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift +++ b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift @@ -21,10 +21,12 @@ extension EventsRequest { let version: Int var type: EventType var appUserID: String - var sessionID: String + var appSessionID: String var timestamp: UInt64 var darkMode: Bool - var localeIdentifier: String + var locale: String + var isSandbox: Bool + var displayMode: CustomerCenterPresentationMode } @@ -57,10 +59,12 @@ extension EventsRequest.CustomerCenterEvent { version: Self.version, type: customerCenterEvent.eventType, appUserID: storedEvent.userID, - sessionID: data.sessionIdentifier.uuidString, + appSessionID: data.appSessionID.uuidString, timestamp: creationData.date.millisecondsSince1970, darkMode: data.darkMode, - localeIdentifier: data.localeIdentifier + locale: data.localeIdentifier, + isSandbox: data.isSandbox, + displayMode: data.displayMode ) } catch { return nil @@ -96,10 +100,12 @@ extension EventsRequest.CustomerCenterEvent: Encodable { case version case type case appUserID = "appUserId" - case sessionID = "sessionId" + case appSessionID = "appSessionID" case timestamp case darkMode - case localeIdentifier = "locale" + case locale + case isSandbox = "isSandbox" + case displayMode = "displayMode" } From 97a5058e416a388dd313c0904d6381d546435b3f Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 27 Nov 2024 19:09:37 +0100 Subject: [PATCH 07/17] fixes appSessionId and isSandbox --- .../CustomerCenterPurchasesType.swift | 6 +-- .../Data/CustomerCenterPurchases.swift | 13 ++--- .../ViewModels/CustomerCenterViewModel.swift | 6 +-- .../CustomerCenterPresentationMode.swift | 48 ++++++++++++++++++- .../Events/CustomerCenterEvent.swift | 3 -- .../Events/EventsRequest+CustomerCenter.swift | 7 ++- Sources/Events/StoredEvent.swift | 9 +++- Sources/Logging/Strings/PaywallsStrings.swift | 4 ++ .../Events/PaywallEventsManager.swift | 12 ++++- Sources/Purchasing/Purchases/Purchases.swift | 16 +++++-- 10 files changed, 95 insertions(+), 29 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift index 37b8897e2a..ceb7e92896 100644 --- a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift +++ b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift @@ -20,6 +20,8 @@ import RevenueCat @available(watchOS, unavailable) protocol CustomerCenterPurchasesType: Sendable { + var isSandbox: Bool { get } + @Sendable func customerInfo() async throws -> CustomerInfo @@ -37,8 +39,4 @@ protocol CustomerCenterPurchasesType: Sendable { @Sendable func track(customerCenterEvent: CustomerCenterEvent) async throws - func isSandbox() -> Bool - - func appSessionID() -> UUID - } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift index 25aa256791..83fcb92512 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift @@ -20,6 +20,10 @@ import RevenueCat @available(watchOS, unavailable) final class CustomerCenterPurchases: CustomerCenterPurchasesType { + var isSandbox: Bool { + return Purchases.shared.isSandbox + } + func customerInfo() async throws -> RevenueCat.CustomerInfo { try await Purchases.shared.customerInfo() } @@ -48,14 +52,5 @@ final class CustomerCenterPurchases: CustomerCenterPurchasesType { await Purchases.shared.track(customerCenterEvent: customerCenterEvent) } - func isSandbox() -> Bool { - // TODO: implement - return true - } - - func appSessionID() -> UUID { - // TODO: implement - return UUID() - } } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 64fab24920..585c32d733 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -136,10 +136,8 @@ import RevenueCat } func trackImpression(darkMode: Bool, displayMode: CustomerCenterPresentationMode) { - let isSandbox = purchasesProvider.isSandbox() - let appSessionID = purchasesProvider.appSessionID() - let eventData = CustomerCenterEvent.Data(appSessionID: appSessionID, - locale: .current, + let isSandbox = purchasesProvider.isSandbox + let eventData = CustomerCenterEvent.Data(locale: .current, darkMode: darkMode, isSandbox: isSandbox, displayMode: displayMode) diff --git a/Sources/CustomerCenter/CustomerCenterPresentationMode.swift b/Sources/CustomerCenter/CustomerCenterPresentationMode.swift index cc21c81925..a38525f9d8 100644 --- a/Sources/CustomerCenter/CustomerCenterPresentationMode.swift +++ b/Sources/CustomerCenter/CustomerCenterPresentationMode.swift @@ -35,6 +35,52 @@ extension CustomerCenterPresentationMode { } -extension CustomerCenterPresentationMode: Equatable, Codable, Sendable {} +extension CustomerCenterPresentationMode { + + var identifier: String { + switch self { + case .fullScreen: return "full_screen" + case .sheet: return "sheet" + } + } + +} + +// MARK: - Extensions + +extension CustomerCenterPresentationMode: CaseIterable { + + // swiftlint:disable:next missing_docs + public static var allCases: [CustomerCenterPresentationMode] { + return [ + .fullScreen, + .sheet + ] + } + +} + +extension CustomerCenterPresentationMode: Equatable, Sendable {} + +extension CustomerCenterPresentationMode: Codable { + + // swiftlint:disable:next missing_docs + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.identifier) + } + + // swiftlint:disable:next missing_docs + public init(from decoder: Decoder) throws { + let identifier = try decoder.singleValueContainer().decode(String.self) + + self = try Self.modesByIdentifier[identifier] + .orThrow(CodableError.unexpectedValue(Self.self, identifier)) + } + + private static let modesByIdentifier: [String: Self] = Set(Self.allCases) + .dictionaryWithKeys(\.identifier) + +} #endif diff --git a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift index 82a2a23e97..02f4078eb5 100644 --- a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift +++ b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift @@ -68,20 +68,17 @@ extension CustomerCenterEvent { public struct Data { // swiftlint:disable missing_docs - var appSessionID: SessionID var localeIdentifier: String var darkMode: Bool var isSandbox: Bool var displayMode: CustomerCenterPresentationMode public init( - appSessionID: SessionID, locale: Locale, darkMode: Bool, isSandbox: Bool, displayMode: CustomerCenterPresentationMode ) { - self.appSessionID = appSessionID self.localeIdentifier = locale.identifier self.darkMode = darkMode self.isSandbox = isSandbox diff --git a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift index d4c4bbeb90..ec9b7b5a3b 100644 --- a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift +++ b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift @@ -44,6 +44,11 @@ extension EventsRequest.CustomerCenterEvent { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) init?(storedEvent: StoredEvent) { + guard let appSessionID = storedEvent.appSessionID else { + Logger.error(Strings.paywalls.event_missing_app_session_id) + return nil + } + guard let jsonData = storedEvent.encodedEvent.data(using: .utf8) else { Logger.error(Strings.paywalls.event_cannot_get_encoded_event) return nil @@ -59,7 +64,7 @@ extension EventsRequest.CustomerCenterEvent { version: Self.version, type: customerCenterEvent.eventType, appUserID: storedEvent.userID, - appSessionID: data.appSessionID.uuidString, + appSessionID: appSessionID.uuidString, timestamp: creationData.date.millisecondsSince1970, darkMode: data.darkMode, locale: data.localeIdentifier, diff --git a/Sources/Events/StoredEvent.swift b/Sources/Events/StoredEvent.swift index 7423fc6871..8b49f9fb05 100644 --- a/Sources/Events/StoredEvent.swift +++ b/Sources/Events/StoredEvent.swift @@ -19,8 +19,9 @@ struct StoredEvent { private(set) var encodedEvent: String private(set) var userID: String private(set) var feature: Feature + private(set) var appSessionID: UUID? - init?(event: T, userID: String, feature: Feature) { + init?(event: T, userID: String, feature: Feature, appSessionID: UUID?) { guard let encodedJSON = try? event.encodedJSON else { return nil } @@ -28,6 +29,7 @@ struct StoredEvent { self.encodedEvent = encodedJSON self.userID = userID self.feature = feature + self.appSessionID = appSessionID } } @@ -50,6 +52,7 @@ extension StoredEvent: Codable { case encodedEvent = "event" case userID = "userId" case feature + case appSessionID = "appSessionId" } @@ -81,6 +84,10 @@ extension StoredEvent: Codable { } else { self.feature = .paywalls } + + if let appSessionID = try container.decodeIfPresent(UUID.self, forKey: .appSessionID) { + self.appSessionID = appSessionID + } } } diff --git a/Sources/Logging/Strings/PaywallsStrings.swift b/Sources/Logging/Strings/PaywallsStrings.swift index 89af28779b..389f2cf73a 100644 --- a/Sources/Logging/Strings/PaywallsStrings.swift +++ b/Sources/Logging/Strings/PaywallsStrings.swift @@ -45,6 +45,7 @@ enum PaywallsStrings { case event_cannot_serialize case event_cannot_get_encoded_event case event_cannot_deserialize(Error) + case event_missing_app_session_id } @@ -115,6 +116,9 @@ extension PaywallsStrings: LogMessage { case let .event_cannot_deserialize(error): return "Couldn't deserialize PaywallEvent from storage. Error: \((error as NSError).description)" + + case .event_missing_app_session_id: + return "Event is missing the app session ID." } } diff --git a/Sources/Paywalls/Events/PaywallEventsManager.swift b/Sources/Paywalls/Events/PaywallEventsManager.swift index 767dc17ded..9912312e56 100644 --- a/Sources/Paywalls/Events/PaywallEventsManager.swift +++ b/Sources/Paywalls/Events/PaywallEventsManager.swift @@ -23,6 +23,8 @@ protocol PaywallEventsManagerType { @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) func flushEvents(count: Int) async throws -> Int + func resetAppSessionID() async + } @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) @@ -31,6 +33,7 @@ actor PaywallEventsManager: PaywallEventsManagerType { private let internalAPI: InternalAPI private let userProvider: CurrentUserProvider private let store: PaywallEventStoreType + private var appSessionID: UUID private var flushInProgress = false @@ -42,12 +45,19 @@ actor PaywallEventsManager: PaywallEventsManagerType { self.internalAPI = internalAPI self.userProvider = userProvider self.store = store + self.appSessionID = UUID() + } + + + func resetAppSessionID() { + self.appSessionID = UUID() } func track(featureEvent: FeatureEvent) async { guard let event: StoredEvent = .init(event: featureEvent, userID: self.userProvider.currentAppUserID, - feature: featureEvent.feature) else { + feature: featureEvent.feature, + appSessionID: self.appSessionID) else { Logger.error(Strings.paywalls.event_cannot_serialize) return } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index d25071fc8d..c086c902f4 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -769,6 +769,8 @@ public extension Purchases { @objc var isAnonymous: Bool { self.identityManager.currentUserIsAnonymous } + @objc var isSandbox: Bool { return self.systemInfo.isSandbox } + @objc func getOfferings(completion: @escaping (Offerings?, PublicError?) -> Void) { self.getOfferings(fetchPolicy: .default, completion: completion) } @@ -832,6 +834,10 @@ public extension Purchases { self.systemInfo.isApplicationBackgrounded { isAppBackgrounded in self.updateOfferingsCache(isAppBackgrounded: isAppBackgrounded) } + + Task { + await self.paywallEventsManager?.resetAppSessionID() + } } } @@ -865,6 +871,10 @@ public extension Purchases { return } + Task { + await self.paywallEventsManager?.resetAppSessionID() + } + self.updateAllCaches { completion?($0.value, $0.error) } @@ -1249,7 +1259,7 @@ public extension Purchases { // swiftlint:enable missing_docs -// MARK: - Paywalls +// MARK: - Paywalls & Customer Center @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) public extension Purchases { @@ -1789,10 +1799,6 @@ internal extension Purchases { return self.productsManager.requestTimeout } - var isSandbox: Bool { - return self.systemInfo.isSandbox - } - var observerMode: Bool { return self.systemInfo.observerMode } From c56474fcf9fb91ec70227bf216a5c6efbcc7e2a9 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 27 Nov 2024 19:10:38 +0100 Subject: [PATCH 08/17] linter --- RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift | 1 - Sources/Paywalls/Events/PaywallEventsManager.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift index 83fcb92512..f4ee40cda8 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift @@ -52,5 +52,4 @@ final class CustomerCenterPurchases: CustomerCenterPurchasesType { await Purchases.shared.track(customerCenterEvent: customerCenterEvent) } - } diff --git a/Sources/Paywalls/Events/PaywallEventsManager.swift b/Sources/Paywalls/Events/PaywallEventsManager.swift index 9912312e56..b6328e1fb5 100644 --- a/Sources/Paywalls/Events/PaywallEventsManager.swift +++ b/Sources/Paywalls/Events/PaywallEventsManager.swift @@ -48,7 +48,6 @@ actor PaywallEventsManager: PaywallEventsManagerType { self.appSessionID = UUID() } - func resetAppSessionID() { self.appSessionID = UUID() } From bcac529f57be6636f115e4c93806ee2e43cf666f Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 28 Nov 2024 11:49:19 +0100 Subject: [PATCH 09/17] fix test --- .../Mocks/MockPaywallEventsManager.swift | 14 ++++- .../Events/PaywallEventStoreTests.swift | 3 +- .../Events/PaywallEventsBackendTests.swift | 9 ++- .../Events/PaywallEventsManagerTests.swift | 59 ++++++++++++++----- .../Events/PaywallEventsRequestTests.swift | 14 +++-- .../Events/StoredEventSerializerTests.swift | 14 +++-- 6 files changed, 83 insertions(+), 30 deletions(-) diff --git a/Tests/UnitTests/Mocks/MockPaywallEventsManager.swift b/Tests/UnitTests/Mocks/MockPaywallEventsManager.swift index 35de67b5cb..e00a0a9566 100644 --- a/Tests/UnitTests/Mocks/MockPaywallEventsManager.swift +++ b/Tests/UnitTests/Mocks/MockPaywallEventsManager.swift @@ -17,10 +17,10 @@ import Foundation @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) actor MockPaywallEventsManager: PaywallEventsManagerType { - var trackedEvents: [PaywallEvent] = [] + var trackedEvents: [FeatureEvent] = [] - func track(paywallEvent: PaywallEvent) async { - self.trackedEvents.append(paywallEvent) + func track(featureEvent: FeatureEvent) async { + self.trackedEvents.append(featureEvent) } var invokedFlushEvents = false @@ -33,4 +33,12 @@ actor MockPaywallEventsManager: PaywallEventsManagerType { return 0 } + var invokedResetAppSessionID = false + var invokedResetAppSessionIDCount = 0 + + func resetAppSessionID() async { + self.invokedResetAppSessionID = true + self.invokedResetAppSessionIDCount += 1 + } + } diff --git a/Tests/UnitTests/Paywalls/Events/PaywallEventStoreTests.swift b/Tests/UnitTests/Paywalls/Events/PaywallEventStoreTests.swift index 2e877f7127..f8ebaed3c8 100644 --- a/Tests/UnitTests/Paywalls/Events/PaywallEventStoreTests.swift +++ b/Tests/UnitTests/Paywalls/Events/PaywallEventStoreTests.swift @@ -284,7 +284,8 @@ private extension StoredEvent { let event = PaywallEvent.randomImpressionEvent() return .init(event: event, userID: UUID().uuidString, - feature: .paywalls)! + feature: .paywalls, + appSessionID: UUID())! } } diff --git a/Tests/UnitTests/Paywalls/Events/PaywallEventsBackendTests.swift b/Tests/UnitTests/Paywalls/Events/PaywallEventsBackendTests.swift index 981f98cfe3..75a8dcad23 100644 --- a/Tests/UnitTests/Paywalls/Events/PaywallEventsBackendTests.swift +++ b/Tests/UnitTests/Paywalls/Events/PaywallEventsBackendTests.swift @@ -43,7 +43,8 @@ class BackendPaywallEventTests: BaseBackendTests { let event = PaywallEvent.impression(Self.eventCreation1, Self.eventData1) let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, userID: Self.userID, - feature: .paywalls)) + feature: .paywalls, + appSessionID: UUID())) let error = waitUntilValue { completion in self.internalAPI.postPaywallEvents(events: [storedEvent], completion: completion) @@ -56,11 +57,13 @@ class BackendPaywallEventTests: BaseBackendTests { let event1 = PaywallEvent.impression(Self.eventCreation1, Self.eventData1) let storedEvent1: StoredEvent = try XCTUnwrap(.init(event: event1, userID: Self.userID, - feature: .paywalls)) + feature: .paywalls, + appSessionID: UUID())) let event2 = PaywallEvent.close(Self.eventCreation2, Self.eventData2) let storedEvent2: StoredEvent = try XCTUnwrap(.init(event: event2, userID: Self.userID, - feature: .paywalls)) + feature: .paywalls, + appSessionID: UUID())) let error = waitUntilValue { completion in self.internalAPI.postPaywallEvents(events: [storedEvent1, storedEvent2], diff --git a/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift b/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift index 476ef22268..e8eb829983 100644 --- a/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift +++ b/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift @@ -46,11 +46,14 @@ class PaywallEventsManagerTests: TestCase { func testTrackEvent() async throws { let event: PaywallEvent = .impression(.random(), .random()) - await self.manager.track(paywallEvent: event) + await self.manager.track(featureEvent: event) let events = await self.store.storedEvents expect(events) == [ - try XCTUnwrap(.init(event: event, userID: Self.userID, feature: .paywalls)) + try XCTUnwrap(.init(event: event, + userID: Self.userID, + feature: .paywalls, + appSessionID: UUID())) ] } @@ -58,13 +61,19 @@ class PaywallEventsManagerTests: TestCase { let event1: PaywallEvent = .impression(.random(), .random()) let event2: PaywallEvent = .close(.random(), .random()) - await self.manager.track(paywallEvent: event1) - await self.manager.track(paywallEvent: event2) + await self.manager.track(featureEvent: event1) + await self.manager.track(featureEvent: event2) let events = await self.store.storedEvents expect(events) == [ - try XCTUnwrap(.init(event: event1, userID: Self.userID, feature: .paywalls)), - try XCTUnwrap(.init(event: event2, userID: Self.userID, feature: .paywalls)) + try XCTUnwrap(.init(event: event1, + userID: Self.userID, + feature: .paywalls, + appSessionID: UUID())), + try XCTUnwrap(.init(event: event2, + userID: Self.userID, + feature: .paywalls, + appSessionID: UUID())) ] } @@ -85,7 +94,8 @@ class PaywallEventsManagerTests: TestCase { expect(self.api.invokedPostPaywallEvents) == true expect(self.api.invokedPostPaywallEventsParameters) == [[try XCTUnwrap(.init(event: event, userID: Self.userID, - feature: .paywalls))]] + feature: .paywalls, + appSessionID: UUID()))]] await self.verifyEmptyStore() } @@ -102,8 +112,14 @@ class PaywallEventsManagerTests: TestCase { expect(self.api.invokedPostPaywallEvents) == true expect(self.api.invokedPostPaywallEventsParameters) == [ - [try XCTUnwrap(.init(event: event1, userID: Self.userID, feature: .paywalls))], - [try XCTUnwrap(.init(event: event2, userID: Self.userID, feature: .paywalls))] + [try XCTUnwrap(.init(event: event1, + userID: Self.userID, + feature: .paywalls, + appSessionID: UUID()))], + [try XCTUnwrap(.init(event: event2, + userID: Self.userID, + feature: .paywalls, + appSessionID: UUID()))] ] await self.verifyEmptyStore() @@ -111,7 +127,10 @@ class PaywallEventsManagerTests: TestCase { func testFlushOnlyOneEventPostsFirstOne() async throws { let event = await self.storeRandomEvent() - let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, userID: Self.userID, feature: .paywalls)) + let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, + userID: Self.userID, + feature: .paywalls, + appSessionID: UUID())) _ = await self.storeRandomEvent() _ = await self.storeRandomEvent() @@ -129,7 +148,10 @@ class PaywallEventsManagerTests: TestCase { func testFlushWithUnsuccessfulPostError() async throws { let event = await self.storeRandomEvent() - let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, userID: Self.userID, feature: .paywalls)) + let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, + userID: Self.userID, + feature: .paywalls, + appSessionID: UUID())) let expectedError: NetworkError = .offlineConnection() self.api.stubbedPostPaywallEventsCompletionResult = .networkError(expectedError) @@ -189,10 +211,14 @@ class PaywallEventsManagerTests: TestCase { expect(self.api.invokedPostPaywallEvents) == true let expectedEvent: StoredEvent = try XCTUnwrap(.init(event: event1, userID: Self.userID, - feature: .paywalls)) + feature: .paywalls, + appSessionID: UUID())) expect(self.api.invokedPostPaywallEventsParameters) == [[expectedEvent]] - await self.verifyEvents([try XCTUnwrap(.init(event: event2, userID: Self.userID, feature: .paywalls))]) + await self.verifyEvents([try XCTUnwrap(.init(event: event2, + userID: Self.userID, + feature: .paywalls, + appSessionID: UUID()))]) } #if swift(>=5.9) @@ -233,7 +259,10 @@ class PaywallEventsManagerTests: TestCase { expect(self.api.invokedPostPaywallEvents) == true expect(self.api.invokedPostPaywallEventsParameters).to(haveCount(1)) expect(self.api.invokedPostPaywallEventsParameters.onlyElement) == [ - try XCTUnwrap(.init(event: event1, userID: Self.userID, feature: .paywalls)) + try XCTUnwrap(.init(event: event1, + userID: Self.userID, + feature: .paywalls, + appSessionID: UUID())) ] self.logger.verifyMessageWasLogged( @@ -257,7 +286,7 @@ private extension PaywallEventsManagerTests { func storeRandomEvent() async -> PaywallEvent { let event: PaywallEvent = .impression(.random(), .random()) - await self.manager.track(paywallEvent: event) + await self.manager.track(featureEvent: event) return event } diff --git a/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift b/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift index 4b16193ac8..f1daa498ac 100644 --- a/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift +++ b/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift @@ -30,7 +30,8 @@ class PaywallEventsRequestTests: TestCase { let event = PaywallEvent.impression(Self.eventCreationData, Self.eventData) let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, userID: Self.userID, - feature: .paywalls)) + feature: .paywalls, + appSessionID: UUID())) let requestEvent: EventsRequest.PaywallEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) @@ -40,7 +41,8 @@ class PaywallEventsRequestTests: TestCase { let event = PaywallEvent.cancel(Self.eventCreationData, Self.eventData) let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, userID: Self.userID, - feature: .paywalls)) + feature: .paywalls, + appSessionID: UUID())) let requestEvent: EventsRequest.PaywallEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) @@ -51,7 +53,8 @@ class PaywallEventsRequestTests: TestCase { Self.eventData) let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, userID: Self.userID, - feature: .paywalls)) + feature: .paywalls, + appSessionID: UUID())) let requestEvent: EventsRequest.PaywallEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) @@ -73,7 +76,10 @@ class PaywallEventsRequestTests: TestCase { ) let paywallEvent = PaywallEvent.impression(paywallEventCreationData, paywallEventData) - let storedEvent = try XCTUnwrap(StoredEvent(event: paywallEvent, userID: expectedUserID, feature: .paywalls)) + let storedEvent = try XCTUnwrap(StoredEvent(event: paywallEvent, + userID: expectedUserID, + feature: .paywalls, + appSessionID: UUID())) let serializedEvent = try StoredEventSerializer.encode(storedEvent) let deserializedEvent = try StoredEventSerializer.decode(serializedEvent) expect(deserializedEvent.userID) == expectedUserID diff --git a/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift b/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift index 9f570335d6..667c36ad9e 100644 --- a/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift +++ b/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift @@ -29,7 +29,8 @@ class StoredEventSerializerTests: TestCase { let originalEvent = PaywallEvent.impression(.random(), .random()) let event: StoredEvent = try XCTUnwrap(.init(event: originalEvent, userID: Self.userID, - feature: .paywalls)) + feature: .paywalls, + appSessionID: UUID())) expect(try event.encodeAndDecode()) == event } @@ -38,7 +39,8 @@ class StoredEventSerializerTests: TestCase { let originalEvent = PaywallEvent.cancel(.random(), .random()) let event: StoredEvent = try XCTUnwrap(.init(event: originalEvent, userID: Self.userID, - feature: .paywalls)) + feature: .paywalls, + appSessionID: UUID())) expect(try event.encodeAndDecode()) == event } @@ -47,7 +49,8 @@ class StoredEventSerializerTests: TestCase { let originalEvent = PaywallEvent.close(.random(), .random()) let event: StoredEvent = try XCTUnwrap(.init(event: originalEvent, userID: Self.userID, - feature: .paywalls)) + feature: .paywalls, + appSessionID: UUID())) expect(try event.encodeAndDecode()) == event } @@ -68,7 +71,10 @@ class StoredEventSerializerTests: TestCase { ) let paywallEvent = PaywallEvent.impression(paywallEventCreationData, paywallEventData) - let storedEvent = try XCTUnwrap(StoredEvent(event: paywallEvent, userID: expectedUserID, feature: .paywalls)) + let storedEvent = try XCTUnwrap(StoredEvent(event: paywallEvent, + userID: expectedUserID, + feature: .paywalls, + appSessionID: UUID())) let serializedEvent = try StoredEventSerializer.encode(storedEvent) let deserializedEvent = try StoredEventSerializer.decode(serializedEvent) expect(deserializedEvent.userID) == expectedUserID From 2f3907c2fb235de461e195c04457853a7af6bad7 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 28 Nov 2024 11:59:36 +0100 Subject: [PATCH 10/17] compile CustomerCenterPresentationMode not only for iOS --- Sources/CustomerCenter/CustomerCenterPresentationMode.swift | 4 ---- .../Paywalls/Events/StoredEventSerializerTests.swift | 1 - 2 files changed, 5 deletions(-) diff --git a/Sources/CustomerCenter/CustomerCenterPresentationMode.swift b/Sources/CustomerCenter/CustomerCenterPresentationMode.swift index a38525f9d8..1a2b71147e 100644 --- a/Sources/CustomerCenter/CustomerCenterPresentationMode.swift +++ b/Sources/CustomerCenter/CustomerCenterPresentationMode.swift @@ -13,8 +13,6 @@ import Foundation -#if os(iOS) - /// Warning: This is currently in beta and subject to change. /// /// Presentation options to use with the [presentCustomerCenter](x-source-tag://presentCustomerCenter) View modifiers. @@ -82,5 +80,3 @@ extension CustomerCenterPresentationMode: Codable { .dictionaryWithKeys(\.identifier) } - -#endif diff --git a/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift b/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift index 667c36ad9e..83c1504fac 100644 --- a/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift +++ b/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift @@ -80,7 +80,6 @@ class StoredEventSerializerTests: TestCase { expect(deserializedEvent.userID) == expectedUserID expect(deserializedEvent.feature) == .paywalls - let eventData = deserializedEvent.encodedEvent let jsonData = try XCTUnwrap(storedEvent.encodedEvent.data(using: .utf8)) let decodedPaywallEvent = try JSONDecoder.default.decode(PaywallEvent.self, from: jsonData) expect(decodedPaywallEvent) == paywallEvent From f76b5eab7b11b303212b7e7b358f4b8094b7245d Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 28 Nov 2024 12:50:04 +0100 Subject: [PATCH 11/17] start writing tests --- RevenueCat.xcodeproj/project.pbxproj | 4 ++ .../CustomerCenterPurchasesType.swift | 3 +- .../Data/CustomerCenterPurchases.swift | 4 +- .../ViewModels/CustomerCenterViewModel.swift | 4 +- .../Events/CustomerCenterEvent.swift | 12 ++-- Sources/Purchasing/Purchases/Purchases.swift | 6 +- .../CustomerCenterViewModelTests.swift | 69 ++++++++++++++----- .../MockCustomerCenterPurchases.swift | 63 +++++++++++++++++ 8 files changed, 134 insertions(+), 31 deletions(-) create mode 100644 Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 3ef766b2e7..f8c3a39269 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -277,6 +277,7 @@ 3551E39D2C4A6A1400D27C25 /* TintedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3551E39C2C4A6A1400D27C25 /* TintedProgressView.swift */; }; 35549323269E298B005F9AE9 /* OfferingsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35549322269E298B005F9AE9 /* OfferingsFactory.swift */; }; 356523A82CF7719C00B6E3EA /* CustomerCenterPresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356523A72CF7719B00B6E3EA /* CustomerCenterPresentationMode.swift */; }; + 356523AA2CF885D300B6E3EA /* MockCustomerCenterPurchases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356523A92CF885CE00B6E3EA /* MockCustomerCenterPurchases.swift */; }; 356979E02CCFDAA100EE6A9E /* CustomerInfoFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356979DF2CCFDA9C00EE6A9E /* CustomerInfoFixtures.swift */; }; 356E2DE82CD3CF930055AABB /* StoredEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356E2DE72CD3CF8F0055AABB /* StoredEventTests.swift */; }; 357349012C3BEB5C000EEB86 /* CustomerCenterConfigDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */; }; @@ -1536,6 +1537,7 @@ 3551E39C2C4A6A1400D27C25 /* TintedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TintedProgressView.swift; sourceTree = ""; }; 35549322269E298B005F9AE9 /* OfferingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsFactory.swift; sourceTree = ""; }; 356523A72CF7719B00B6E3EA /* CustomerCenterPresentationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterPresentationMode.swift; sourceTree = ""; }; + 356523A92CF885CE00B6E3EA /* MockCustomerCenterPurchases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCustomerCenterPurchases.swift; sourceTree = ""; }; 356979DF2CCFDA9C00EE6A9E /* CustomerInfoFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerInfoFixtures.swift; sourceTree = ""; }; 356E2DE72CD3CF8F0055AABB /* StoredEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredEventTests.swift; sourceTree = ""; }; 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigDataTests.swift; sourceTree = ""; }; @@ -3645,6 +3647,7 @@ 3544DA6B2C2C848E00704E9D /* CustomerCenter */ = { isa = PBXGroup; children = ( + 356523A92CF885CE00B6E3EA /* MockCustomerCenterPurchases.swift */, 35A99C822CCB95950074AB41 /* SubscriptionInformationFixtures.swift */, 3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */, 3544DA6A2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift */, @@ -6583,6 +6586,7 @@ 3544DA6F2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift in Sources */, 887A63382C1D177800E1A461 /* PurchaseHandlerTests.swift in Sources */, 887A63392C1D177800E1A461 /* OtherPaywallViewTests.swift in Sources */, + 356523AA2CF885D300B6E3EA /* MockCustomerCenterPurchases.swift in Sources */, 3544DA6D2C2C848E00704E9D /* CustomerCenterViewModelTests.swift in Sources */, 887A633A2C1D177800E1A461 /* PaywallViewDynamicTypeTests.swift in Sources */, 35A99C832CCB95950074AB41 /* SubscriptionInformationFixtures.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift index ceb7e92896..e9d76f8790 100644 --- a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift +++ b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift @@ -36,7 +36,6 @@ protocol CustomerCenterPurchasesType: Sendable { promotionalOffer: PromotionalOffer ) async throws -> PurchaseResultData - @Sendable - func track(customerCenterEvent: CustomerCenterEvent) async throws + func track(customerCenterEvent: CustomerCenterEvent) } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift index f4ee40cda8..7e6f47c919 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift @@ -48,8 +48,8 @@ final class CustomerCenterPurchases: CustomerCenterPurchasesType { ) } - func track(customerCenterEvent: CustomerCenterEvent) async throws { - await Purchases.shared.track(customerCenterEvent: customerCenterEvent) + func track(customerCenterEvent: CustomerCenterEvent) { + Purchases.shared.track(customerCenterEvent: customerCenterEvent) } } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 585c32d733..fc272689c4 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -143,9 +143,7 @@ import RevenueCat displayMode: displayMode) let event = CustomerCenterEvent.impression(CustomerCenterEvent.CreationData(), eventData) - Task.detached(priority: .background) { [purchasesProvider = self.purchasesProvider] in - try await purchasesProvider.track(customerCenterEvent: event) - } + purchasesProvider.track(customerCenterEvent: event) } } diff --git a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift index 02f4078eb5..939c5b956e 100644 --- a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift +++ b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift @@ -47,8 +47,8 @@ extension CustomerCenterEvent { public struct CreationData { // swiftlint:disable missing_docs - var id: ID - var date: Date + public var id: ID + public var date: Date public init( id: ID = .init(), @@ -68,10 +68,10 @@ extension CustomerCenterEvent { public struct Data { // swiftlint:disable missing_docs - var localeIdentifier: String - var darkMode: Bool - var isSandbox: Bool - var displayMode: CustomerCenterPresentationMode + public var localeIdentifier: String + public var darkMode: Bool + public var isSandbox: Bool + public var displayMode: CustomerCenterPresentationMode public init( locale: Locale, diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index c086c902f4..6ecf47c60e 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1271,8 +1271,10 @@ public extension Purchases { } /// Used by `RevenueCatUI` to keep track of ``CustomerCenterEvent``s. - func track(customerCenterEvent: CustomerCenterEvent) async { - await self.paywallEventsManager?.track(featureEvent: customerCenterEvent) + func track(customerCenterEvent: CustomerCenterEvent) { + operationDispatcher.dispatchOnWorkerThread { + await self.paywallEventsManager?.track(featureEvent: customerCenterEvent) + } } /// Used by `RevenueCatUI` to download customer center data diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 2f831d4a4b..2b896e015d 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -70,10 +70,13 @@ class CustomerCenterViewModelTests: TestCase { } func testLoadHasSubscriptionsApple() async { - let viewModel = CustomerCenterViewModel(customerCenterActionHandler: nil, - customerInfoFetcher: { - return await CustomerCenterViewModelTests.customerInfoWithAppleSubscriptions - }) + let mockPurchases = MockCustomerCenterPurchases() + mockPurchases.customerInfoResult = .success(CustomerCenterViewModelTests.customerInfoWithAppleSubscriptions) + + let viewModel = CustomerCenterViewModel( + customerCenterActionHandler: nil, + purchasesProvider: mockPurchases + ) await viewModel.loadHasActivePurchases() @@ -83,10 +86,13 @@ class CustomerCenterViewModelTests: TestCase { } func testLoadHasSubscriptionsGoogle() async { - let viewModel = CustomerCenterViewModel(customerCenterActionHandler: nil, - customerInfoFetcher: { - return await CustomerCenterViewModelTests.customerInfoWithGoogleSubscriptions - }) + let mockPurchases = MockCustomerCenterPurchases() + mockPurchases.customerInfoResult = .success(CustomerCenterViewModelTests.customerInfoWithGoogleSubscriptions) + + let viewModel = CustomerCenterViewModel( + customerCenterActionHandler: nil, + purchasesProvider: mockPurchases + ) await viewModel.loadHasActivePurchases() @@ -96,10 +102,13 @@ class CustomerCenterViewModelTests: TestCase { } func testLoadHasSubscriptionsNonActive() async { - let viewModel = CustomerCenterViewModel(customerCenterActionHandler: nil, - customerInfoFetcher: { - return await CustomerCenterViewModelTests.customerInfoWithoutSubscriptions - }) + let mockPurchases = MockCustomerCenterPurchases() + mockPurchases.customerInfoResult = .success(CustomerCenterViewModelTests.customerInfoWithoutSubscriptions) + + let viewModel = CustomerCenterViewModel( + customerCenterActionHandler: nil, + purchasesProvider: mockPurchases + ) await viewModel.loadHasActivePurchases() @@ -109,10 +118,13 @@ class CustomerCenterViewModelTests: TestCase { } func testLoadHasSubscriptionsFailure() async { - let viewModel = CustomerCenterViewModel(customerCenterActionHandler: nil, - customerInfoFetcher: { - throw TestError(message: "An error occurred") - }) + let mockPurchases = MockCustomerCenterPurchases() + mockPurchases.customerInfoResult = .failure(error) + + let viewModel = CustomerCenterViewModel( + customerCenterActionHandler: nil, + purchasesProvider: mockPurchases + ) await viewModel.loadHasActivePurchases() @@ -181,6 +193,31 @@ class CustomerCenterViewModelTests: TestCase { } } + func testTrackImpression() throws { + let mockPurchases = MockCustomerCenterPurchases() + mockPurchases.isSandbox = true + let viewModel = CustomerCenterViewModel( + customerCenterActionHandler: nil, + purchasesProvider: mockPurchases + ) + + let darkMode = true + let displayMode: CustomerCenterPresentationMode = .fullScreen + + viewModel.trackImpression(darkMode: darkMode, displayMode: displayMode) + + expect(mockPurchases.trackedEvents.count) == 1 + let trackedEvent = try XCTUnwrap(mockPurchases.trackedEvents.first) + + expect(trackedEvent.data.darkMode) == darkMode + expect(trackedEvent.data.displayMode) == displayMode + expect(trackedEvent.data.localeIdentifier) == Locale.current.identifier + expect(trackedEvent.data.isSandbox) == true + if case .impression = trackedEvent {} else { + fail("Expected an impression event") + } + } + } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) diff --git a/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift b/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift new file mode 100644 index 0000000000..6f50d05a4c --- /dev/null +++ b/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift @@ -0,0 +1,63 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// MockCustomerCenterPurchases.swift +// +// Created by Cesar de la Vega on 28/11/24. + +import Foundation +import RevenueCat +@testable import RevenueCatUI + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +final class MockCustomerCenterPurchases: @unchecked Sendable, CustomerCenterPurchasesType { + + var isSandbox: Bool = false + + var customerInfoCallCount = 0 + var customerInfoResult: Result = .failure(NSError(domain: "", code: -1)) + func customerInfo() async throws -> CustomerInfo { + customerInfoCallCount += 1 + return try customerInfoResult.get() + } + + var productsCallCount = 0 + var productsResult: [StoreProduct] = [] + func products(_ productIdentifiers: [String]) async -> [StoreProduct] { + productsCallCount += 1 + return productsResult + } + + var promotionalOfferCallCount = 0 + var promotionalOfferResult: Result = .failure(NSError(domain: "", code: -1)) + func promotionalOffer(forProductDiscount discount: StoreProductDiscount, + product: StoreProduct) async throws -> PromotionalOffer { + promotionalOfferCallCount += 1 + return try promotionalOfferResult.get() + } + + var purchaseCallCount = 0 + var purchaseResult: Result = .failure(NSError(domain: "", code: -1)) + func purchase(product: StoreProduct, + promotionalOffer: PromotionalOffer) async throws -> PurchaseResultData { + purchaseCallCount += 1 + return try purchaseResult.get() + } + + var trackCallCount = 0 + var trackError: Error? + var trackedEvents: [CustomerCenterEvent] = [] + func track(customerCenterEvent: CustomerCenterEvent) { + trackCallCount += 1 + trackedEvents.append(customerCenterEvent) + } +} From 68a9f8489212915194552a902c2bd722a8ae4676 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 28 Nov 2024 14:17:09 +0100 Subject: [PATCH 12/17] add CustomerCenterEventsRequestTests --- RevenueCat.xcodeproj/project.pbxproj | 12 ++ .../CustomerCenterViewModelTests.swift | 10 +- .../MockCustomerCenterPurchases.swift | 4 +- .../CustomerCenterEventsRequestTests.swift | 106 ++++++++++++++++++ .../testCanInitFromDeserializedEvent.1.json | 12 ++ .../testCloseEvent.1.json | 12 ++ .../testImpressionEvent.1.json | 12 ++ .../testSurveyCompletedEvent.1.json | 12 ++ 8 files changed, 173 insertions(+), 7 deletions(-) create mode 100644 Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift create mode 100644 Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCanInitFromDeserializedEvent.1.json create mode 100644 Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCloseEvent.1.json create mode 100644 Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testImpressionEvent.1.json create mode 100644 Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testSurveyCompletedEvent.1.json diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index f8c3a39269..69c254627c 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -278,6 +278,7 @@ 35549323269E298B005F9AE9 /* OfferingsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35549322269E298B005F9AE9 /* OfferingsFactory.swift */; }; 356523A82CF7719C00B6E3EA /* CustomerCenterPresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356523A72CF7719B00B6E3EA /* CustomerCenterPresentationMode.swift */; }; 356523AA2CF885D300B6E3EA /* MockCustomerCenterPurchases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356523A92CF885CE00B6E3EA /* MockCustomerCenterPurchases.swift */; }; + 356523AC2CF890F400B6E3EA /* CustomerCenterEventsRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356523AB2CF890EA00B6E3EA /* CustomerCenterEventsRequestTests.swift */; }; 356979E02CCFDAA100EE6A9E /* CustomerInfoFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356979DF2CCFDA9C00EE6A9E /* CustomerInfoFixtures.swift */; }; 356E2DE82CD3CF930055AABB /* StoredEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356E2DE72CD3CF8F0055AABB /* StoredEventTests.swift */; }; 357349012C3BEB5C000EEB86 /* CustomerCenterConfigDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */; }; @@ -1538,6 +1539,7 @@ 35549322269E298B005F9AE9 /* OfferingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsFactory.swift; sourceTree = ""; }; 356523A72CF7719B00B6E3EA /* CustomerCenterPresentationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterPresentationMode.swift; sourceTree = ""; }; 356523A92CF885CE00B6E3EA /* MockCustomerCenterPurchases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCustomerCenterPurchases.swift; sourceTree = ""; }; + 356523AB2CF890EA00B6E3EA /* CustomerCenterEventsRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterEventsRequestTests.swift; sourceTree = ""; }; 356979DF2CCFDA9C00EE6A9E /* CustomerInfoFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerInfoFixtures.swift; sourceTree = ""; }; 356E2DE72CD3CF8F0055AABB /* StoredEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredEventTests.swift; sourceTree = ""; }; 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterConfigDataTests.swift; sourceTree = ""; }; @@ -3669,6 +3671,14 @@ path = SubscriberAttributes; sourceTree = ""; }; + 356523AD2CF8916700B6E3EA /* Events */ = { + isa = PBXGroup; + children = ( + 356523AB2CF890EA00B6E3EA /* CustomerCenterEventsRequestTests.swift */, + ); + path = Events; + sourceTree = ""; + }; 35653BD32C46803A009E8ADB /* Abstractions */ = { isa = PBXGroup; children = ( @@ -3689,6 +3699,7 @@ 357348FE2C3BEAF8000EEB86 /* CustomerCenter */ = { isa = PBXGroup; children = ( + 356523AD2CF8916700B6E3EA /* Events */, 357348FF2C3BEB0A000EEB86 /* CustomerCenterConfigDataTests.swift */, ); path = CustomerCenter; @@ -6126,6 +6137,7 @@ 57544C28285FA94B004E54D5 /* MockAttributeSyncing.swift in Sources */, 4F34AEEC2A5DCCBA00F4BCB0 /* VerificationResultTests.swift in Sources */, FD2046832CB833CD00166727 /* MockStoreKit2PurchaseIntentListenerDelegate.swift in Sources */, + 356523AC2CF890F400B6E3EA /* CustomerCenterEventsRequestTests.swift in Sources */, 1EFA95102CDB7FAA00CA5951 /* WebPurchaseRedemptionHelperTests.swift in Sources */, 35AAEB4C2BBC39D100A12548 /* DiagnosticsFileHandlerTests.swift in Sources */, 57DE80802807529F008D6C6F /* MockStorefront.swift in Sources */, diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 2b896e015d..9543aa54b1 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -72,7 +72,7 @@ class CustomerCenterViewModelTests: TestCase { func testLoadHasSubscriptionsApple() async { let mockPurchases = MockCustomerCenterPurchases() mockPurchases.customerInfoResult = .success(CustomerCenterViewModelTests.customerInfoWithAppleSubscriptions) - + let viewModel = CustomerCenterViewModel( customerCenterActionHandler: nil, purchasesProvider: mockPurchases @@ -88,7 +88,7 @@ class CustomerCenterViewModelTests: TestCase { func testLoadHasSubscriptionsGoogle() async { let mockPurchases = MockCustomerCenterPurchases() mockPurchases.customerInfoResult = .success(CustomerCenterViewModelTests.customerInfoWithGoogleSubscriptions) - + let viewModel = CustomerCenterViewModel( customerCenterActionHandler: nil, purchasesProvider: mockPurchases @@ -104,7 +104,7 @@ class CustomerCenterViewModelTests: TestCase { func testLoadHasSubscriptionsNonActive() async { let mockPurchases = MockCustomerCenterPurchases() mockPurchases.customerInfoResult = .success(CustomerCenterViewModelTests.customerInfoWithoutSubscriptions) - + let viewModel = CustomerCenterViewModel( customerCenterActionHandler: nil, purchasesProvider: mockPurchases @@ -120,7 +120,7 @@ class CustomerCenterViewModelTests: TestCase { func testLoadHasSubscriptionsFailure() async { let mockPurchases = MockCustomerCenterPurchases() mockPurchases.customerInfoResult = .failure(error) - + let viewModel = CustomerCenterViewModel( customerCenterActionHandler: nil, purchasesProvider: mockPurchases @@ -208,7 +208,7 @@ class CustomerCenterViewModelTests: TestCase { expect(mockPurchases.trackedEvents.count) == 1 let trackedEvent = try XCTUnwrap(mockPurchases.trackedEvents.first) - + expect(trackedEvent.data.darkMode) == darkMode expect(trackedEvent.data.displayMode) == displayMode expect(trackedEvent.data.localeIdentifier) == Locale.current.identifier diff --git a/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift b/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift index 6f50d05a4c..74be986619 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift @@ -40,7 +40,7 @@ final class MockCustomerCenterPurchases: @unchecked Sendable, CustomerCenterPurc var promotionalOfferCallCount = 0 var promotionalOfferResult: Result = .failure(NSError(domain: "", code: -1)) func promotionalOffer(forProductDiscount discount: StoreProductDiscount, - product: StoreProduct) async throws -> PromotionalOffer { + product: StoreProduct) async throws -> PromotionalOffer { promotionalOfferCallCount += 1 return try promotionalOfferResult.get() } @@ -48,7 +48,7 @@ final class MockCustomerCenterPurchases: @unchecked Sendable, CustomerCenterPurc var purchaseCallCount = 0 var purchaseResult: Result = .failure(NSError(domain: "", code: -1)) func purchase(product: StoreProduct, - promotionalOffer: PromotionalOffer) async throws -> PurchaseResultData { + promotionalOffer: PromotionalOffer) async throws -> PurchaseResultData { purchaseCallCount += 1 return try purchaseResult.get() } diff --git a/Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift b/Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift new file mode 100644 index 0000000000..e79ad95ca7 --- /dev/null +++ b/Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift @@ -0,0 +1,106 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// CustomerCenterEventsRequestTests.swift +// +// Created by Cesar de la Vega on 28/11/24. + +import Foundation +import Nimble +@testable import RevenueCat +import SnapshotTesting +import XCTest + +@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) +class CustomerCenterEventsRequestTests: TestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + + try AvailabilityChecks.iOS15APIAvailableOrSkipTest() + } + + func testImpressionEvent() throws { + let event = CustomerCenterEvent.impression(Self.eventCreationData, Self.eventData) + let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, + userID: Self.userID, + feature: .customerCenter, + appSessionID: UUID())) + let requestEvent: EventsRequest.CustomerCenterEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) + + assertSnapshot(matching: requestEvent, as: .formattedJson) + } + + func testCloseEvent() throws { + let event = CustomerCenterEvent.close(Self.eventCreationData, Self.eventData) + let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, + userID: Self.userID, + feature: .customerCenter, + appSessionID: UUID())) + let requestEvent: EventsRequest.CustomerCenterEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) + + assertSnapshot(matching: requestEvent, as: .formattedJson) + } + + func testSurveyCompletedEvent() throws { + let event = CustomerCenterEvent.surveyCompleted(Self.eventCreationData, Self.eventData) + let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, + userID: Self.userID, + feature: .customerCenter, + appSessionID: UUID())) + let requestEvent: EventsRequest.CustomerCenterEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) + + assertSnapshot(matching: requestEvent, as: .formattedJson) + } + + func testCanInitFromDeserializedEvent() throws { + let expectedUserID = "test-user" + let customerCenterEventCreationData: CustomerCenterEvent.CreationData = .init( + id: .init(uuidString: "72164C05-2BDC-4807-8918-A4105F727DEB")!, + date: .init(timeIntervalSince1970: 1694029328) + ) + let customerCenterEventData: CustomerCenterEvent.Data = .init( + locale: .init(identifier: "en_US"), + darkMode: true, + isSandbox: true, + displayMode: .fullScreen + ) + let customerCenterEvent = CustomerCenterEvent.impression(customerCenterEventCreationData, + customerCenterEventData) + + let storedEvent = try XCTUnwrap(StoredEvent(event: customerCenterEvent, + userID: expectedUserID, + feature: .customerCenter, + appSessionID: UUID())) + let serializedEvent = try StoredEventSerializer.encode(storedEvent) + let deserializedEvent = try StoredEventSerializer.decode(serializedEvent) + expect(deserializedEvent.userID) == expectedUserID + expect(deserializedEvent.feature) == .customerCenter + + let requestEvent = try XCTUnwrap(EventsRequest.CustomerCenterEvent(storedEvent: deserializedEvent)) + + assertSnapshot(matching: requestEvent, as: .formattedJson) + } + + // MARK: - + + private static let eventCreationData: CustomerCenterEvent.CreationData = .init( + id: .init(uuidString: "72164C05-2BDC-4807-8918-A4105F727DEB")!, + date: .init(timeIntervalSince1970: 1694029328) + ) + + private static let eventData: CustomerCenterEvent.Data = .init( + locale: .init(identifier: "es_ES"), + darkMode: true, + isSandbox: true, + displayMode: .fullScreen + ) + + private static let userID = "Jack Shepard" +} diff --git a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCanInitFromDeserializedEvent.1.json b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCanInitFromDeserializedEvent.1.json new file mode 100644 index 0000000000..c8fbb21038 --- /dev/null +++ b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCanInitFromDeserializedEvent.1.json @@ -0,0 +1,12 @@ +{ + "app_session_id" : "7761E1EC-CE42-4094-9086-30806BD835AA", + "app_user_id" : "test-user", + "dark_mode" : true, + "display_mode" : "full_screen", + "id" : "72164C05-2BDC-4807-8918-A4105F727DEB", + "is_sandbox" : true, + "locale" : "en_US", + "timestamp" : 1694029328000, + "type" : "customer_center_impression", + "version" : 1 +} \ No newline at end of file diff --git a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCloseEvent.1.json b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCloseEvent.1.json new file mode 100644 index 0000000000..0a04b01bbf --- /dev/null +++ b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCloseEvent.1.json @@ -0,0 +1,12 @@ +{ + "app_session_id" : "F1B2FC0A-54B0-4BDD-A7EE-C46A2530B7F1", + "app_user_id" : "Jack Shepard", + "dark_mode" : true, + "display_mode" : "full_screen", + "id" : "72164C05-2BDC-4807-8918-A4105F727DEB", + "is_sandbox" : true, + "locale" : "es_ES", + "timestamp" : 1694029328000, + "type" : "customer_center_close", + "version" : 1 +} \ No newline at end of file diff --git a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testImpressionEvent.1.json b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testImpressionEvent.1.json new file mode 100644 index 0000000000..347a517be7 --- /dev/null +++ b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testImpressionEvent.1.json @@ -0,0 +1,12 @@ +{ + "app_session_id" : "50BEA5F1-105B-402B-ABF8-9AD1BBF4AEDF", + "app_user_id" : "Jack Shepard", + "dark_mode" : true, + "display_mode" : "full_screen", + "id" : "72164C05-2BDC-4807-8918-A4105F727DEB", + "is_sandbox" : true, + "locale" : "es_ES", + "timestamp" : 1694029328000, + "type" : "customer_center_impression", + "version" : 1 +} \ No newline at end of file diff --git a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testSurveyCompletedEvent.1.json b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testSurveyCompletedEvent.1.json new file mode 100644 index 0000000000..2a7367e652 --- /dev/null +++ b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testSurveyCompletedEvent.1.json @@ -0,0 +1,12 @@ +{ + "app_session_id" : "4AB019E5-B0F9-48C7-810E-44D950920E72", + "app_user_id" : "Jack Shepard", + "dark_mode" : true, + "display_mode" : "full_screen", + "id" : "72164C05-2BDC-4807-8918-A4105F727DEB", + "is_sandbox" : true, + "locale" : "es_ES", + "timestamp" : 1694029328000, + "type" : "customer_center_survey_completed", + "version" : 1 +} \ No newline at end of file From e81cfb0bbf9c44e58b181d1bdaa04d082f39e2e4 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 28 Nov 2024 14:36:04 +0100 Subject: [PATCH 13/17] add testResetAppSessionID --- .../Events/PaywallEventsManagerTests.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift b/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift index e8eb829983..41ba067e4f 100644 --- a/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift +++ b/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift @@ -273,6 +273,22 @@ class PaywallEventsManagerTests: TestCase { } #endif + func testResetAppSessionID() async throws { + _ = await self.storeRandomEvent() + var storedEvents = await self.store.storedEvents + let storedEvent1 = try XCTUnwrap(storedEvents.first) + let initialSessionID = storedEvent1.appSessionID + + await self.manager.resetAppSessionID() + + _ = await self.storeRandomEvent() + storedEvents = await self.store.storedEvents + let storedEvent2 = try XCTUnwrap(storedEvents.last) + let newSessionID = storedEvent2.appSessionID + + expect(initialSessionID) != newSessionID + } + // MARK: - private static let userID = "nacho" From 1607795a7cf3926ea99f903735a9e8fcd94b91cc Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 28 Nov 2024 15:31:29 +0100 Subject: [PATCH 14/17] adds purchases tests --- Sources/Purchasing/Purchases/Purchases.swift | 4 +- .../Events/PurchasesPaywallEventsTests.swift | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 6ecf47c60e..e2b0ae459e 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -835,7 +835,7 @@ public extension Purchases { self.updateOfferingsCache(isAppBackgrounded: isAppBackgrounded) } - Task { + self.operationDispatcher.dispatchOnWorkerThread { await self.paywallEventsManager?.resetAppSessionID() } } @@ -871,7 +871,7 @@ public extension Purchases { return } - Task { + self.operationDispatcher.dispatchOnWorkerThread { await self.paywallEventsManager?.resetAppSessionID() } diff --git a/Tests/UnitTests/Paywalls/Events/PurchasesPaywallEventsTests.swift b/Tests/UnitTests/Paywalls/Events/PurchasesPaywallEventsTests.swift index 289e7a8243..5b2ce4a780 100644 --- a/Tests/UnitTests/Paywalls/Events/PurchasesPaywallEventsTests.swift +++ b/Tests/UnitTests/Paywalls/Events/PurchasesPaywallEventsTests.swift @@ -48,4 +48,62 @@ class PurchasesPaywallEventsTests: BasePurchasesTests { expect(self.mockOperationDispatcher.invokedDispatchAsyncOnWorkerThreadDelayParam) == JitterableDelay.none } + func testLogInWithSuccessResetsAppSessionID() async throws { + self.identityManager.mockLogInResult = .success((Self.mockLoggedInInfo, true)) + + _ = try await self.purchases.logIn("Static string") + + let manager = try self.mockPaywallEventsManager + try await asyncWait { await manager.invokedResetAppSessionID == true } + } + + func testLogOutWithSuccessResetsAppSessionID() async throws { + self.identityManager.mockLogOutError = nil + + _ = try await self.purchases.logOut() + + let manager = try self.mockPaywallEventsManager + try await asyncWait { await manager.invokedResetAppSessionID == true } + } + +} + +@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) +private extension PurchasesPaywallEventsTests { + + typealias LogInResult = Result<(customerInfo: CustomerInfo, created: Bool), PublicError> + typealias LogOutResult = Result + + // swiftlint:disable force_try + static let mockLoggedInInfo = try! CustomerInfo(data: PurchasesPaywallEventsTests.loggedInCustomerInfoData) + static let mockLoggedOutInfo = try! CustomerInfo(data: PurchasesPaywallEventsTests.loggedOutCustomerInfoData) + // swiftlint:enable force_try + + private static let loggedInCustomerInfoData: [String: Any] = [ + "request_date": "2019-08-16T10:30:42Z", + "subscriber": [ + "first_seen": "2019-07-17T00:05:54Z", + "original_app_user_id": "user", + "subscriptions": [:] as [String: Any], + "other_purchases": [:] as [String: Any], + "original_application_version": NSNull() + ] as [String: Any] + ] + + private static let loggedOutCustomerInfoData: [String: Any] = [ + "request_date": "2019-08-16T10:30:42Z", + "subscriber": [ + "first_seen": "2019-07-17T00:05:54Z", + "original_app_user_id": "$RCAnonymousID:5b6fdbad3a0c4f879e43d269ecdf9ba1", + "subscriptions": [:] as [String: Any], + "other_purchases": [:] as [String: Any], + "original_application_version": NSNull() + ] as [String: Any] + ] + + /// Converts the result of `Purchases.logIn` into `LogInResult` + static func logInResult(_ info: CustomerInfo?, _ created: Bool, _ error: PublicError?) -> LogInResult { + return .init(info.map { ($0, created) }, error) + } + } From bd5ecd8075abceb1c540d27285cf0e468de48605 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 28 Nov 2024 15:44:08 +0100 Subject: [PATCH 15/17] remove other events and fix snapshots --- .../Events/CustomerCenterEvent.swift | 12 +------- .../Events/EventsRequest+CustomerCenter.swift | 2 -- .../CustomerCenterEventsRequestTests.swift | 29 ++++--------------- .../testCanInitFromDeserializedEvent.1.json | 2 +- .../testCloseEvent.1.json | 12 -------- .../testImpressionEvent.1.json | 2 +- .../testSurveyCompletedEvent.1.json | 12 -------- .../Events/PaywallEventsRequestTests.swift | 10 ++++--- 8 files changed, 14 insertions(+), 67 deletions(-) delete mode 100644 Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCloseEvent.1.json delete mode 100644 Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testSurveyCompletedEvent.1.json diff --git a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift index 939c5b956e..27f9c318ac 100644 --- a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift +++ b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift @@ -30,15 +30,9 @@ public enum CustomerCenterEvent: FeatureEvent { return .customerCenter } - /// A ``CustomerCenterView`` was displayed. + /// The Customer Center was displayed. case impression(CreationData, Data) - /// A feedback survey was completed with a particular option. - case surveyCompleted(CreationData, Data) - - /// A ``CustomerCenterView`` was closed. - case close(CreationData, Data) - } extension CustomerCenterEvent { @@ -96,8 +90,6 @@ extension CustomerCenterEvent { public var creationData: CreationData { switch self { case let .impression(creationData, _): return creationData - case let .surveyCompleted(creationData, _): return creationData - case let .close(creationData, _): return creationData } } @@ -105,8 +97,6 @@ extension CustomerCenterEvent { public var data: Data { switch self { case let .impression(_, data): return data - case let .surveyCompleted(_, data): return data - case let .close(_, data): return data } } diff --git a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift index ec9b7b5a3b..6d2039feac 100644 --- a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift +++ b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift @@ -86,8 +86,6 @@ private extension CustomerCenterEvent { var eventType: EventsRequest.CustomerCenterEvent.EventType { switch self { case .impression: return .impression - case .close: return .close - case .surveyCompleted: return .surveyCompleted } } diff --git a/Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift b/Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift index e79ad95ca7..27a6fc379e 100644 --- a/Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift +++ b/Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift @@ -31,29 +31,7 @@ class CustomerCenterEventsRequestTests: TestCase { let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, userID: Self.userID, feature: .customerCenter, - appSessionID: UUID())) - let requestEvent: EventsRequest.CustomerCenterEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) - - assertSnapshot(matching: requestEvent, as: .formattedJson) - } - - func testCloseEvent() throws { - let event = CustomerCenterEvent.close(Self.eventCreationData, Self.eventData) - let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, - userID: Self.userID, - feature: .customerCenter, - appSessionID: UUID())) - let requestEvent: EventsRequest.CustomerCenterEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) - - assertSnapshot(matching: requestEvent, as: .formattedJson) - } - - func testSurveyCompletedEvent() throws { - let event = CustomerCenterEvent.surveyCompleted(Self.eventCreationData, Self.eventData) - let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, - userID: Self.userID, - feature: .customerCenter, - appSessionID: UUID())) + appSessionID: Self.appSessionID)) let requestEvent: EventsRequest.CustomerCenterEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) @@ -77,7 +55,7 @@ class CustomerCenterEventsRequestTests: TestCase { let storedEvent = try XCTUnwrap(StoredEvent(event: customerCenterEvent, userID: expectedUserID, feature: .customerCenter, - appSessionID: UUID())) + appSessionID: Self.appSessionID)) let serializedEvent = try StoredEventSerializer.encode(storedEvent) let deserializedEvent = try StoredEventSerializer.decode(serializedEvent) expect(deserializedEvent.userID) == expectedUserID @@ -103,4 +81,7 @@ class CustomerCenterEventsRequestTests: TestCase { ) private static let userID = "Jack Shepard" + + private static let appSessionID = UUID(uuidString: "83164C05-2BDC-4807-8918-A4105F727DEB") + } diff --git a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCanInitFromDeserializedEvent.1.json b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCanInitFromDeserializedEvent.1.json index c8fbb21038..8e2423be7a 100644 --- a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCanInitFromDeserializedEvent.1.json +++ b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCanInitFromDeserializedEvent.1.json @@ -1,5 +1,5 @@ { - "app_session_id" : "7761E1EC-CE42-4094-9086-30806BD835AA", + "app_session_id" : "83164C05-2BDC-4807-8918-A4105F727DEB", "app_user_id" : "test-user", "dark_mode" : true, "display_mode" : "full_screen", diff --git a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCloseEvent.1.json b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCloseEvent.1.json deleted file mode 100644 index 0a04b01bbf..0000000000 --- a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testCloseEvent.1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "app_session_id" : "F1B2FC0A-54B0-4BDD-A7EE-C46A2530B7F1", - "app_user_id" : "Jack Shepard", - "dark_mode" : true, - "display_mode" : "full_screen", - "id" : "72164C05-2BDC-4807-8918-A4105F727DEB", - "is_sandbox" : true, - "locale" : "es_ES", - "timestamp" : 1694029328000, - "type" : "customer_center_close", - "version" : 1 -} \ No newline at end of file diff --git a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testImpressionEvent.1.json b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testImpressionEvent.1.json index 347a517be7..5f8deca8a6 100644 --- a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testImpressionEvent.1.json +++ b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testImpressionEvent.1.json @@ -1,5 +1,5 @@ { - "app_session_id" : "50BEA5F1-105B-402B-ABF8-9AD1BBF4AEDF", + "app_session_id" : "83164C05-2BDC-4807-8918-A4105F727DEB", "app_user_id" : "Jack Shepard", "dark_mode" : true, "display_mode" : "full_screen", diff --git a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testSurveyCompletedEvent.1.json b/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testSurveyCompletedEvent.1.json deleted file mode 100644 index 2a7367e652..0000000000 --- a/Tests/UnitTests/CustomerCenter/Events/__Snapshots__/CustomerCenterEventsRequestTests/testSurveyCompletedEvent.1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "app_session_id" : "4AB019E5-B0F9-48C7-810E-44D950920E72", - "app_user_id" : "Jack Shepard", - "dark_mode" : true, - "display_mode" : "full_screen", - "id" : "72164C05-2BDC-4807-8918-A4105F727DEB", - "is_sandbox" : true, - "locale" : "es_ES", - "timestamp" : 1694029328000, - "type" : "customer_center_survey_completed", - "version" : 1 -} \ No newline at end of file diff --git a/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift b/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift index f1daa498ac..4975a4290e 100644 --- a/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift +++ b/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift @@ -31,7 +31,7 @@ class PaywallEventsRequestTests: TestCase { let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, userID: Self.userID, feature: .paywalls, - appSessionID: UUID())) + appSessionID: Self.appSessionID)) let requestEvent: EventsRequest.PaywallEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) @@ -42,7 +42,7 @@ class PaywallEventsRequestTests: TestCase { let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, userID: Self.userID, feature: .paywalls, - appSessionID: UUID())) + appSessionID: Self.appSessionID)) let requestEvent: EventsRequest.PaywallEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) @@ -54,7 +54,7 @@ class PaywallEventsRequestTests: TestCase { let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, userID: Self.userID, feature: .paywalls, - appSessionID: UUID())) + appSessionID: Self.appSessionID)) let requestEvent: EventsRequest.PaywallEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) @@ -79,7 +79,7 @@ class PaywallEventsRequestTests: TestCase { let storedEvent = try XCTUnwrap(StoredEvent(event: paywallEvent, userID: expectedUserID, feature: .paywalls, - appSessionID: UUID())) + appSessionID: Self.appSessionID)) let serializedEvent = try StoredEventSerializer.encode(storedEvent) let deserializedEvent = try StoredEventSerializer.decode(serializedEvent) expect(deserializedEvent.userID) == expectedUserID @@ -108,4 +108,6 @@ class PaywallEventsRequestTests: TestCase { private static let userID = "Jack Shepard" + private static let appSessionID = UUID(uuidString: "83164C05-2BDC-4807-8918-A4105F727DEB") + } From a2e3ca9b57736d82f6014f557a080b01c62b8aab Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 28 Nov 2024 16:01:44 +0100 Subject: [PATCH 16/17] remove customer_center_survey_completed --- Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift index 6d2039feac..a242451822 100644 --- a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift +++ b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift @@ -38,7 +38,6 @@ extension EventsRequest.CustomerCenterEvent { case impression = "customer_center_impression" case close = "customer_center_close" - case surveyCompleted = "customer_center_survey_completed" } From ee7f80eaff9bc91e0052877d0e05d05ae4fa3d2b Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 2 Dec 2024 15:10:54 +0100 Subject: [PATCH 17/17] PR comments --- RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift | 2 +- .../CustomerCenter/Events/EventsRequest+CustomerCenter.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 9594676d56..2f7c38f942 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -70,7 +70,7 @@ public struct CustomerCenterView: View { await loadInformationIfNeeded() } .task { - trackImpression() + self.trackImpression() } .environmentObject(self.viewModel) } diff --git a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift index a242451822..4d71c19fe8 100644 --- a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift +++ b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift @@ -102,9 +102,9 @@ extension EventsRequest.CustomerCenterEvent: Encodable { case version case type case appUserID = "appUserId" - case appSessionID = "appSessionID" + case appSessionID = "appSessionId" case timestamp - case darkMode + case darkMode = "darkMode" case locale case isSandbox = "isSandbox" case displayMode = "displayMode"