From eb334dec67c168c5daa9ebd40fb44839eee963bd Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 10 Dec 2024 14:31:57 +0100 Subject: [PATCH] Add feedback survey option chosen event (#4528) --- .../CustomerCenterPurchasesType.swift | 2 +- .../Data/CustomerCenterEnvironment.swift | 11 + .../Data/CustomerCenterPurchases.swift | 2 +- .../Data/FeedbackSurveyData.swift | 3 + .../ViewModels/CustomerCenterViewModel.swift | 2 +- .../ViewModels/FeedbackSurveyViewModel.swift | 31 +++ .../ManageSubscriptionsViewModel.swift | 3 +- .../Views/CustomerCenterView.swift | 1 + .../Views/FeedbackSurveyView.swift | 4 + .../Views/ManageSubscriptionsView.swift | 5 +- .../CustomerCenterConfigData.swift | 2 + .../Events/CustomerCenterEvent.swift | 191 +++++++++++++--- .../Events/EventsRequest+CustomerCenter.swift | 206 +++++++++++++++--- Sources/Events/FeatureEvent.swift | 1 + Sources/Events/Networking/EventsRequest.swift | 14 +- Sources/Events/StoredEvent.swift | 13 +- Sources/Networking/InternalAPI.swift | 3 +- Sources/Paywalls/Events/PaywallEvent.swift | 5 + .../Events/PaywallEventsManager.swift | 8 +- Sources/Purchasing/Purchases/Purchases.swift | 6 +- .../CustomerCenterViewModelTests.swift | 2 +- .../MockCustomerCenterPurchases.swift | 4 +- .../CustomerCenterEventsRequestTests.swift | 16 +- .../Events/PaywallEventStoreTests.swift | 3 +- .../Events/PaywallEventsBackendTests.swift | 23 +- .../Events/PaywallEventsManagerTests.swift | 67 ++---- .../Events/PaywallEventsRequestTests.swift | 42 ++-- .../Events/StoredEventSerializerTests.swift | 36 ++- 28 files changed, 519 insertions(+), 187 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift index e9d76f8790..5a8839389d 100644 --- a/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift +++ b/RevenueCatUI/CustomerCenter/Abstractions/CustomerCenterPurchasesType.swift @@ -36,6 +36,6 @@ protocol CustomerCenterPurchasesType: Sendable { promotionalOffer: PromotionalOffer ) async throws -> PurchaseResultData - func track(customerCenterEvent: CustomerCenterEvent) + func track(customerCenterEvent: any CustomerCenterEventType) } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift index ac4b325dfb..60c229c199 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterEnvironment.swift @@ -33,6 +33,12 @@ struct SupportKey: EnvironmentKey { } +struct CustomerCenterPresentationModeKey: EnvironmentKey { + + static let defaultValue: CustomerCenterPresentationMode = .default + +} + extension CustomerCenterConfigData.Localization { /// Default ``CustomerCenterConfigData.Localization`` value for Environment usage @@ -70,4 +76,9 @@ extension EnvironmentValues { set { self[SupportKey.self] = newValue } } + var customerCenterPresentationMode: CustomerCenterPresentationMode { + get { self[CustomerCenterPresentationModeKey.self] } + set { self[CustomerCenterPresentationModeKey.self] = newValue } + } + } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift index 7e6f47c919..deb9a67d87 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterPurchases.swift @@ -48,7 +48,7 @@ final class CustomerCenterPurchases: CustomerCenterPurchasesType { ) } - func track(customerCenterEvent: CustomerCenterEvent) { + func track(customerCenterEvent: any CustomerCenterEventType) { Purchases.shared.track(customerCenterEvent: customerCenterEvent) } diff --git a/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift b/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift index c7e85c0fe3..22d4c53ee9 100644 --- a/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift +++ b/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift @@ -25,11 +25,14 @@ import RevenueCat class FeedbackSurveyData: ObservableObject { var configuration: CustomerCenterConfigData.HelpPath.FeedbackSurvey + var path: CustomerCenterConfigData.HelpPath var onOptionSelected: (() -> Void) init(configuration: CustomerCenterConfigData.HelpPath.FeedbackSurvey, + path: CustomerCenterConfigData.HelpPath, onOptionSelected: @escaping (() -> Void)) { self.configuration = configuration + self.path = path self.onOptionSelected = onOptionSelected } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index fc272689c4..461568874f 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -141,7 +141,7 @@ import RevenueCat darkMode: darkMode, isSandbox: isSandbox, displayMode: displayMode) - let event = CustomerCenterEvent.impression(CustomerCenterEvent.CreationData(), eventData) + let event = CustomerCenterEvent.impression(CustomerCenterEventCreationData(), eventData) purchasesProvider.track(customerCenterEvent: event) } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift index cf2765064a..a49feeb1f8 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/FeedbackSurveyViewModel.swift @@ -57,9 +57,12 @@ class FeedbackSurveyViewModel: ObservableObject { func handleAction( for option: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option, + darkMode: Bool, + displayMode: CustomerCenterPresentationMode, dismissView: () -> Void ) async { if let customerCenterActionHandler = self.customerCenterActionHandler { + trackSurveyAnswerSubmitted(option: option, darkMode: darkMode, displayMode: displayMode) customerCenterActionHandler(.feedbackSurveyCompleted(option.id)) } @@ -105,4 +108,32 @@ extension FeedbackSurveyViewModel { } } +// MARK: - Events +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +private extension FeedbackSurveyViewModel { + + func trackSurveyAnswerSubmitted(option: CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option, + darkMode: Bool, + displayMode: CustomerCenterPresentationMode) { + let isSandbox = purchasesProvider.isSandbox + let surveyOptionData = CustomerCenterAnswerSubmittedEvent.Data(locale: .current, + darkMode: darkMode, + isSandbox: isSandbox, + displayMode: displayMode, + path: feedbackSurveyData.path.type, + url: feedbackSurveyData.path.url, + surveyOptionID: option.id, + surveyOptionTitleKey: option.title, + additionalContext: nil, + revisionID: 0) + let event = CustomerCenterAnswerSubmittedEvent.answerSubmitted(CustomerCenterEventCreationData(), + surveyOptionData) + purchasesProvider.track(customerCenterEvent: event) + } + +} + #endif diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 63a84d53ac..43b60f7535 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -116,7 +116,8 @@ class ManageSubscriptionsViewModel: ObservableObject { func determineFlow(for path: CustomerCenterConfigData.HelpPath) async { switch path.detail { case let .feedbackSurvey(feedbackSurvey): - self.feedbackSurveyData = FeedbackSurveyData(configuration: feedbackSurvey) { [weak self] in + self.feedbackSurveyData = FeedbackSurveyData(configuration: feedbackSurvey, + path: path) { [weak self] in Task { await self?.onPathSelected(path: path) } diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index c771502b41..f1c6f7c9a2 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -71,6 +71,7 @@ public struct CustomerCenterView: View { .environment(\.localization, configuration.localization) .environment(\.appearance, configuration.appearance) .environment(\.supportInformation, configuration.support) + .environment(\.customerCenterPresentationMode, self.mode) } } } diff --git a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift index 0043cc9e3e..a649d333d6 100644 --- a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift +++ b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift @@ -33,6 +33,8 @@ struct FeedbackSurveyView: View { private var appearance: CustomerCenterConfigData.Appearance @Environment(\.colorScheme) private var colorScheme + @Environment(\.customerCenterPresentationMode) + private var mode: CustomerCenterPresentationMode @Binding private var isPresented: Bool @@ -57,6 +59,8 @@ struct FeedbackSurveyView: View { onOptionSelected: { option in await self.viewModel.handleAction( for: option, + darkMode: self.colorScheme == .dark, + displayMode: self.mode, dismissView: self.dismissView ) }, diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 22609ec0d8..bf8ffe6ebe 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -82,9 +82,8 @@ struct ManageSubscriptionsView: View { if let purchaseInformation = self.viewModel.purchaseInformation { Section { - SubscriptionDetailsView( - purchaseInformation: purchaseInformation, - refundRequestStatus: self.viewModel.refundRequestStatus) + SubscriptionDetailsView(purchaseInformation: purchaseInformation, + refundRequestStatus: self.viewModel.refundRequestStatus) } Section { ManageSubscriptionsButtonsView(viewModel: self.viewModel, diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index f9508aaabe..70193af705 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -553,3 +553,5 @@ extension CustomerCenterConfigData.Support { } } + +extension CustomerCenterConfigData.HelpPath.PathType: Sendable, Codable {} diff --git a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift index 27f9c318ac..5bdf0562c2 100644 --- a/Sources/CustomerCenter/Events/CustomerCenterEvent.swift +++ b/Sources/CustomerCenter/Events/CustomerCenterEvent.swift @@ -13,70 +13,134 @@ import Foundation -/// An event to be sent by the `RevenueCatUI` SDK. -public enum CustomerCenterEvent: FeatureEvent { +/// A protocol that represents a customer center event. +public protocol CustomerCenterEventType {} + +extension CustomerCenterEventType { - // swiftlint:disable type_name + var feature: Feature { .customerCenter } + +} - /// An identifier that represents a customer center event. - public typealias ID = UUID +enum CustomerCenterEventDiscriminator: String { + + case lifecycle = "lifecycle" + case answerSubmitted = "answer_submitted" + +} - // swiftlint:enable type_name +/// Data that represents a customer center event creation. +public struct CustomerCenterEventCreationData { - /// An identifier that represents a paywall session. - public typealias SessionID = UUID + let id: UUID + let date: Date - var feature: Feature { - return .customerCenter + // swiftlint:disable:next missing_docs + public init( + id: UUID = .init(), + date: Date = .init() + ) { + self.id = id + self.date = date } +} + +/// An event to be sent by the `RevenueCatUI` SDK. +public enum CustomerCenterEvent: FeatureEvent, CustomerCenterEventType { + + var eventDiscriminator: String? { CustomerCenterEventDiscriminator.lifecycle.rawValue } + /// The Customer Center was displayed. - case impression(CreationData, Data) + case impression(CustomerCenterEventCreationData, Data) + +} + +/// An event to be sent by the `RevenueCatUI` SDK. +public enum CustomerCenterAnswerSubmittedEvent: FeatureEvent, CustomerCenterEventType { + + var eventDiscriminator: String? { CustomerCenterEventDiscriminator.lifecycle.rawValue } + + /// A feedback survey was completed with a particular option. + case answerSubmitted(CustomerCenterEventCreationData, Data) } extension CustomerCenterEvent { - /// The creation data of a ``CustomerCenterEvent``. - public struct CreationData { + /// The content of a ``CustomerCenterEvent``. + public struct Data { // swiftlint:disable missing_docs - public var id: ID - public var date: Date + public var localeIdentifier: String { base.localeIdentifier } + public var darkMode: Bool { base.darkMode } + public var isSandbox: Bool { base.isSandbox } + public var displayMode: CustomerCenterPresentationMode { base.displayMode } + + private let base: CustomerCenterBaseData public init( - id: ID = .init(), - date: Date = .init() + locale: Locale, + darkMode: Bool, + isSandbox: Bool, + displayMode: CustomerCenterPresentationMode ) { - self.id = id - self.date = date + self.base = CustomerCenterBaseData( + locale: locale, + darkMode: darkMode, + isSandbox: isSandbox, + displayMode: displayMode + ) } + // swiftlint:enable missing_docs } } -extension CustomerCenterEvent { +extension CustomerCenterAnswerSubmittedEvent { - /// The content of a ``CustomerCenterEvent``. + /// The content of a ``CustomerCenterAnswerSubmittedEvent``. public struct Data { // swiftlint:disable missing_docs - public var localeIdentifier: String - public var darkMode: Bool - public var isSandbox: Bool - public var displayMode: CustomerCenterPresentationMode + public var localeIdentifier: String { base.localeIdentifier } + public var darkMode: Bool { base.darkMode } + public var isSandbox: Bool { base.isSandbox } + public var displayMode: CustomerCenterPresentationMode { base.displayMode } + public let path: CustomerCenterConfigData.HelpPath.PathType + public let url: URL? + public let surveyOptionID: String + public let surveyOptionTitleKey: String + public let additionalContext: String? + public let revisionID: Int + + private let base: CustomerCenterBaseData public init( locale: Locale, darkMode: Bool, isSandbox: Bool, - displayMode: CustomerCenterPresentationMode + displayMode: CustomerCenterPresentationMode, + path: CustomerCenterConfigData.HelpPath.PathType, + url: URL?, + surveyOptionID: String, + surveyOptionTitleKey: String, + additionalContext: String? = nil, + revisionID: Int ) { - self.localeIdentifier = locale.identifier - self.darkMode = darkMode - self.isSandbox = isSandbox - self.displayMode = displayMode + self.base = CustomerCenterBaseData( + locale: locale, + darkMode: darkMode, + isSandbox: isSandbox, + displayMode: displayMode + ) + self.path = path + self.url = url + self.surveyOptionID = surveyOptionID + self.surveyOptionTitleKey = surveyOptionTitleKey + self.additionalContext = additionalContext + self.revisionID = revisionID } // swiftlint:enable missing_docs @@ -86,8 +150,8 @@ extension CustomerCenterEvent { extension CustomerCenterEvent { - /// - Returns: the underlying ``CustomerCenterEvent/CreationData-swift.struct`` for this event. - public var creationData: CreationData { + /// - Returns: the underlying ``CustomerCenterEventCreationData-swift.struct`` for this event. + public var creationData: CustomerCenterEventCreationData { switch self { case let .impression(creationData, _): return creationData } @@ -102,8 +166,69 @@ extension CustomerCenterEvent { } +extension CustomerCenterAnswerSubmittedEvent { + + /// - Returns: the underlying ``CustomerCenterEventCreationData-swift.struct`` for this event. + public var creationData: CustomerCenterEventCreationData { + switch self { + case let .answerSubmitted(creationData, _): return creationData + } + } + + /// - Returns: the underlying ``CustomerCenterAnswerSubmittedEvent/Data-swift.struct`` for this event. + public var data: Data { + switch self { + case let .answerSubmitted(_, surveyData): return surveyData + } + } + +} + +private struct CustomerCenterBaseData { + + // swiftlint:disable missing_docs + public let localeIdentifier: String + public let darkMode: Bool + public let isSandbox: Bool + public let displayMode: CustomerCenterPresentationMode + + public init( + locale: Locale, + darkMode: Bool, + isSandbox: Bool, + displayMode: CustomerCenterPresentationMode + ) { + self.localeIdentifier = locale.identifier + self.darkMode = darkMode + self.isSandbox = isSandbox + self.displayMode = displayMode + } + // swiftlint:enable missing_docs + +} + // MARK: - -extension CustomerCenterEvent.CreationData: Equatable, Codable, Sendable {} +extension CustomerCenterEventCreationData: Equatable, Codable, Sendable {} extension CustomerCenterEvent.Data: Equatable, Codable, Sendable {} extension CustomerCenterEvent: Equatable, Codable, Sendable {} + +extension CustomerCenterBaseData: Equatable, Codable, Sendable {} + +extension CustomerCenterAnswerSubmittedEvent.Data: Equatable, Codable, Sendable { + + private enum CodingKeys: String, CodingKey { + + case base + case path + case url + case surveyOptionID = "surveyOptionId" + case surveyOptionTitleKey = "surveyOptionTitleKey" + case additionalContext = "additionalContext" + case revisionID = "revisionId" + + } + +} + +extension CustomerCenterAnswerSubmittedEvent: Equatable, Codable, Sendable {} diff --git a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift index cf4216bfad..72699214b4 100644 --- a/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift +++ b/Sources/CustomerCenter/Events/EventsRequest+CustomerCenter.swift @@ -15,11 +15,24 @@ import Foundation extension EventsRequest { - struct CustomerCenterEvent { + struct TypeContainer: Decodable { + + let type: String + + } + + enum CustomerCenterEventType: String { + + case impression = "customer_center_impression" + case answerSubmitted = "customer_center_survey_option_chosen" + + } + + class CustomerCenterEventBaseRequest { let id: String? let version: Int - var type: EventType + var type: CustomerCenterEventType var appUserID: String var appSessionID: String var timestamp: UInt64 @@ -27,42 +40,144 @@ extension EventsRequest { var locale: String var isSandbox: Bool var displayMode: CustomerCenterPresentationMode - var revisionId: Int + // We don't support revisions in the backend yet so hardcoding to 1 for now + let revisionId: Int = 1 - } + init(id: String?, + version: Int, + type: CustomerCenterEventType, + appUserID: String, + appSessionID: String, + timestamp: UInt64, + darkMode: Bool, + locale: String, + isSandbox: Bool, + displayMode: CustomerCenterPresentationMode) { + self.id = id + self.version = version + self.type = type + self.appUserID = appUserID + self.appSessionID = appSessionID + self.timestamp = timestamp + self.darkMode = darkMode + self.locale = locale + self.isSandbox = isSandbox + self.displayMode = displayMode + } -} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + static func createBase(from storedEvent: StoredEvent) -> CustomerCenterEventBaseRequest? { + guard let appSessionID = storedEvent.appSessionID else { + Logger.error(Strings.paywalls.event_missing_app_session_id) + return nil + } -extension EventsRequest.CustomerCenterEvent { + guard let jsonData = storedEvent.encodedEvent.data(using: .utf8) else { + Logger.error(Strings.paywalls.event_cannot_get_encoded_event) + return nil + } + guard let customerCenterEvent = try? JSONDecoder.default.decode(CustomerCenterEvent.self, + from: jsonData) else { + Logger.error(Strings.paywalls.event_cannot_get_encoded_event) + return nil + } - enum EventType: String { + let creationData = customerCenterEvent.creationData + let data = customerCenterEvent.data - case impression = "customer_center_impression" - case close = "customer_center_close" + return CustomerCenterEventBaseRequest( + id: creationData.id.uuidString, + version: version, + type: customerCenterEvent.eventType, + appUserID: storedEvent.userID, + appSessionID: appSessionID.uuidString, + timestamp: creationData.date.millisecondsSince1970, + darkMode: data.darkMode, + locale: data.localeIdentifier, + isSandbox: data.isSandbox, + displayMode: data.displayMode + ) + } + private static let version: Int = 1 } - @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 - } + // swiftlint:disable:next type_name + final class CustomerCenterAnswerSubmittedEventRequest { + + let id: String? + let version: Int + var type: CustomerCenterEventType + var appUserID: String + var appSessionID: String + var timestamp: UInt64 + var darkMode: Bool + var locale: String + var isSandbox: Bool + var displayMode: CustomerCenterPresentationMode + var path: String + var url: String? + var surveyOptionID: String + var surveyOptionTitleKey: String + var additionalContext: String? + var revisionId: Int - guard let jsonData = storedEvent.encodedEvent.data(using: .utf8) else { - Logger.error(Strings.paywalls.event_cannot_get_encoded_event) - return nil + init(id: String?, + version: Int, + appUserID: String, + appSessionID: String, + timestamp: UInt64, + darkMode: Bool, + locale: String, + isSandbox: Bool, + displayMode: CustomerCenterPresentationMode, + path: CustomerCenterConfigData.HelpPath.PathType, + url: URL?, + surveyOptionID: String, + surveyOptionTitleKey: String, + additionalContext: String?, + revisionId: Int) { + self.id = id + self.version = version + self.type = .answerSubmitted + self.appUserID = appUserID + self.appSessionID = appSessionID + self.timestamp = timestamp + self.darkMode = darkMode + self.locale = locale + self.isSandbox = isSandbox + self.displayMode = displayMode + self.path = path.rawValue + self.url = url?.absoluteString + self.surveyOptionID = surveyOptionID + self.surveyOptionTitleKey = surveyOptionTitleKey + self.additionalContext = additionalContext + self.revisionId = revisionId } - do { - let customerCenterEvent = try JSONDecoder.default.decode(CustomerCenterEvent.self, from: jsonData) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + static func create(from storedEvent: StoredEvent) -> CustomerCenterAnswerSubmittedEventRequest? { + 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 + } + guard let customerCenterEvent = try? JSONDecoder.default.decode(CustomerCenterAnswerSubmittedEvent.self, + from: jsonData) else { + Logger.error(Strings.paywalls.event_cannot_get_encoded_event) + return nil + } + let creationData = customerCenterEvent.creationData let data = customerCenterEvent.data - self.init( + return CustomerCenterAnswerSubmittedEventRequest( id: creationData.id.uuidString, - version: Self.version, - type: customerCenterEvent.eventType, + version: version, appUserID: storedEvent.userID, appSessionID: appSessionID.uuidString, timestamp: creationData.date.millisecondsSince1970, @@ -70,21 +185,25 @@ extension EventsRequest.CustomerCenterEvent { locale: data.localeIdentifier, isSandbox: data.isSandbox, displayMode: data.displayMode, - revisionId: 1 + path: data.path, + url: data.url, + surveyOptionID: data.surveyOptionID, + surveyOptionTitleKey: data.surveyOptionTitleKey, + additionalContext: data.additionalContext, + revisionId: data.revisionID ) - } catch { - return nil } - } - private static let version: Int = 1 + 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 { + var eventType: EventsRequest.CustomerCenterEventType { switch self { case .impression: return .impression } @@ -95,8 +214,8 @@ private extension CustomerCenterEvent { // MARK: - Codable -extension EventsRequest.CustomerCenterEvent.EventType: Encodable {} -extension EventsRequest.CustomerCenterEvent: Encodable { +extension EventsRequest.CustomerCenterEventType: Encodable {} +extension EventsRequest.CustomerCenterEventBaseRequest: Encodable { private enum CodingKeys: String, CodingKey { @@ -115,3 +234,28 @@ extension EventsRequest.CustomerCenterEvent: Encodable { } } + +extension EventsRequest.CustomerCenterAnswerSubmittedEventRequest: Encodable { + + private enum CodingKeys: String, CodingKey { + + case id + case version + case type + case appUserID = "appUserId" + case appSessionID = "appSessionId" + case timestamp + case darkMode + case locale + case isSandbox = "isSandbox" + case displayMode = "displayMode" + case path + case url + case surveyOptionID = "surveyOptionId" + case surveyOptionTitleKey = "surveyOptionTitleKey" + case additionalContext = "additionalContext" + case revisionId = "revisionId" + + } + +} diff --git a/Sources/Events/FeatureEvent.swift b/Sources/Events/FeatureEvent.swift index c941b45a36..e205622a86 100644 --- a/Sources/Events/FeatureEvent.swift +++ b/Sources/Events/FeatureEvent.swift @@ -14,5 +14,6 @@ protocol FeatureEvent: Encodable, Sendable { var feature: Feature { get } + var eventDiscriminator: String? { get } } diff --git a/Sources/Events/Networking/EventsRequest.swift b/Sources/Events/Networking/EventsRequest.swift index 7ca78c03e1..e486c55726 100644 --- a/Sources/Events/Networking/EventsRequest.swift +++ b/Sources/Events/Networking/EventsRequest.swift @@ -32,10 +32,18 @@ struct EventsRequest { } return AnyEncodable(event) case .customerCenter: - guard let event = CustomerCenterEvent(storedEvent: storedEvent) else { - return nil + switch storedEvent.eventDiscriminator { + case CustomerCenterEventDiscriminator.answerSubmitted.rawValue: + guard let event = CustomerCenterAnswerSubmittedEventRequest.create(from: storedEvent) else { + return nil + } + return AnyEncodable(event) + default: + guard let event = CustomerCenterEventBaseRequest.createBase(from: storedEvent) else { + return nil + } + return AnyEncodable(event) } - return AnyEncodable(event) } }) } diff --git a/Sources/Events/StoredEvent.swift b/Sources/Events/StoredEvent.swift index 8b49f9fb05..88b62fed89 100644 --- a/Sources/Events/StoredEvent.swift +++ b/Sources/Events/StoredEvent.swift @@ -20,8 +20,9 @@ struct StoredEvent { private(set) var userID: String private(set) var feature: Feature private(set) var appSessionID: UUID? + private(set) var eventDiscriminator: String? - init?(event: T, userID: String, feature: Feature, appSessionID: UUID?) { + init?(event: T, userID: String, feature: Feature, appSessionID: UUID?, eventDiscriminator: String?) { guard let encodedJSON = try? event.encodedJSON else { return nil } @@ -30,6 +31,7 @@ struct StoredEvent { self.userID = userID self.feature = feature self.appSessionID = appSessionID + self.eventDiscriminator = eventDiscriminator } } @@ -53,6 +55,7 @@ extension StoredEvent: Codable { case userID = "userId" case feature case appSessionID = "appSessionId" + case eventDiscriminator = "eventDiscriminator" } @@ -88,6 +91,10 @@ extension StoredEvent: Codable { if let appSessionID = try container.decodeIfPresent(UUID.self, forKey: .appSessionID) { self.appSessionID = appSessionID } + + if let eventDiscriminator = try container.decodeIfPresent(String.self, forKey: .eventDiscriminator) { + self.eventDiscriminator = eventDiscriminator + } } } @@ -96,7 +103,9 @@ extension StoredEvent: Equatable { static func == (lhs: StoredEvent, rhs: StoredEvent) -> Bool { guard lhs.userID == rhs.userID, - lhs.feature == rhs.feature else { + lhs.feature == rhs.feature, + lhs.appSessionID == rhs.appSessionID, + lhs.eventDiscriminator == rhs.eventDiscriminator else { return false } diff --git a/Sources/Networking/InternalAPI.swift b/Sources/Networking/InternalAPI.swift index eefc98b2a5..c13d230805 100644 --- a/Sources/Networking/InternalAPI.swift +++ b/Sources/Networking/InternalAPI.swift @@ -45,8 +45,9 @@ class InternalAPI { return } + let request = EventsRequest(events: events) let operation = PostPaywallEventsOperation(configuration: .init(httpClient: self.backendConfig.httpClient), - request: .init(events: events), + request: request, responseHandler: completion) self.backendConfig.operationQueue.addOperation(operation) diff --git a/Sources/Paywalls/Events/PaywallEvent.swift b/Sources/Paywalls/Events/PaywallEvent.swift index b865585428..921640deaf 100644 --- a/Sources/Paywalls/Events/PaywallEvent.swift +++ b/Sources/Paywalls/Events/PaywallEvent.swift @@ -30,6 +30,10 @@ public enum PaywallEvent: FeatureEvent { return .paywalls } + var eventDiscriminator: String? { + return nil + } + /// A `PaywallView` was displayed. case impression(CreationData, Data) @@ -57,6 +61,7 @@ extension PaywallEvent { self.id = id self.date = date } + // swiftlint:enable missing_docs } diff --git a/Sources/Paywalls/Events/PaywallEventsManager.swift b/Sources/Paywalls/Events/PaywallEventsManager.swift index b6328e1fb5..9ab27514f2 100644 --- a/Sources/Paywalls/Events/PaywallEventsManager.swift +++ b/Sources/Paywalls/Events/PaywallEventsManager.swift @@ -40,12 +40,13 @@ actor PaywallEventsManager: PaywallEventsManagerType { init( internalAPI: InternalAPI, userProvider: CurrentUserProvider, - store: PaywallEventStoreType + store: PaywallEventStoreType, + appSessionID: UUID = UUID() ) { self.internalAPI = internalAPI self.userProvider = userProvider self.store = store - self.appSessionID = UUID() + self.appSessionID = appSessionID } func resetAppSessionID() { @@ -56,7 +57,8 @@ actor PaywallEventsManager: PaywallEventsManagerType { guard let event: StoredEvent = .init(event: featureEvent, userID: self.userProvider.currentAppUserID, feature: featureEvent.feature, - appSessionID: self.appSessionID) else { + appSessionID: self.appSessionID, + eventDiscriminator: featureEvent.eventDiscriminator) else { Logger.error(Strings.paywalls.event_cannot_serialize) return } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index e2b0ae459e..efcf8136bb 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -1271,9 +1271,11 @@ public extension Purchases { } /// Used by `RevenueCatUI` to keep track of ``CustomerCenterEvent``s. - func track(customerCenterEvent: CustomerCenterEvent) { + func track(customerCenterEvent: any CustomerCenterEventType) { operationDispatcher.dispatchOnWorkerThread { - await self.paywallEventsManager?.track(featureEvent: customerCenterEvent) + // If we make CustomerCenterEventType implement FeatureEvent, we have to make FeatureEvent public + guard let event = customerCenterEvent as? FeatureEvent else { return } + await self.paywallEventsManager?.track(featureEvent: event) } } diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 9543aa54b1..997740bfa6 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -207,7 +207,7 @@ class CustomerCenterViewModelTests: TestCase { viewModel.trackImpression(darkMode: darkMode, displayMode: displayMode) expect(mockPurchases.trackedEvents.count) == 1 - let trackedEvent = try XCTUnwrap(mockPurchases.trackedEvents.first) + let trackedEvent = try XCTUnwrap(mockPurchases.trackedEvents.first as? CustomerCenterEvent) expect(trackedEvent.data.darkMode) == darkMode expect(trackedEvent.data.displayMode) == displayMode diff --git a/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift b/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift index 74be986619..4534f14651 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift @@ -55,8 +55,8 @@ final class MockCustomerCenterPurchases: @unchecked Sendable, CustomerCenterPurc var trackCallCount = 0 var trackError: Error? - var trackedEvents: [CustomerCenterEvent] = [] - func track(customerCenterEvent: CustomerCenterEvent) { + var trackedEvents: [CustomerCenterEventType] = [] + func track(customerCenterEvent: any CustomerCenterEventType) { trackCallCount += 1 trackedEvents.append(customerCenterEvent) } diff --git a/Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift b/Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift index 27a6fc379e..f2b07754d9 100644 --- a/Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift +++ b/Tests/UnitTests/CustomerCenter/Events/CustomerCenterEventsRequestTests.swift @@ -28,18 +28,20 @@ class CustomerCenterEventsRequestTests: TestCase { func testImpressionEvent() throws { let event = CustomerCenterEvent.impression(Self.eventCreationData, Self.eventData) + let eventDiscriminator: String = CustomerCenterEventDiscriminator.lifecycle.rawValue let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, userID: Self.userID, feature: .customerCenter, - appSessionID: Self.appSessionID)) - let requestEvent: EventsRequest.CustomerCenterEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) + appSessionID: Self.appSessionID, + eventDiscriminator: eventDiscriminator)) + let requestEvent = try XCTUnwrap(EventsRequest.CustomerCenterEventBaseRequest.createBase(from: storedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) } func testCanInitFromDeserializedEvent() throws { let expectedUserID = "test-user" - let customerCenterEventCreationData: CustomerCenterEvent.CreationData = .init( + let customerCenterEventCreationData: CustomerCenterEventCreationData = .init( id: .init(uuidString: "72164C05-2BDC-4807-8918-A4105F727DEB")!, date: .init(timeIntervalSince1970: 1694029328) ) @@ -55,20 +57,22 @@ class CustomerCenterEventsRequestTests: TestCase { let storedEvent = try XCTUnwrap(StoredEvent(event: customerCenterEvent, userID: expectedUserID, feature: .customerCenter, - appSessionID: Self.appSessionID)) + appSessionID: Self.appSessionID, + eventDiscriminator: "impression")) 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)) + let requestEvent = + try XCTUnwrap(EventsRequest.CustomerCenterEventBaseRequest.createBase(from: deserializedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) } // MARK: - - private static let eventCreationData: CustomerCenterEvent.CreationData = .init( + private static let eventCreationData: CustomerCenterEventCreationData = .init( id: .init(uuidString: "72164C05-2BDC-4807-8918-A4105F727DEB")!, date: .init(timeIntervalSince1970: 1694029328) ) diff --git a/Tests/UnitTests/Paywalls/Events/PaywallEventStoreTests.swift b/Tests/UnitTests/Paywalls/Events/PaywallEventStoreTests.swift index f8ebaed3c8..06597a2a83 100644 --- a/Tests/UnitTests/Paywalls/Events/PaywallEventStoreTests.swift +++ b/Tests/UnitTests/Paywalls/Events/PaywallEventStoreTests.swift @@ -285,7 +285,8 @@ private extension StoredEvent { return .init(event: event, userID: UUID().uuidString, feature: .paywalls, - appSessionID: UUID())! + appSessionID: UUID(), + eventDiscriminator: "impression")! } } diff --git a/Tests/UnitTests/Paywalls/Events/PaywallEventsBackendTests.swift b/Tests/UnitTests/Paywalls/Events/PaywallEventsBackendTests.swift index 75a8dcad23..c2dcf729ee 100644 --- a/Tests/UnitTests/Paywalls/Events/PaywallEventsBackendTests.swift +++ b/Tests/UnitTests/Paywalls/Events/PaywallEventsBackendTests.swift @@ -41,10 +41,7 @@ class BackendPaywallEventTests: BaseBackendTests { func testPostPaywallEventsWithOneEvent() throws { let event = PaywallEvent.impression(Self.eventCreation1, Self.eventData1) - let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID())) + let storedEvent: StoredEvent = try Self.createStoredEvent(from: event) let error = waitUntilValue { completion in self.internalAPI.postPaywallEvents(events: [storedEvent], completion: completion) @@ -55,15 +52,9 @@ class BackendPaywallEventTests: BaseBackendTests { func testPostPaywallEventsWithMultipleEvents() throws { let event1 = PaywallEvent.impression(Self.eventCreation1, Self.eventData1) - let storedEvent1: StoredEvent = try XCTUnwrap(.init(event: event1, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID())) + let storedEvent1: StoredEvent = try Self.createStoredEvent(from: event1) let event2 = PaywallEvent.close(Self.eventCreation2, Self.eventData2) - let storedEvent2: StoredEvent = try XCTUnwrap(.init(event: event2, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID())) + let storedEvent2: StoredEvent = try Self.createStoredEvent(from: event2) let error = waitUntilValue { completion in self.internalAPI.postPaywallEvents(events: [storedEvent1, storedEvent2], @@ -108,4 +99,12 @@ private extension BackendPaywallEventTests { darkMode: false ) + static func createStoredEvent(from event: PaywallEvent) throws -> StoredEvent { + return try XCTUnwrap(.init(event: event, + userID: Self.userID, + feature: .paywalls, + appSessionID: UUID(), + eventDiscriminator: "impression")) + } + } diff --git a/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift b/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift index 41ba067e4f..6944262620 100644 --- a/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift +++ b/Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift @@ -25,6 +25,7 @@ class PaywallEventsManagerTests: TestCase { private var userProvider: MockCurrentUserProvider! private var store: MockPaywallEventStore! private var manager: PaywallEventsManager! + private var appSessionID = UUID() override func setUpWithError() throws { try super.setUpWithError() @@ -37,7 +38,8 @@ class PaywallEventsManagerTests: TestCase { self.manager = .init( internalAPI: self.api, userProvider: self.userProvider, - store: self.store + store: self.store, + appSessionID: self.appSessionID ) } @@ -50,10 +52,7 @@ class PaywallEventsManagerTests: TestCase { let events = await self.store.storedEvents expect(events) == [ - try XCTUnwrap(.init(event: event, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID())) + try createStoredEvent(from: event) ] } @@ -66,14 +65,8 @@ class PaywallEventsManagerTests: TestCase { let events = await self.store.storedEvents expect(events) == [ - try XCTUnwrap(.init(event: event1, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID())), - try XCTUnwrap(.init(event: event2, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID())) + try createStoredEvent(from: event1), + try createStoredEvent(from: event2) ] } @@ -92,10 +85,7 @@ class PaywallEventsManagerTests: TestCase { expect(result) == 1 expect(self.api.invokedPostPaywallEvents) == true - expect(self.api.invokedPostPaywallEventsParameters) == [[try XCTUnwrap(.init(event: event, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID()))]] + expect(self.api.invokedPostPaywallEventsParameters) == [[try createStoredEvent(from: event)]] await self.verifyEmptyStore() } @@ -112,14 +102,8 @@ class PaywallEventsManagerTests: TestCase { expect(self.api.invokedPostPaywallEvents) == true expect(self.api.invokedPostPaywallEventsParameters) == [ - [try XCTUnwrap(.init(event: event1, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID()))], - [try XCTUnwrap(.init(event: event2, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID()))] + [try createStoredEvent(from: event1)], + [try createStoredEvent(from: event2)] ] await self.verifyEmptyStore() @@ -127,10 +111,7 @@ 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, - appSessionID: UUID())) + let storedEvent = try createStoredEvent(from: event) _ = await self.storeRandomEvent() _ = await self.storeRandomEvent() @@ -148,10 +129,7 @@ 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, - appSessionID: UUID())) + let storedEvent = try createStoredEvent(from: event) let expectedError: NetworkError = .offlineConnection() self.api.stubbedPostPaywallEventsCompletionResult = .networkError(expectedError) @@ -209,16 +187,10 @@ class PaywallEventsManagerTests: TestCase { } expect(self.api.invokedPostPaywallEvents) == true - let expectedEvent: StoredEvent = try XCTUnwrap(.init(event: event1, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID())) + let expectedEvent = try createStoredEvent(from: event1) expect(self.api.invokedPostPaywallEventsParameters) == [[expectedEvent]] - await self.verifyEvents([try XCTUnwrap(.init(event: event2, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID()))]) + await self.verifyEvents([try createStoredEvent(from: event2)]) } #if swift(>=5.9) @@ -259,10 +231,7 @@ 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, - appSessionID: UUID())) + try createStoredEvent(from: event1) ] self.logger.verifyMessageWasLogged( @@ -321,6 +290,14 @@ private extension PaywallEventsManagerTests { expect(file: file, line: line, events) == expected } + func createStoredEvent(from event: PaywallEvent) throws -> StoredEvent { + return try XCTUnwrap(.init(event: event, + userID: Self.userID, + feature: .paywalls, + appSessionID: self.appSessionID, + eventDiscriminator: nil)) + } + } // MARK: - MockPaywallEventStore diff --git a/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift b/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift index 4975a4290e..0060d3966e 100644 --- a/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift +++ b/Tests/UnitTests/Paywalls/Events/PaywallEventsRequestTests.swift @@ -28,10 +28,7 @@ class PaywallEventsRequestTests: TestCase { func testImpressionEvent() throws { let event = PaywallEvent.impression(Self.eventCreationData, Self.eventData) - let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, - userID: Self.userID, - feature: .paywalls, - appSessionID: Self.appSessionID)) + let storedEvent = try Self.createStoredEvent(from: event) let requestEvent: EventsRequest.PaywallEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) @@ -39,22 +36,15 @@ class PaywallEventsRequestTests: TestCase { func testCancelEvent() throws { let event = PaywallEvent.cancel(Self.eventCreationData, Self.eventData) - let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, - userID: Self.userID, - feature: .paywalls, - appSessionID: Self.appSessionID)) + let storedEvent = try Self.createStoredEvent(from: event) let requestEvent: EventsRequest.PaywallEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) } func testCloseEvent() throws { - let event = PaywallEvent.close(Self.eventCreationData, - Self.eventData) - let storedEvent: StoredEvent = try XCTUnwrap(.init(event: event, - userID: Self.userID, - feature: .paywalls, - appSessionID: Self.appSessionID)) + let event = PaywallEvent.close(Self.eventCreationData, Self.eventData) + let storedEvent = try Self.createStoredEvent(from: event) let requestEvent: EventsRequest.PaywallEvent = try XCTUnwrap(.init(storedEvent: storedEvent)) assertSnapshot(matching: requestEvent, as: .formattedJson) @@ -79,7 +69,8 @@ class PaywallEventsRequestTests: TestCase { let storedEvent = try XCTUnwrap(StoredEvent(event: paywallEvent, userID: expectedUserID, feature: .paywalls, - appSessionID: Self.appSessionID)) + appSessionID: Self.appSessionID, + eventDiscriminator: "impression")) let serializedEvent = try StoredEventSerializer.encode(storedEvent) let deserializedEvent = try StoredEventSerializer.decode(serializedEvent) expect(deserializedEvent.userID) == expectedUserID @@ -92,12 +83,25 @@ class PaywallEventsRequestTests: TestCase { // MARK: - - private static let eventCreationData: PaywallEvent.CreationData = .init( +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension PaywallEventsRequestTests { + + static func createStoredEvent(from event: PaywallEvent) throws -> StoredEvent { + return try XCTUnwrap(.init(event: event, + userID: Self.userID, + feature: .paywalls, + appSessionID: Self.appSessionID, + eventDiscriminator: "impression")) + } + + static let eventCreationData: PaywallEvent.CreationData = .init( id: .init(uuidString: "72164C05-2BDC-4807-8918-A4105F727DEB")!, date: .init(timeIntervalSince1970: 1694029328) ) - private static let eventData: PaywallEvent.Data = .init( + static let eventData: PaywallEvent.Data = .init( offeringIdentifier: "offering", paywallRevision: 0, sessionID: .init(uuidString: "98CC0F1D-7665-4093-9624-1D7308FFF4DB")!, @@ -106,8 +110,8 @@ class PaywallEventsRequestTests: TestCase { darkMode: true ) - private static let userID = "Jack Shepard" + static let userID = "Jack Shepard" - private static let appSessionID = UUID(uuidString: "83164C05-2BDC-4807-8918-A4105F727DEB") + static let appSessionID = UUID(uuidString: "83164C05-2BDC-4807-8918-A4105F727DEB") } diff --git a/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift b/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift index 83c1504fac..91468b929d 100644 --- a/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift +++ b/Tests/UnitTests/Paywalls/Events/StoredEventSerializerTests.swift @@ -27,30 +27,20 @@ class StoredEventSerializerTests: TestCase { func testEncodeImpressionEvent() throws { let originalEvent = PaywallEvent.impression(.random(), .random()) - let event: StoredEvent = try XCTUnwrap(.init(event: originalEvent, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID())) - + let event = try Self.createStoredEvent(from: originalEvent) expect(try event.encodeAndDecode()) == event } func testDecodeCancelEvent() throws { let originalEvent = PaywallEvent.cancel(.random(), .random()) - let event: StoredEvent = try XCTUnwrap(.init(event: originalEvent, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID())) + let event = try Self.createStoredEvent(from: originalEvent) expect(try event.encodeAndDecode()) == event } func testDecodeCloseEvent() throws { let originalEvent = PaywallEvent.close(.random(), .random()) - let event: StoredEvent = try XCTUnwrap(.init(event: originalEvent, - userID: Self.userID, - feature: .paywalls, - appSessionID: UUID())) + let event = try Self.createStoredEvent(from: originalEvent) expect(try event.encodeAndDecode()) == event } @@ -71,10 +61,7 @@ class StoredEventSerializerTests: TestCase { ) let paywallEvent = PaywallEvent.impression(paywallEventCreationData, paywallEventData) - let storedEvent = try XCTUnwrap(StoredEvent(event: paywallEvent, - userID: expectedUserID, - feature: .paywalls, - appSessionID: UUID())) + let storedEvent = try Self.createStoredEvent(from: paywallEvent, expectedUserID: expectedUserID) let serializedEvent = try StoredEventSerializer.encode(storedEvent) let deserializedEvent = try StoredEventSerializer.decode(serializedEvent) expect(deserializedEvent.userID) == expectedUserID @@ -85,9 +72,20 @@ class StoredEventSerializerTests: TestCase { expect(decodedPaywallEvent) == paywallEvent } - // MARK: - +} + +@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) +private extension StoredEventSerializerTests { + + static let userID = UUID().uuidString - private static let userID = UUID().uuidString + static func createStoredEvent(from event: PaywallEvent, expectedUserID: String = userID) throws -> StoredEvent { + return try XCTUnwrap(.init(event: event, + userID: expectedUserID, + feature: .paywalls, + appSessionID: UUID(), + eventDiscriminator: "impression")) + } }