From 116a849f4f756349f2b55d3527f2dc3f6afd60c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Wed, 9 Aug 2023 16:23:23 +0800 Subject: [PATCH 01/12] Adds support for `enable_expression_params` feature flag - Collects all the device/user/event params plus all the computed properties from all the rules and adds them as stringified JSON to the presentation request event. --- CHANGELOG.md | 7 +++++ .../TrackableSuperwallEvent.swift | 23 ++++++++++++-- .../SuperwallKit/Config/Models/Config.swift | 7 +++++ .../Config/Models/FeatureFlags.swift | 9 ++++-- .../Dependencies/DependencyContainer.swift | 31 ++++++++++++++----- .../Dependencies/FactoryProtocols.swift | 14 +++++++-- Sources/SuperwallKit/Misc/Constants.swift | 2 +- .../Network/Device Helper/DeviceHelper.swift | 1 + .../Operators/LogErrors.swift | 10 ++++-- .../Operators/PresentPaywall.swift | 8 +++-- .../Operators/WaitToPresent.swift | 6 ++-- .../ExpressionEvaluator.swift | 14 +++------ SuperwallKit.podspec | 2 +- 13 files changed, 100 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c03a6edea..71eccec9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall-me/Superwall-iOS/releases) on GitHub. +## 3.3.1 + +### Enhancements + +- Adds an `enable_expression_params` feature flag which can enhance debugging by sending a stringified version of all the device/user/event parameters with a `paywallPresentationRequest` event. + + ## 3.3.0 ### Enhancements diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift index f9c251ea7..3ea812755 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift @@ -288,18 +288,37 @@ enum InternalSuperwallEvent { let type: PresentationRequestType let status: PaywallPresentationRequestStatus let statusReason: PaywallPresentationRequestStatusReason? + let factory: RuleAttributesFactory & FeatureFlagsFactory & ComputedPropertyRequestsFactory var superwallEvent: SuperwallEvent { - return .paywallPresentationRequest(status: status, reason: statusReason) + return .paywallPresentationRequest( + status: status, + reason: statusReason + ) } var customParameters: [String: Any] = [:] func getSuperwallParameters() async -> [String: Any] { - [ + var params = [ "source_event_name": eventData?.name ?? "", "pipeline_type": type.description, "status": status.rawValue, "status_reason": statusReason?.description ?? "" ] + + if let featureFlags = factory.makeFeatureFlags(), + featureFlags.enableExpressionParameters { + let computedPropertyRequests = factory.makeComputedPropertyRequests() + let rules = await factory.makeRuleAttributes( + forEvent: eventData, + withComputedProperties: computedPropertyRequests + ) + let rulesString = rules.description + params += [ + "expression_params": rulesString + ] + } + + return params } } diff --git a/Sources/SuperwallKit/Config/Models/Config.swift b/Sources/SuperwallKit/Config/Models/Config.swift index 9689bf078..958fad97c 100644 --- a/Sources/SuperwallKit/Config/Models/Config.swift +++ b/Sources/SuperwallKit/Config/Models/Config.swift @@ -17,6 +17,13 @@ struct Config: Decodable { var featureFlags: FeatureFlags var preloadingDisabled: PreloadingDisabled var requestId: String? + var allComputedProperties: [ComputedPropertyRequest] { + return triggers.flatMap { + $0.rules.flatMap { + $0.computedPropertyRequests + } + } + } enum CodingKeys: String, CodingKey { case triggers = "triggerOptions" diff --git a/Sources/SuperwallKit/Config/Models/FeatureFlags.swift b/Sources/SuperwallKit/Config/Models/FeatureFlags.swift index 147022499..e35baa548 100644 --- a/Sources/SuperwallKit/Config/Models/FeatureFlags.swift +++ b/Sources/SuperwallKit/Config/Models/FeatureFlags.swift @@ -15,6 +15,7 @@ struct RawFeatureFlag: Decodable { struct FeatureFlags: Decodable { var enableSessionEvents: Bool var enablePostback: Bool + var enableExpressionParameters: Bool enum CodingKeys: String, CodingKey { case toggles @@ -25,15 +26,18 @@ struct FeatureFlags: Decodable { let rawFeatureFlags = try values.decode([RawFeatureFlag].self, forKey: .toggles) enableSessionEvents = rawFeatureFlags.value(forKey: "enable_session_events", default: false) + enableExpressionParameters = rawFeatureFlags.value(forKey: "enable_expression_params", default: false) enablePostback = rawFeatureFlags.value(forKey: "enable_postback", default: false) } init( enableSessionEvents: Bool, - enablePostback: Bool + enablePostback: Bool, + enableExpressionParameters: Bool ) { self.enableSessionEvents = enableSessionEvents self.enablePostback = enablePostback + self.enableExpressionParameters = enableExpressionParameters } } @@ -53,7 +57,8 @@ extension FeatureFlags: Stubbable { static func stub() -> FeatureFlags { return FeatureFlags( enableSessionEvents: true, - enablePostback: true + enablePostback: true, + enableExpressionParameters: true ) } } diff --git a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift index a02dd244a..54f8221ca 100644 --- a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift +++ b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift @@ -320,21 +320,22 @@ extension DependencyContainer: ApiFactory { // MARK: - Rule Params extension DependencyContainer: RuleAttributesFactory { func makeRuleAttributes( - forEvent event: EventData, - from rule: TriggerRule - ) async -> RuleAttributes { + forEvent event: EventData?, + withComputedProperties computedPropertyRequests: [ComputedPropertyRequest] + ) async -> JSON { var userAttributes = identityManager.userAttributes userAttributes["isLoggedIn"] = identityManager.isLoggedIn let deviceAttributes = await deviceHelper.getDeviceAttributes( since: event, - computedPropertyRequests: rule.computedPropertyRequests + computedPropertyRequests: computedPropertyRequests ) - return RuleAttributes( - user: userAttributes, - device: deviceAttributes - ) + return JSON([ + "user": userAttributes, + "device": deviceAttributes, + "params": event?.parameters ?? "" + ] as [String: Any]) } } @@ -453,3 +454,17 @@ extension DependencyContainer: HasPurchaseControllerFactory { return delegateAdapter.hasPurchaseController } } + +// MARK: - Feature Flags Factory +extension DependencyContainer: FeatureFlagsFactory { + func makeFeatureFlags() -> FeatureFlags? { + return configManager.config?.featureFlags + } +} + +// MARK: - Computed Property Requests Factory +extension DependencyContainer: ComputedPropertyRequestsFactory { + func makeComputedPropertyRequests() -> [ComputedPropertyRequest] { + return configManager.config?.allComputedProperties ?? [] + } +} diff --git a/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift b/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift index 33fdd6935..42aa65395 100644 --- a/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift +++ b/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift @@ -55,9 +55,17 @@ protocol RequestFactory: AnyObject { protocol RuleAttributesFactory: AnyObject { func makeRuleAttributes( - forEvent event: EventData, - from rule: TriggerRule - ) async -> RuleAttributes + forEvent event: EventData?, + withComputedProperties computedPropertyRequests: [ComputedPropertyRequest] + ) async -> JSON +} + +protocol FeatureFlagsFactory: AnyObject { + func makeFeatureFlags() -> FeatureFlags? +} + +protocol ComputedPropertyRequestsFactory: AnyObject { + func makeComputedPropertyRequests() -> [ComputedPropertyRequest] } protocol TriggerSessionManagerFactory: AnyObject { diff --git a/Sources/SuperwallKit/Misc/Constants.swift b/Sources/SuperwallKit/Misc/Constants.swift index fce905c0e..7ab72ae0a 100644 --- a/Sources/SuperwallKit/Misc/Constants.swift +++ b/Sources/SuperwallKit/Misc/Constants.swift @@ -18,5 +18,5 @@ let sdkVersion = """ */ let sdkVersion = """ -3.3.0 +3.3.1 """ diff --git a/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift b/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift index 3bd810e8e..ae38c2c8c 100644 --- a/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift +++ b/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift @@ -307,6 +307,7 @@ class DeviceHelper { requests: computedPropertyRequests ) dictionary += computedProperties + return dictionary } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogErrors.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogErrors.swift index a356de04b..0827e3c27 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogErrors.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogErrors.swift @@ -17,15 +17,19 @@ extension Superwall { // Don't print anything if we've just cancelled a pipeline that timed out. return } - Task { + Task { [weak self] in + guard let self = self else { + return + } if let reason = error as? PresentationPipelineError { let trackedEvent = InternalSuperwallEvent.PresentationRequest( eventData: request.presentationInfo.eventData, type: request.flags.type, status: .noPresentation, - statusReason: reason + statusReason: reason, + factory: self.dependencyContainer ) - await track(trackedEvent) + await self.track(trackedEvent) } } Logger.debug( diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywall.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywall.swift index 73049a549..dc4fa68a6 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywall.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywall.swift @@ -25,13 +25,17 @@ extension Superwall { paywallStatePublisher: PassthroughSubject ) async throws { Task.detached { [weak self] in + guard let self = self else { + return + } let trackedEvent = InternalSuperwallEvent.PresentationRequest( eventData: request.presentationInfo.eventData, type: request.flags.type, status: .presentation, - statusReason: nil + statusReason: nil, + factory: self.dependencyContainer ) - await self?.track(trackedEvent) + await self.track(trackedEvent) } try await withCheckedThrowingContinuation { continuation in diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresent.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresent.swift index 8ab698491..45755be48 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresent.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresent.swift @@ -48,7 +48,8 @@ extension Superwall { eventData: request.presentationInfo.eventData, type: request.flags.type, status: .timeout, - statusReason: .subscriptionStatusTimeout + statusReason: .subscriptionStatusTimeout, + factory: dependencyContainer ) await self.track(trackedEvent) } @@ -101,7 +102,8 @@ extension Superwall { eventData: request.presentationInfo.eventData, type: request.flags.type, status: .timeout, - statusReason: .noConfig + statusReason: .noConfig, + factory: dependencyContainer ) await self.track(trackedEvent) } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluator.swift b/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluator.swift index e703fd9be..e1d0fe047 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluator.swift @@ -86,21 +86,15 @@ struct ExpressionEvaluator { forRule rule: TriggerRule, withEventData eventData: EventData ) async -> String? { - let ruleAttributes = await factory.makeRuleAttributes( + let attributes = await factory.makeRuleAttributes( forEvent: eventData, - from: rule + withComputedProperties: rule.computedPropertyRequests ) - let values = JSON([ - "user": ruleAttributes.user, - "device": ruleAttributes.device, - "params": eventData.parameters - ] as [String: Any]) - if let expressionJs = rule.expressionJs { if let base64Params = JavascriptExpressionEvaluatorParams( expressionJs: expressionJs, - values: values + values: attributes ).toBase64Input() { let postfix = "\n SuperwallSDKJS.evaluateJS64('\(base64Params)');" return postfix @@ -109,7 +103,7 @@ struct ExpressionEvaluator { } else if let expression = rule.expression { if let base64Params = LiquidExpressionEvaluatorParams( expression: expression, - values: values + values: attributes ).toBase64Input() { let postfix = "\n SuperwallSDKJS.evaluate64('\(base64Params)');" return postfix diff --git a/SuperwallKit.podspec b/SuperwallKit.podspec index f679afafb..7d313b593 100644 --- a/SuperwallKit.podspec +++ b/SuperwallKit.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "SuperwallKit" - s.version = "3.3.0" + s.version = "3.3.1" s.summary = "Superwall: In-App Paywalls Made Easy" s.description = "Paywall infrastructure for mobile apps :) we make things like editing your paywall and running price tests as easy as clicking a few buttons. superwall.com" From 453088fd07b15a0c78a2055f81c233cfc8e1f97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Wed, 9 Aug 2023 16:26:40 +0800 Subject: [PATCH 02/12] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71eccec9b..a54b220bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup - Adds an `enable_expression_params` feature flag which can enhance debugging by sending a stringified version of all the device/user/event parameters with a `paywallPresentationRequest` event. - ## 3.3.0 ### Enhancements From ca0eb3c9f600adc46af15c62b2e03b48759c6d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 10 Aug 2023 13:24:30 +0800 Subject: [PATCH 03/12] Used JSONSerialization for expression params, reenabled parallelization for unit tests --- .../xcschemes/SuperwallKit.xcscheme | 3 +- .../TrackableSuperwallEvent.swift | 12 ++++--- .../Dependencies/DependencyContainer.swift | 3 +- .../ExpressionEvaluator.swift | 5 +-- SuperwallKit.xcodeproj/project.pbxproj | 22 +++++++++++- .../xcschemes/SuperwallKit.xcscheme | 29 ++++++++-------- .../Internal Tracking/TrackTests.swift | 34 ++++++++++++++----- project.yml | 1 + 8 files changed, 75 insertions(+), 34 deletions(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SuperwallKit.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SuperwallKit.xcscheme index cb1ec0db8..fa0270a24 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/SuperwallKit.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/SuperwallKit.xcscheme @@ -35,7 +35,8 @@ + skipped = "NO" + parallelizable = "YES"> + buildImplicitDependencies = "YES"> - - - - - - + + + + + + Date: Thu, 10 Aug 2023 16:12:38 +0800 Subject: [PATCH 04/12] Added test for expression params when tracking and removed parallelizable again... --- .../xcschemes/SuperwallKit.xcscheme | 3 +-- .../Internal Tracking/TrackTests.swift | 23 +++++++++++++++++++ project.yml | 1 - 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SuperwallKit.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SuperwallKit.xcscheme index fa0270a24..cb1ec0db8 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/SuperwallKit.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/SuperwallKit.xcscheme @@ -35,8 +35,7 @@ + skipped = "NO"> Date: Thu, 10 Aug 2023 16:13:02 +0800 Subject: [PATCH 05/12] Updated xcode proj --- SuperwallKit.xcodeproj/project.pbxproj | 2 +- .../xcschemes/SuperwallKit.xcscheme | 29 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/SuperwallKit.xcodeproj/project.pbxproj b/SuperwallKit.xcodeproj/project.pbxproj index 5021d9f0f..d7bc6540c 100644 --- a/SuperwallKit.xcodeproj/project.pbxproj +++ b/SuperwallKit.xcodeproj/project.pbxproj @@ -2230,7 +2230,7 @@ path = Assignment; sourceTree = ""; }; - "TEMP_ECA8B518-590E-4F20-9C6C-E8E617C40640" /* Events */ = { + "TEMP_5FF3E573-7335-434E-94E2-8F7B70E94A14" /* Events */ = { isa = PBXGroup; children = ( ); diff --git a/SuperwallKit.xcodeproj/xcshareddata/xcschemes/SuperwallKit.xcscheme b/SuperwallKit.xcodeproj/xcshareddata/xcschemes/SuperwallKit.xcscheme index e7cb028d2..092f92ad0 100644 --- a/SuperwallKit.xcodeproj/xcshareddata/xcschemes/SuperwallKit.xcscheme +++ b/SuperwallKit.xcodeproj/xcshareddata/xcschemes/SuperwallKit.xcscheme @@ -4,7 +4,8 @@ version = "1.3"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> + + + + + + - - - - - - Date: Tue, 15 Aug 2023 13:36:58 +0800 Subject: [PATCH 06/12] Adds `enable_userid_seed` feature flag to enable generation of the user's seed based on a supplied userId --- CHANGELOG.md | 3 +- .../Superwall Event/SuperwallEvent.swift | 2 +- .../Config/Models/FeatureFlags.swift | 9 +++- .../Identity/IdentityManager.swift | 14 ++++++ .../Extensions/String/String+SHA256.swift | 45 +++++++++++++++++++ 5 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 Sources/SuperwallKit/Misc/Extensions/String/String+SHA256.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a54b220bc..e9d71f5c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup ### Enhancements -- Adds an `enable_expression_params` feature flag which can enhance debugging by sending a stringified version of all the device/user/event parameters with a `paywallPresentationRequest` event. +- Adds logic to enhance debugging by sending a stringified version of all the device/user/event parameters used to evaluate rules within the `paywallPresentationRequest` event. This is behind a feature flag. +- Adds logic to keep the user's generated `seed` value consistent when `Superwall.identify` is called. This is behind a feature flag. ## 3.3.0 diff --git a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift index 168c78949..8c3a75d96 100644 --- a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift @@ -54,7 +54,7 @@ public enum SuperwallEvent { /// When a paywall is closed. case paywallClose(paywallInfo: PaywallInfo) - /// When a user dismisses a paywall instead of purchasing + /// When a user manually dismisses a paywall. case paywallDecline(paywallInfo: PaywallInfo) /// When the payment sheet is displayed to the user. diff --git a/Sources/SuperwallKit/Config/Models/FeatureFlags.swift b/Sources/SuperwallKit/Config/Models/FeatureFlags.swift index e35baa548..2ba7d9d74 100644 --- a/Sources/SuperwallKit/Config/Models/FeatureFlags.swift +++ b/Sources/SuperwallKit/Config/Models/FeatureFlags.swift @@ -16,6 +16,7 @@ struct FeatureFlags: Decodable { var enableSessionEvents: Bool var enablePostback: Bool var enableExpressionParameters: Bool + var enableUserIdSeed: Bool enum CodingKeys: String, CodingKey { case toggles @@ -28,16 +29,19 @@ struct FeatureFlags: Decodable { enableSessionEvents = rawFeatureFlags.value(forKey: "enable_session_events", default: false) enableExpressionParameters = rawFeatureFlags.value(forKey: "enable_expression_params", default: false) enablePostback = rawFeatureFlags.value(forKey: "enable_postback", default: false) + enableUserIdSeed = rawFeatureFlags.value(forKey: "enable_userid_seed", default: false) } init( enableSessionEvents: Bool, enablePostback: Bool, - enableExpressionParameters: Bool + enableExpressionParameters: Bool, + enableUserIdSeed: Bool ) { self.enableSessionEvents = enableSessionEvents self.enablePostback = enablePostback self.enableExpressionParameters = enableExpressionParameters + self.enableUserIdSeed = enableUserIdSeed } } @@ -58,7 +62,8 @@ extension FeatureFlags: Stubbable { return FeatureFlags( enableSessionEvents: true, enablePostback: true, - enableExpressionParameters: true + enableExpressionParameters: true, + enableUserIdSeed: true ) } } diff --git a/Sources/SuperwallKit/Identity/IdentityManager.swift b/Sources/SuperwallKit/Identity/IdentityManager.swift index abc14b05a..06a7a5104 100644 --- a/Sources/SuperwallKit/Identity/IdentityManager.swift +++ b/Sources/SuperwallKit/Identity/IdentityManager.swift @@ -194,6 +194,20 @@ class IdentityManager { self._appUserId = userId + // Regenerate seed based on userId. + self.group.enter() + Task { + let config = try? await self.configManager.configState + .compactMap { $0.getConfig() } + .throwableAsync() + + if config?.featureFlags.enableUserIdSeed == true, + let seed = userId.sha256MappedToRange() { + self._seed = seed + } + self.group.leave() + } + func getAssignmentsAsync() { Task.detached { try? await self.configManager.getAssignments() diff --git a/Sources/SuperwallKit/Misc/Extensions/String/String+SHA256.swift b/Sources/SuperwallKit/Misc/Extensions/String/String+SHA256.swift new file mode 100644 index 000000000..e0bc41ed6 --- /dev/null +++ b/Sources/SuperwallKit/Misc/Extensions/String/String+SHA256.swift @@ -0,0 +1,45 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 15/08/2023. +// + +import Foundation +import CommonCrypto + +extension String { + /// Creates SHA256 hash of string. + func sha256() -> [UInt8]? { + guard let data = data(using: .utf8) else { + return nil + } + var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) + } + return hash + } + + /// Produces a number from 0 to 99 inclusive based on the SHA256 hash value + /// of the string. + func sha256MappedToRange() -> Int? { + guard let hashBytes = sha256() else { + return nil + } + + // Break the hash into 8-byte chunks + let chunks = stride(from: 0, to: hashBytes.count, by: 8).map { + Array(hashBytes[$0..<$0 + 8]) + } + + // Sum the modulo 100 value of each chunk + var sum: UInt64 = 0 + for chunk in chunks { + let chunkValue = chunk.withUnsafeBytes { $0.load(as: UInt64.self) } + sum = sum &+ chunkValue // &+ ensures wrapping addition + } + + return Int(sum % 100) + } +} From 64b03b51abb622cf10f83a3d2b965db2af1d9199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Wed, 16 Aug 2023 14:40:29 +0800 Subject: [PATCH 07/12] Adds to `survey_attached` and `survey_presentation` to `paywall_close` - `survey_attached` is true if the paywall has a survey and false if not. - `survey_presentation` can be either show, holdout or nil if the survey wasn't supposed to show. - Added tests for new logic. --- .../TrackableSuperwallEvent.swift | 14 +- .../SuperwallKit/Config/Models/Survey.swift | 23 ++- .../SuperwallKit/Models/Paywall/Paywall.swift | 2 +- .../Models/Triggers/TriggerResult.swift | 2 +- .../PaywallViewController.swift | 13 +- .../{ => Survey}/SurveyManager.swift | 28 +-- .../Survey/SurveyPresentationResult.swift | 14 ++ .../Internal Tracking/TrackTests.swift | 171 +++++++++++++++++- .../Config/Models/SurveyTests.swift | 115 ++++++++++++ .../View Controller/SurveyManagerTests.swift | 38 ++-- .../Storage/StorageMock.swift | 7 + 11 files changed, 379 insertions(+), 48 deletions(-) rename Sources/SuperwallKit/Paywall/View Controller/{ => Survey}/SurveyManager.swift (91%) create mode 100644 Sources/SuperwallKit/Paywall/View Controller/Survey/SurveyPresentationResult.swift create mode 100644 Tests/SuperwallKitTests/Config/Models/SurveyTests.swift diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift index 239929e01..265709de9 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift @@ -344,8 +344,20 @@ enum InternalSuperwallEvent { return .paywallClose(paywallInfo: paywallInfo) } let paywallInfo: PaywallInfo + let surveyPresentationResult: SurveyPresentationResult + func getSuperwallParameters() async -> [String: Any] { - return await paywallInfo.eventParams() + var params: [String: Any] = [ + "survey_attached": paywallInfo.survey == nil ? false : true + ] + + if surveyPresentationResult != .noShow { + params["survey_presentation"] = surveyPresentationResult.rawValue + } + + let eventParams = await paywallInfo.eventParams() + params += eventParams + return params } var customParameters: [String: Any] { return paywallInfo.customParams() diff --git a/Sources/SuperwallKit/Config/Models/Survey.swift b/Sources/SuperwallKit/Config/Models/Survey.swift index 7821ef572..1027b726f 100644 --- a/Sources/SuperwallKit/Config/Models/Survey.swift +++ b/Sources/SuperwallKit/Config/Models/Survey.swift @@ -34,31 +34,30 @@ final public class Survey: NSObject, Decodable { /// response. public let includeOtherOption: Bool - /// Rolls dice to see if survey should present. - func shouldPresent( + /// Rolls dice to see if survey should present or is in holdout. + /// + /// - Returns: `true` if user is in holdout, false if survey should present. + func shouldAssignHoldout( isDebuggerLaunched: Bool, - storage: Storage + storage: Storage, + randomiser: (Range) -> Double = Double.random ) -> Bool { if isDebuggerLaunched { - return true + return false } // Return immediately if no chance to present. if presentationProbability == 0 { - return false + return true } // Choose random number to present the survey with // the probability of presentationProbability. - let randomNumber = Double.random(in: 0..<1) + let randomNumber = randomiser(0..<1) guard randomNumber < presentationProbability else { - return false - } - - if hasSeenSurvey(storage: storage) { - return false + return true } - return true + return false } /// Determines whether a survey with the same `assignmentKey` has been diff --git a/Sources/SuperwallKit/Models/Paywall/Paywall.swift b/Sources/SuperwallKit/Models/Paywall/Paywall.swift index 967912a3e..f22db89f7 100644 --- a/Sources/SuperwallKit/Models/Paywall/Paywall.swift +++ b/Sources/SuperwallKit/Models/Paywall/Paywall.swift @@ -50,7 +50,7 @@ struct Paywall: Decodable { let onDeviceCache: OnDeviceCaching /// A survey to potentially show on close of the paywall. - let survey: Survey? + var survey: Survey? /// The products associated with the paywall. var products: [Product] { diff --git a/Sources/SuperwallKit/Models/Triggers/TriggerResult.swift b/Sources/SuperwallKit/Models/Triggers/TriggerResult.swift index e34a2afc6..a7b4a1d70 100644 --- a/Sources/SuperwallKit/Models/Triggers/TriggerResult.swift +++ b/Sources/SuperwallKit/Models/Triggers/TriggerResult.swift @@ -36,6 +36,6 @@ public enum TriggerResult: Sendable, Equatable { /// /// If the error code is `101`, it means that no view controller could be found to present on. Otherwise a network failure may have occurred. /// - /// In these instances, consider fallng back to a native paywall. + /// In these instances, consider falling back to a native paywall. case error(NSError) } diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 8b1f4d893..995f40a9c 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -136,7 +136,10 @@ public class PaywallViewController: UIViewController, LoadingDelegate { /// The presenting view controller, saved for presenting surveys from when /// the view disappears. - var internalPresentingViewController: UIViewController? + private var internalPresentingViewController: UIViewController? + + /// Whether the survey was shown, not shown, or in a holdout. Defaults to not shown. + private var surveyPresentationResult: SurveyPresentationResult = .noShow private unowned let factory: TriggerSessionManagerFactory & TriggerFactory private unowned let storage: Storage @@ -229,7 +232,10 @@ public class PaywallViewController: UIViewController, LoadingDelegate { nonisolated private func trackClose() async { let triggerSessionManager = factory.getTriggerSessionManager() - let trackedEvent = await InternalSuperwallEvent.PaywallClose(paywallInfo: info) + let trackedEvent = await InternalSuperwallEvent.PaywallClose( + paywallInfo: info, + surveyPresentationResult: surveyPresentationResult + ) await Superwall.shared.track(trackedEvent) await triggerSessionManager.trackPaywallClose() } @@ -775,7 +781,8 @@ extension PaywallViewController { paywallInfo: info, storage: storage, factory: factory - ) { + ) { [weak self] result in + self?.surveyPresentationResult = result dismissView() } } diff --git a/Sources/SuperwallKit/Paywall/View Controller/SurveyManager.swift b/Sources/SuperwallKit/Paywall/View Controller/Survey/SurveyManager.swift similarity index 91% rename from Sources/SuperwallKit/Paywall/View Controller/SurveyManager.swift rename to Sources/SuperwallKit/Paywall/View Controller/Survey/SurveyManager.swift index 202a9f8fd..7e50f1385 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/SurveyManager.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Survey/SurveyManager.swift @@ -20,22 +20,26 @@ final class SurveyManager { paywallInfo: PaywallInfo, storage: Storage, factory: TriggerFactory, - completion: @escaping () -> Void + completion: @escaping (SurveyPresentationResult) -> Void ) { + guard let survey = survey else { + completion(.noShow) + return + } guard loadingState == .ready else { - completion() + completion(.noShow) return } guard paywallIsManuallyDeclined else { - completion() + completion(.noShow) return } - guard let survey = survey else { - completion() + if survey.hasSeenSurvey(storage: storage) { + completion(.noShow) return } - let shouldPresent = survey.shouldPresent( + let isHoldout = survey.shouldAssignHoldout( isDebuggerLaunched: isDebuggerLaunched, storage: storage ) @@ -45,13 +49,13 @@ final class SurveyManager { storage.save(survey.assignmentKey, forType: SurveyAssignmentKey.self) } - guard shouldPresent else { + if isHoldout { Logger.debug( logLevel: .info, scope: .paywallViewController, message: "The survey will not present." ) - completion() + completion(.holdout) return } @@ -153,12 +157,12 @@ final class SurveyManager { paywallViewController: PaywallViewController, isDebuggerLaunched: Bool, alertController: UIAlertController, - completion: @escaping () -> Void + completion: @escaping (SurveyPresentationResult) -> Void ) { alertController.dismiss(animated: true) { // Always complete without tracking if debugger launched. if isDebuggerLaunched { - completion() + completion(.show) } else { Task { let event = InternalSuperwallEvent.SurveyResponse( @@ -175,12 +179,12 @@ final class SurveyManager { ) await Superwall.shared.track(event) - // If we are going to show another paywall, we don't call the completion + // If we are going to implicitly show another paywall, we don't call the completion // block as this will call didDismiss, which is going to be called // implicitly anyway. if outcome == .dontTriggerPaywall { await MainActor.run { - completion() + completion(.show) } } } diff --git a/Sources/SuperwallKit/Paywall/View Controller/Survey/SurveyPresentationResult.swift b/Sources/SuperwallKit/Paywall/View Controller/Survey/SurveyPresentationResult.swift new file mode 100644 index 000000000..16b50aa9b --- /dev/null +++ b/Sources/SuperwallKit/Paywall/View Controller/Survey/SurveyPresentationResult.swift @@ -0,0 +1,14 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 15/08/2023. +// + +import Foundation + +enum SurveyPresentationResult: String { + case show + case holdout + case noShow +} diff --git a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift index dc19f3762..933a1c6de 100644 --- a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift +++ b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift @@ -579,9 +579,172 @@ final class TrackingTests: XCTestCase { XCTAssertEqual(result.parameters.eventParams["presented_by_event_name"] as? String, paywallInfo.presentedByEventWithName) } - func test_paywallClose() async { - let paywallInfo: PaywallInfo = .stub() - let result = await Superwall.shared.track(InternalSuperwallEvent.PaywallClose(paywallInfo: paywallInfo)) + func test_paywallClose_survey_show() async { + let paywall: Paywall = .stub() + .setting(\.survey, to: .stub()) + let paywallInfo = paywall.getInfo(fromEvent: .stub(), factory: DependencyContainer()) + let result = await Superwall.shared.track( + InternalSuperwallEvent.PaywallClose( + paywallInfo: paywallInfo, + surveyPresentationResult: .show + ) + ) + XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) + XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) + + XCTAssertEqual(result.parameters.eventParams["$paywall_id"] as! String, paywallInfo.databaseId) + XCTAssertEqual(result.parameters.eventParams["$paywalljs_version"] as? String, paywallInfo.paywalljsVersion) + XCTAssertEqual(result.parameters.eventParams["$paywall_identifier"] as! String, paywallInfo.identifier) + XCTAssertEqual(result.parameters.eventParams["$paywall_name"] as! String, paywallInfo.name) + XCTAssertEqual(result.parameters.eventParams["$paywall_url"] as! String, paywallInfo.url.absoluteString) + XCTAssertEqual(result.parameters.eventParams["$presented_by_event_name"] as? String, paywallInfo.presentedByEventWithName) + XCTAssertEqual(result.parameters.eventParams["$presented_by_event_id"] as? String, paywallInfo.presentedByEventWithId) + XCTAssertEqual(result.parameters.eventParams["$presented_by_event_timestamp"] as? String, paywallInfo.presentedByEventAt) + XCTAssertEqual(result.parameters.eventParams["$presented_by"] as! String, paywallInfo.presentedBy) + XCTAssertEqual(result.parameters.eventParams["$paywall_product_ids"] as? String, paywallInfo.productIds.joined(separator: ",")) + XCTAssertEqual(result.parameters.eventParams["$paywall_response_load_start_time"] as! String, paywallInfo.responseLoadStartTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_response_load_complete_time"] as! String, paywallInfo.responseLoadCompleteTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_response_load_duration"] as? TimeInterval, paywallInfo.responseLoadDuration) + XCTAssertEqual(result.parameters.eventParams["$paywall_webview_load_start_time"] as! String, paywallInfo.webViewLoadStartTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_webview_load_complete_time"] as! String, paywallInfo.webViewLoadCompleteTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_webview_load_duration"] as? TimeInterval, paywallInfo.webViewLoadDuration) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_start_time"] as! String, paywallInfo.productsLoadStartTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_complete_time"] as! String, paywallInfo.productsLoadCompleteTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_fail_time"] as! String, paywallInfo.productsLoadFailTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_duration"] as? TimeInterval, paywallInfo.productsLoadDuration) + XCTAssertEqual(result.parameters.eventParams["$is_free_trial_available"] as! Bool, paywallInfo.isFreeTrialAvailable) + XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywall_close") + XCTAssertTrue(result.parameters.eventParams["$survey_attached"] as! Bool) + XCTAssertEqual(result.parameters.eventParams["$survey_presentation"] as! String, "show") + + // Custom parameters + XCTAssertEqual(result.parameters.eventParams["event_name"] as! String, "paywall_close") + XCTAssertEqual(result.parameters.eventParams["paywall_id"] as! String, paywallInfo.databaseId) + XCTAssertEqual(result.parameters.eventParams["paywall_name"] as! String, paywallInfo.name) + XCTAssertEqual(result.parameters.eventParams["is_free_trial_available"] as! Bool, paywallInfo.isFreeTrialAvailable) + XCTAssertEqual(result.parameters.eventParams["feature_gating"] as! Int, 1) + XCTAssertEqual(result.parameters.eventParams["presented_by"] as! String, paywallInfo.presentedBy) + XCTAssertEqual(result.parameters.eventParams["paywall_product_ids"] as? String, paywallInfo.productIds.joined(separator: ",")) + XCTAssertNotNil(result.parameters.eventParams["primary_product_id"]) + XCTAssertNotNil(result.parameters.eventParams["secondary_product_id"]) + XCTAssertNotNil(result.parameters.eventParams["tertiary_product_id"]) + XCTAssertEqual(result.parameters.eventParams["presented_by_event_name"] as? String, paywallInfo.presentedByEventWithName) + } + + func test_paywallClose_survey_noShow() async { + let paywall: Paywall = .stub() + .setting(\.survey, to: .stub()) + let paywallInfo = paywall.getInfo(fromEvent: .stub(), factory: DependencyContainer()) + let result = await Superwall.shared.track( + InternalSuperwallEvent.PaywallClose( + paywallInfo: paywallInfo, + surveyPresentationResult: .noShow + ) + ) + XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) + XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) + + XCTAssertEqual(result.parameters.eventParams["$paywall_id"] as! String, paywallInfo.databaseId) + XCTAssertEqual(result.parameters.eventParams["$paywalljs_version"] as? String, paywallInfo.paywalljsVersion) + XCTAssertEqual(result.parameters.eventParams["$paywall_identifier"] as! String, paywallInfo.identifier) + XCTAssertEqual(result.parameters.eventParams["$paywall_name"] as! String, paywallInfo.name) + XCTAssertEqual(result.parameters.eventParams["$paywall_url"] as! String, paywallInfo.url.absoluteString) + XCTAssertEqual(result.parameters.eventParams["$presented_by_event_name"] as? String, paywallInfo.presentedByEventWithName) + XCTAssertEqual(result.parameters.eventParams["$presented_by_event_id"] as? String, paywallInfo.presentedByEventWithId) + XCTAssertEqual(result.parameters.eventParams["$presented_by_event_timestamp"] as? String, paywallInfo.presentedByEventAt) + XCTAssertEqual(result.parameters.eventParams["$presented_by"] as! String, paywallInfo.presentedBy) + XCTAssertEqual(result.parameters.eventParams["$paywall_product_ids"] as? String, paywallInfo.productIds.joined(separator: ",")) + XCTAssertEqual(result.parameters.eventParams["$paywall_response_load_start_time"] as! String, paywallInfo.responseLoadStartTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_response_load_complete_time"] as! String, paywallInfo.responseLoadCompleteTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_response_load_duration"] as? TimeInterval, paywallInfo.responseLoadDuration) + XCTAssertEqual(result.parameters.eventParams["$paywall_webview_load_start_time"] as! String, paywallInfo.webViewLoadStartTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_webview_load_complete_time"] as! String, paywallInfo.webViewLoadCompleteTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_webview_load_duration"] as? TimeInterval, paywallInfo.webViewLoadDuration) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_start_time"] as! String, paywallInfo.productsLoadStartTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_complete_time"] as! String, paywallInfo.productsLoadCompleteTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_fail_time"] as! String, paywallInfo.productsLoadFailTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_duration"] as? TimeInterval, paywallInfo.productsLoadDuration) + XCTAssertEqual(result.parameters.eventParams["$is_free_trial_available"] as! Bool, paywallInfo.isFreeTrialAvailable) + XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywall_close") + XCTAssertTrue(result.parameters.eventParams["$survey_attached"] as! Bool) + XCTAssertNil(result.parameters.eventParams["$survey_presentation"]) + + // Custom parameters + XCTAssertEqual(result.parameters.eventParams["event_name"] as! String, "paywall_close") + XCTAssertEqual(result.parameters.eventParams["paywall_id"] as! String, paywallInfo.databaseId) + XCTAssertEqual(result.parameters.eventParams["paywall_name"] as! String, paywallInfo.name) + XCTAssertEqual(result.parameters.eventParams["is_free_trial_available"] as! Bool, paywallInfo.isFreeTrialAvailable) + XCTAssertEqual(result.parameters.eventParams["feature_gating"] as! Int, 1) + XCTAssertEqual(result.parameters.eventParams["presented_by"] as! String, paywallInfo.presentedBy) + XCTAssertEqual(result.parameters.eventParams["paywall_product_ids"] as? String, paywallInfo.productIds.joined(separator: ",")) + XCTAssertNotNil(result.parameters.eventParams["primary_product_id"]) + XCTAssertNotNil(result.parameters.eventParams["secondary_product_id"]) + XCTAssertNotNil(result.parameters.eventParams["tertiary_product_id"]) + XCTAssertEqual(result.parameters.eventParams["presented_by_event_name"] as? String, paywallInfo.presentedByEventWithName) + } + + func test_paywallClose_survey_holdout() async { + let paywall: Paywall = .stub() + .setting(\.survey, to: .stub()) + let paywallInfo = paywall.getInfo(fromEvent: .stub(), factory: DependencyContainer()) + let result = await Superwall.shared.track( + InternalSuperwallEvent.PaywallClose( + paywallInfo: paywallInfo, + surveyPresentationResult: .holdout + ) + ) + XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) + XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) + + XCTAssertEqual(result.parameters.eventParams["$paywall_id"] as! String, paywallInfo.databaseId) + XCTAssertEqual(result.parameters.eventParams["$paywalljs_version"] as? String, paywallInfo.paywalljsVersion) + XCTAssertEqual(result.parameters.eventParams["$paywall_identifier"] as! String, paywallInfo.identifier) + XCTAssertEqual(result.parameters.eventParams["$paywall_name"] as! String, paywallInfo.name) + XCTAssertEqual(result.parameters.eventParams["$paywall_url"] as! String, paywallInfo.url.absoluteString) + XCTAssertEqual(result.parameters.eventParams["$presented_by_event_name"] as? String, paywallInfo.presentedByEventWithName) + XCTAssertEqual(result.parameters.eventParams["$presented_by_event_id"] as? String, paywallInfo.presentedByEventWithId) + XCTAssertEqual(result.parameters.eventParams["$presented_by_event_timestamp"] as? String, paywallInfo.presentedByEventAt) + XCTAssertEqual(result.parameters.eventParams["$presented_by"] as! String, paywallInfo.presentedBy) + XCTAssertEqual(result.parameters.eventParams["$paywall_product_ids"] as? String, paywallInfo.productIds.joined(separator: ",")) + XCTAssertEqual(result.parameters.eventParams["$paywall_response_load_start_time"] as! String, paywallInfo.responseLoadStartTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_response_load_complete_time"] as! String, paywallInfo.responseLoadCompleteTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_response_load_duration"] as? TimeInterval, paywallInfo.responseLoadDuration) + XCTAssertEqual(result.parameters.eventParams["$paywall_webview_load_start_time"] as! String, paywallInfo.webViewLoadStartTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_webview_load_complete_time"] as! String, paywallInfo.webViewLoadCompleteTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_webview_load_duration"] as? TimeInterval, paywallInfo.webViewLoadDuration) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_start_time"] as! String, paywallInfo.productsLoadStartTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_complete_time"] as! String, paywallInfo.productsLoadCompleteTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_fail_time"] as! String, paywallInfo.productsLoadFailTime!) + XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_duration"] as? TimeInterval, paywallInfo.productsLoadDuration) + XCTAssertEqual(result.parameters.eventParams["$is_free_trial_available"] as! Bool, paywallInfo.isFreeTrialAvailable) + XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywall_close") + XCTAssertTrue(result.parameters.eventParams["$survey_attached"] as! Bool) + XCTAssertEqual(result.parameters.eventParams["$survey_presentation"] as! String, "holdout") + + // Custom parameters + XCTAssertEqual(result.parameters.eventParams["event_name"] as! String, "paywall_close") + XCTAssertEqual(result.parameters.eventParams["paywall_id"] as! String, paywallInfo.databaseId) + XCTAssertEqual(result.parameters.eventParams["paywall_name"] as! String, paywallInfo.name) + XCTAssertEqual(result.parameters.eventParams["is_free_trial_available"] as! Bool, paywallInfo.isFreeTrialAvailable) + XCTAssertEqual(result.parameters.eventParams["feature_gating"] as! Int, 1) + XCTAssertEqual(result.parameters.eventParams["presented_by"] as! String, paywallInfo.presentedBy) + XCTAssertEqual(result.parameters.eventParams["paywall_product_ids"] as? String, paywallInfo.productIds.joined(separator: ",")) + XCTAssertNotNil(result.parameters.eventParams["primary_product_id"]) + XCTAssertNotNil(result.parameters.eventParams["secondary_product_id"]) + XCTAssertNotNil(result.parameters.eventParams["tertiary_product_id"]) + XCTAssertEqual(result.parameters.eventParams["presented_by_event_name"] as? String, paywallInfo.presentedByEventWithName) + } + + func test_paywallClose_noSurvey() async { + let paywall: Paywall = .stub() + .setting(\.survey, to: nil) + let paywallInfo = paywall.getInfo(fromEvent: .stub(), factory: DependencyContainer()) + let result = await Superwall.shared.track( + InternalSuperwallEvent.PaywallClose( + paywallInfo: paywallInfo, + surveyPresentationResult: .noShow + ) + ) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) @@ -607,6 +770,8 @@ final class TrackingTests: XCTestCase { XCTAssertEqual(result.parameters.eventParams["$paywall_products_load_duration"] as? TimeInterval, paywallInfo.productsLoadDuration) XCTAssertEqual(result.parameters.eventParams["$is_free_trial_available"] as! Bool, paywallInfo.isFreeTrialAvailable) XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywall_close") + XCTAssertFalse(result.parameters.eventParams["$survey_attached"] as! Bool) + XCTAssertNil(result.parameters.eventParams["$survey_presentation"]) // Custom parameters XCTAssertEqual(result.parameters.eventParams["event_name"] as! String, "paywall_close") diff --git a/Tests/SuperwallKitTests/Config/Models/SurveyTests.swift b/Tests/SuperwallKitTests/Config/Models/SurveyTests.swift new file mode 100644 index 000000000..8c0965c47 --- /dev/null +++ b/Tests/SuperwallKitTests/Config/Models/SurveyTests.swift @@ -0,0 +1,115 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 16/08/2023. +// + +@testable import SuperwallKit +import XCTest + +@available(iOS 14.0, *) +final class SurveyTests: XCTestCase { + func test_shouldAssignHoldout_debuggerLaunched() { + let survey = Survey.stub() + let isHoldout = survey.shouldAssignHoldout( + isDebuggerLaunched: true, + storage: StorageMock() + ) + XCTAssertFalse(isHoldout) + } + + func test_shouldAssignHoldout_presentationProbabilityZero() { + let survey = Survey( + id: UUID().uuidString, + assignmentKey: "abc", + title: "test", + message: "test", + options: [.stub()], + presentationProbability: 0, + includeOtherOption: true + ) + let isHoldout = survey.shouldAssignHoldout( + isDebuggerLaunched: false, + storage: StorageMock() + ) + XCTAssertTrue(isHoldout) + } + + func test_shouldAssignHoldout_presentationProbabilityOne() { + let survey = Survey( + id: UUID().uuidString, + assignmentKey: "abc", + title: "test", + message: "test", + options: [.stub()], + presentationProbability: 1, + includeOtherOption: true + ) + let isHoldout = survey.shouldAssignHoldout( + isDebuggerLaunched: false, + storage: StorageMock() + ) + XCTAssertFalse(isHoldout) + } + + func test_shouldAssignHoldout_presentationProbabilityPointFive() { + let survey = Survey( + id: UUID().uuidString, + assignmentKey: "abc", + title: "test", + message: "test", + options: [.stub()], + presentationProbability: 0.4, + includeOtherOption: true + ) + func random(in: Range) -> Double { + return 0.5 + } + let isHoldout = survey.shouldAssignHoldout( + isDebuggerLaunched: false, + storage: StorageMock(), + randomiser: random(in:) + ) + XCTAssertTrue(isHoldout) + } + + func test_hasSeenSurvey_noAssignmentKey() { + let survey: Survey = .stub() + let storage = StorageMock() + let hasSeen = survey.hasSeenSurvey(storage: storage) + XCTAssertFalse(hasSeen) + } + + func test_hasSeenSurvey_sameAssignmentKey() { + let survey = Survey( + id: UUID().uuidString, + assignmentKey: "abc", + title: "test", + message: "test", + options: [.stub()], + presentationProbability: 0.4, + includeOtherOption: true + ) + let existingAssignmentKey = "abc" + let storage = StorageMock(internalSurveyAssignmentKey: existingAssignmentKey) + let hasSeen = survey.hasSeenSurvey(storage: storage) + XCTAssertTrue(hasSeen) + } + + func test_hasSeenSurvey_diffAssignmentKey() { + let survey = Survey( + id: UUID().uuidString, + assignmentKey: "cde", + title: "test", + message: "test", + options: [.stub()], + presentationProbability: 0.4, + includeOtherOption: true + ) + let existingAssignmentKey = "abc" + let storage = StorageMock(internalSurveyAssignmentKey: existingAssignmentKey) + let hasSeen = survey.hasSeenSurvey(storage: storage) + XCTAssertFalse(hasSeen) + } +} diff --git a/Tests/SuperwallKitTests/Paywall/View Controller/SurveyManagerTests.swift b/Tests/SuperwallKitTests/Paywall/View Controller/SurveyManagerTests.swift index f23bebb3b..ae04f6545 100644 --- a/Tests/SuperwallKitTests/Paywall/View Controller/SurveyManagerTests.swift +++ b/Tests/SuperwallKitTests/Paywall/View Controller/SurveyManagerTests.swift @@ -44,7 +44,8 @@ final class SurveyManagerTests: XCTestCase { paywallInfo: .stub(), storage: StorageMock(), factory: dependencyContainer, - completion: { + completion: { result in + XCTAssertEqual(result, .noShow) expectation.fulfill() } ) @@ -83,7 +84,8 @@ final class SurveyManagerTests: XCTestCase { paywallInfo: .stub(), storage: StorageMock(), factory: dependencyContainer, - completion: { + completion: { result in + XCTAssertEqual(result, .noShow) expectation.fulfill() } ) @@ -122,7 +124,8 @@ final class SurveyManagerTests: XCTestCase { paywallInfo: .stub(), storage: StorageMock(), factory: dependencyContainer, - completion: { + completion: { result in + XCTAssertEqual(result, .noShow) expectation.fulfill() } ) @@ -161,7 +164,8 @@ final class SurveyManagerTests: XCTestCase { paywallInfo: .stub(), storage: StorageMock(), factory: dependencyContainer, - completion: { + completion: { result in + XCTAssertEqual(result, .noShow) expectation.fulfill() } ) @@ -200,7 +204,8 @@ final class SurveyManagerTests: XCTestCase { paywallInfo: .stub(), storage: StorageMock(), factory: dependencyContainer, - completion: { + completion: { result in + XCTAssertEqual(result, .noShow) expectation.fulfill() } ) @@ -239,7 +244,8 @@ final class SurveyManagerTests: XCTestCase { paywallInfo: .stub(), storage: StorageMock(), factory: dependencyContainer, - completion: { + completion: { result in + XCTAssertEqual(result, .noShow) expectation.fulfill() } ) @@ -247,11 +253,9 @@ final class SurveyManagerTests: XCTestCase { } func test_presentSurveyIfAvailable_sameAssignmentKey() { - let storageMock = StorageMock() - + let storageMock = StorageMock(internalSurveyAssignmentKey: "1") let survey = Survey.stub() .setting(\.assignmentKey, to: "1") - storageMock.save("1", forType: SurveyAssignmentKey.self) let expectation = expectation(description: "called completion block") let dependencyContainer = DependencyContainer() @@ -284,12 +288,13 @@ final class SurveyManagerTests: XCTestCase { paywallInfo: .stub(), storage: storageMock, factory: dependencyContainer, - completion: { + completion: { result in + XCTAssertEqual(result, .noShow) expectation.fulfill() } ) - wait(for: [expectation]) - XCTAssertTrue(storageMock.didSave) + wait(for: [expectation], timeout: 0.2) + XCTAssertFalse(storageMock.didSave) } func test_presentSurveyIfAvailable_zeroPresentationProbability() { @@ -330,7 +335,8 @@ final class SurveyManagerTests: XCTestCase { paywallInfo: .stub(), storage: storageMock, factory: dependencyContainer, - completion: { + completion: { result in + XCTAssertEqual(result, .holdout) expectation.fulfill() } ) @@ -375,7 +381,8 @@ final class SurveyManagerTests: XCTestCase { paywallInfo: .stub(), storage: storageMock, factory: dependencyContainer, - completion: { + completion: { result in + XCTAssertEqual(result, .show) expectation.fulfill() } ) @@ -422,7 +429,8 @@ final class SurveyManagerTests: XCTestCase { paywallInfo: .stub(), storage: storageMock, factory: dependencyContainer, - completion: { + completion: { result in + XCTAssertEqual(result, .show) expectation.fulfill() } ) diff --git a/Tests/SuperwallKitTests/Storage/StorageMock.swift b/Tests/SuperwallKitTests/Storage/StorageMock.swift index 0c2064ed9..8a06c51a8 100644 --- a/Tests/SuperwallKitTests/Storage/StorageMock.swift +++ b/Tests/SuperwallKitTests/Storage/StorageMock.swift @@ -14,6 +14,7 @@ final class StorageMock: Storage { var internalCachedTriggerSessions: [TriggerSession] var internalCachedTransactions: [StoreTransaction] var internalConfirmedAssignments: [Experiment.ID: Experiment.Variant] + var internalSurveyAssignmentKey: String? var didClearCachedSessionEvents = false var didSave = false @@ -34,6 +35,7 @@ final class StorageMock: Storage { init( internalCachedTriggerSessions: [TriggerSession] = [], internalCachedTransactions: [StoreTransaction] = [], + internalSurveyAssignmentKey: String? = nil, coreDataManager: CoreDataManagerFakeDataMock = CoreDataManagerFakeDataMock(), confirmedAssignments: [Experiment.ID : Experiment.Variant] = [:], cache: Cache = Cache() @@ -41,6 +43,7 @@ final class StorageMock: Storage { self.internalCachedTriggerSessions = internalCachedTriggerSessions self.internalCachedTransactions = internalCachedTransactions self.internalConfirmedAssignments = confirmedAssignments + self.internalSurveyAssignmentKey = internalSurveyAssignmentKey super.init(factory: DeviceInfoFactoryMock(), cache: cache, coreDataManager: coreDataManager) } @@ -50,6 +53,8 @@ final class StorageMock: Storage { return internalCachedTriggerSessions as? Key.Value } else if keyType == Transactions.self { return internalCachedTransactions as? Key.Value + } else if keyType == SurveyAssignmentKey.self { + return internalSurveyAssignmentKey as? Key.Value } return super.get(keyType) } @@ -59,6 +64,8 @@ final class StorageMock: Storage { return internalCachedTriggerSessions as? Key.Value } else if keyType == Transactions.self { return internalCachedTransactions as? Key.Value + } else if keyType == SurveyAssignmentKey.self { + return internalSurveyAssignmentKey as? Key.Value } return super.get(keyType) } From 6c425aca44852cbc8fa2a273f75ab07f312bd51e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Wed, 16 Aug 2023 16:49:03 +0800 Subject: [PATCH 08/12] Moved the saving of the rule occurrence to on paywall viewDidAppear - Fixes rare issue when using limits on a campaign rule. If a paywall encountered an error preventing it from being presented, it may still have been counted as having been presented. This would then have affected future paywall presentation requests underneath the same rule. --- CHANGELOG.md | 4 ++ .../Models/Triggers/TriggerRule.swift | 5 ++ .../Get Paywall/InternalGetPaywall.swift | 14 +++--- .../InternalGetPresentationResult.swift | 9 ++-- .../InternalPresentation.swift | 14 +++--- .../Operators/ConfirmHoldoutAssignment.swift | 6 +-- .../Operators/EvaluateRules.swift | 19 ++----- .../Operators/GetExperiment.swift | 12 ++--- .../Operators/GetPaywallVC.swift | 1 - .../Operators/GetPresenter.swift | 4 +- .../Operators/PresentPaywall.swift | 7 +++ .../ExpressionEvaluator.swift | 32 ++++++++---- .../ExpressionEvaluatorParams.swift | 2 +- .../Presentation/Rule Logic/RuleLogic.swift | 50 +++++++++++-------- .../PaywallViewController.swift | 16 +++++- .../ExpressionEvaluatorLogicTests.swift | 20 ++++---- .../ExpressionEvaluatorTests.swift | 43 ++++++++++++---- ...CheckPaywallPresentableOperatorTests.swift | 6 +-- .../CheckUserSubscriptionOperatorTests.swift | 6 +-- .../ConfirmHoldoutAssignmentTests.swift | 16 +++--- .../Operators/GetPaywallVcOperatorTests.swift | 3 -- .../HandleTriggerResultOperatorTests.swift | 20 ++++---- .../PresentPaywallOperatorTests.swift | 2 + .../PaywallViewControllerMock.swift | 3 +- 24 files changed, 186 insertions(+), 128 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9d71f5c8..b84779745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup - Adds logic to enhance debugging by sending a stringified version of all the device/user/event parameters used to evaluate rules within the `paywallPresentationRequest` event. This is behind a feature flag. - Adds logic to keep the user's generated `seed` value consistent when `Superwall.identify` is called. This is behind a feature flag. +### Fixes + +- Fixes rare issue when using limits on a campaign rule. If a paywall encountered an error preventing it from being presented, it may still have been counted as having been presented. This would then have affected future paywall presentation requests underneath the same rule. + ## 3.3.0 ### Enhancements diff --git a/Sources/SuperwallKit/Models/Triggers/TriggerRule.swift b/Sources/SuperwallKit/Models/Triggers/TriggerRule.swift index 941ac841c..6d6ec543a 100644 --- a/Sources/SuperwallKit/Models/Triggers/TriggerRule.swift +++ b/Sources/SuperwallKit/Models/Triggers/TriggerRule.swift @@ -7,6 +7,11 @@ import Foundation +struct TriggerRuleOutcome { + let rule: TriggerRule + let unsavedOccurrence: TriggerRuleOccurrence? +} + struct TriggerRule: Decodable, Hashable { var experiment: RawExperiment var expression: String? diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywall.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywall.swift index 1e99cc71a..a81fc74ad 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywall.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywall.swift @@ -28,13 +28,13 @@ extension Superwall { message: "Called Superwall.shared.getPaywall" ) - let rulesOutput = try await evaluateRules(from: request) + let rulesOutcome = try await evaluateRules(from: request) - confirmHoldoutAssignment(rulesOutput: rulesOutput) + confirmHoldoutAssignment(from: rulesOutcome) let experiment = try await getExperiment( request: request, - rulesOutput: rulesOutput, + rulesOutcome: rulesOutcome, debugInfo: debugInfo, paywallStatePublisher: publisher ) @@ -42,7 +42,6 @@ extension Superwall { let paywallViewController = try await getPaywallViewController( request: request, experiment: experiment, - rulesOutput: rulesOutput, debugInfo: debugInfo, paywallStatePublisher: publisher ) @@ -50,18 +49,19 @@ extension Superwall { try await checkSubscriptionStatus( request: request, paywall: paywallViewController.paywall, - triggerResult: rulesOutput.triggerResult, + triggerResult: rulesOutcome.triggerResult, paywallStatePublisher: publisher ) confirmPaywallAssignment( - rulesOutput.confirmableAssignment, + rulesOutcome.confirmableAssignment, isDebuggerLaunched: request.flags.isDebuggerLaunched ) await paywallViewController.set( request: request, - paywallStatePublisher: publisher + paywallStatePublisher: publisher, + unsavedOccurrence: rulesOutcome.unsavedOccurrence ) return paywallViewController } catch { diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/InternalGetPresentationResult.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/InternalGetPresentationResult.swift index 22a1b15ab..290e3ddd5 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/InternalGetPresentationResult.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/InternalGetPresentationResult.swift @@ -17,27 +17,26 @@ extension Superwall { request: request, message: "Called Superwall.shared.getPresentationResult" ) - let rulesOutput = try await evaluateRules(from: request) + let rulesOutcome = try await evaluateRules(from: request) let experiment = try await getExperiment( request: request, - rulesOutput: rulesOutput + rulesOutcome: rulesOutcome ) let paywallViewController = try await getPaywallViewController( request: request, experiment: experiment, - rulesOutput: rulesOutput, debugInfo: debugInfo ) try await getPresenter( for: paywallViewController, - rulesOutput: rulesOutput, + rulesOutcome: rulesOutcome, request: request, debugInfo: debugInfo ) - let presentationResult = GetPresentationResultLogic.convertTriggerResult(rulesOutput.triggerResult) + let presentationResult = GetPresentationResultLogic.convertTriggerResult(rulesOutcome.triggerResult) return presentationResult } catch let error as PresentationPipelineError { return handle(error, requestType: request.flags.type) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift index 0efd5547e..2a0599b7b 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift @@ -41,33 +41,32 @@ extension Superwall { paywallStatePublisher: publisher ) - let rulesOutput = try await evaluateRules(from: request) + let rulesOutcome = try await evaluateRules(from: request) try await checkUserSubscription( request: request, - triggerResult: rulesOutput.triggerResult, + triggerResult: rulesOutcome.triggerResult, paywallStatePublisher: publisher ) - confirmHoldoutAssignment(rulesOutput: rulesOutput) + confirmHoldoutAssignment(from: rulesOutcome) let experiment = try await getExperiment( request: request, - rulesOutput: rulesOutput, + rulesOutcome: rulesOutcome, debugInfo: debugInfo, paywallStatePublisher: publisher ) let paywallViewController = try await getPaywallViewController( request: request, experiment: experiment, - rulesOutput: rulesOutput, debugInfo: debugInfo, paywallStatePublisher: publisher ) guard let presenter = try await getPresenter( for: paywallViewController, - rulesOutput: rulesOutput, + rulesOutcome: rulesOutcome, request: request, debugInfo: debugInfo, paywallStatePublisher: publisher @@ -76,13 +75,14 @@ extension Superwall { } confirmPaywallAssignment( - rulesOutput.confirmableAssignment, + rulesOutcome.confirmableAssignment, isDebuggerLaunched: request.flags.isDebuggerLaunched ) try await presentPaywallViewController( paywallViewController, on: presenter, + unsavedOccurrence: rulesOutcome.unsavedOccurrence, debugInfo: debugInfo, request: request, paywallStatePublisher: publisher diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift index 3545ba0a0..52ffc145c 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift @@ -17,14 +17,14 @@ extension Superwall { /// - rulesOutput: The output from evaluating rules. /// - dependencyContainer: Used for testing only. func confirmHoldoutAssignment( - rulesOutput: EvaluateRulesOutput, + from rulesOutcome: RuleEvaluationOutcome, dependencyContainer: DependencyContainer? = nil ) { let dependencyContainer = dependencyContainer ?? self.dependencyContainer - guard case .holdout = rulesOutput.triggerResult else { + guard case .holdout = rulesOutcome.triggerResult else { return } - if let confirmableAssignment = rulesOutput.confirmableAssignment { + if let confirmableAssignment = rulesOutcome.confirmableAssignment { dependencyContainer.configManager.confirmAssignment(confirmableAssignment) } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/EvaluateRules.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/EvaluateRules.swift index 10535c988..426337c22 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/EvaluateRules.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/EvaluateRules.swift @@ -7,11 +7,6 @@ import Foundation -struct EvaluateRulesOutput { - let triggerResult: TriggerResult - var confirmableAssignment: ConfirmableAssignment? -} - extension Superwall { /// Evaluates the rules from the campaign that the event belongs to /// @@ -19,24 +14,18 @@ extension Superwall { /// - Returns: An `EvaluateRulesOutput` object containing the trigger result and confirmable assignment. func evaluateRules( from request: PresentationRequest - ) async throws -> EvaluateRulesOutput { + ) async throws -> RuleEvaluationOutcome { if let eventData = request.presentationInfo.eventData { - let assignmentLogic = RuleLogic( + let ruleLogic = RuleLogic( configManager: dependencyContainer.configManager, storage: dependencyContainer.storage, factory: dependencyContainer ) - let eventOutcome = await assignmentLogic.evaluateRules( + return await ruleLogic.evaluateRules( forEvent: eventData, triggers: dependencyContainer.configManager.triggersByEventName, isPreemptive: request.flags.type == .getPresentationResult ) - let confirmableAssignment = eventOutcome.confirmableAssignment - - return EvaluateRulesOutput( - triggerResult: eventOutcome.triggerResult, - confirmableAssignment: confirmableAssignment - ) } else { // Called if the debugger is shown. guard let paywallId = request.presentationInfo.identifier else { @@ -44,7 +33,7 @@ extension Superwall { // to force unwrapping. throw PresentationPipelineError.noPaywallViewController } - return EvaluateRulesOutput( + return RuleEvaluationOutcome( triggerResult: .paywall(.presentById(paywallId)) ) } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetExperiment.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetExperiment.swift index 07f96c69c..feffccf44 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetExperiment.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetExperiment.swift @@ -21,21 +21,21 @@ extension Superwall { /// - Returns: A struct that contains info for the next operation. func getExperiment( request: PresentationRequest, - rulesOutput: EvaluateRulesOutput, + rulesOutcome: RuleEvaluationOutcome, debugInfo: [String: Any]? = nil, paywallStatePublisher: PassthroughSubject? = nil ) async throws -> Experiment { let errorType: PresentationPipelineError - switch rulesOutput.triggerResult { + switch rulesOutcome.triggerResult { case .paywall(let experiment): return experiment case .holdout(let experiment): - await activateSession(request: request, rulesOutput: rulesOutput) + await activateSession(request: request, rulesOutcome: rulesOutcome) errorType = .holdout(experiment) paywallStatePublisher?.send(.skipped(.holdout(experiment))) case .noRuleMatch: - await activateSession(request: request, rulesOutput: rulesOutput) + await activateSession(request: request, rulesOutcome: rulesOutcome) errorType = .noRuleMatch paywallStatePublisher?.send(.skipped(.noRuleMatch)) case .eventNotFound: @@ -62,7 +62,7 @@ extension Superwall { private func activateSession( request: PresentationRequest, - rulesOutput: EvaluateRulesOutput + rulesOutcome: RuleEvaluationOutcome ) async { if request.flags.type == .getImplicitPresentationResult || request.flags.type == .getPresentationResult { @@ -72,7 +72,7 @@ extension Superwall { await sessionEventsManager?.triggerSession.activateSession( for: request.presentationInfo, on: request.presenter, - triggerResult: rulesOutput.triggerResult + triggerResult: rulesOutcome.triggerResult ) } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVC.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVC.swift index 333f0598d..8e0bd0f07 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVC.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVC.swift @@ -25,7 +25,6 @@ extension Superwall { func getPaywallViewController( request: PresentationRequest, experiment: Experiment?, - rulesOutput: EvaluateRulesOutput, debugInfo: [String: Any], paywallStatePublisher: PassthroughSubject? = nil, dependencyContainer: DependencyContainer? = nil diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPresenter.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPresenter.swift index 7a3260cf1..a98ba108f 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPresenter.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPresenter.swift @@ -31,7 +31,7 @@ extension Superwall { @discardableResult func getPresenter( for paywallViewController: PaywallViewController, - rulesOutput: EvaluateRulesOutput, + rulesOutcome: RuleEvaluationOutcome, request: PresentationRequest, debugInfo: [String: Any], paywallStatePublisher: PassthroughSubject? = nil @@ -90,7 +90,7 @@ extension Superwall { for: request.presentationInfo, on: request.presenter, paywall: paywallViewController.paywall, - triggerResult: rulesOutput.triggerResult + triggerResult: rulesOutcome.triggerResult ) return presenter diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywall.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywall.swift index dc4fa68a6..452e2bb87 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywall.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywall.swift @@ -14,12 +14,18 @@ extension Superwall { /// /// - Parameters: /// - paywallStatePublisher: A `PassthroughSubject` that gets sent ``PaywallState`` objects. + /// - presenter: The view controller to present that paywall on. + /// - unsavedOccurrence: The trigger rule occurrence to save, if available. + /// - debugInfo: Information to help with debugging. + /// - request: The request to present the paywall. + /// - paywallStatePublisher: A `PassthroughSubject` that gets sent ``PaywallState`` objects. /// /// - Returns: A publisher that contains info for the next pipeline operator. @MainActor func presentPaywallViewController( _ paywallViewController: PaywallViewController, on presenter: UIViewController, + unsavedOccurrence: TriggerRuleOccurrence?, debugInfo: [String: Any], request: PresentationRequest, paywallStatePublisher: PassthroughSubject @@ -42,6 +48,7 @@ extension Superwall { paywallViewController.present( on: presenter, request: request, + unsavedOccurrence: unsavedOccurrence, presentationStyleOverride: request.paywallOverrides?.presentationStyle, paywallStatePublisher: paywallStatePublisher ) { isPresented in diff --git a/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluator.swift b/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluator.swift index 8de410d01..883e16f66 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluator.swift @@ -12,6 +12,11 @@ struct ExpressionEvaluator { private let storage: Storage private unowned let factory: RuleAttributesFactory + struct TriggerFireOutcome { + let shouldFire: Bool + var unsavedOccurrence: TriggerRuleOccurrence? + } + init( storage: Storage, factory: RuleAttributesFactory @@ -24,7 +29,7 @@ struct ExpressionEvaluator { fromRule rule: TriggerRule, eventData: EventData, isPreemptive: Bool - ) async -> Bool { + ) async -> TriggerFireOutcome { // Expression matches all if rule.expressionJs == nil && rule.expression == nil { let shouldFire = await shouldFire( @@ -36,7 +41,7 @@ struct ExpressionEvaluator { } guard let jsCtx = JSContext() else { - return false + return TriggerFireOutcome(shouldFire: false) } jsCtx.exceptionHandler = { (_, value: JSValue?) in guard let value = value else { @@ -63,12 +68,12 @@ struct ExpressionEvaluator { forRule: rule, withEventData: eventData ) else { - return false + return TriggerFireOutcome(shouldFire: false) } let result = jsCtx.evaluateScript(script + "\n " + postfix) if result?.isString == nil { - return false + return TriggerFireOutcome(shouldFire: false) } let isMatched = result?.toString() == "true" @@ -93,7 +98,7 @@ struct ExpressionEvaluator { let jsonAttributes = JSON(attributes) if let expressionJs = rule.expressionJs { - if let base64Params = JavascriptExpressionEvaluatorParams( + if let base64Params = JsExpressionEvaluatorParams( expressionJs: expressionJs, values: jsonAttributes ).toBase64Input() { @@ -118,7 +123,7 @@ struct ExpressionEvaluator { forOccurrence occurrence: TriggerRuleOccurrence?, ruleMatched: Bool, isPreemptive: Bool - ) async -> Bool { + ) async -> TriggerFireOutcome { if ruleMatched { guard let occurrence = occurrence else { Logger.debug( @@ -126,8 +131,10 @@ struct ExpressionEvaluator { scope: .paywallPresentation, message: "No occurrence parameter found for trigger rule." ) - return true + + return TriggerFireOutcome(shouldFire: true) } + let count = await storage .coreDataManager .countTriggerRuleOccurrences( @@ -135,14 +142,19 @@ struct ExpressionEvaluator { ) + 1 let shouldFire = count <= occurrence.maxCount + var unsavedOccurrence: TriggerRuleOccurrence? + if shouldFire, !isPreemptive { - storage.coreDataManager.save(triggerRuleOccurrence: occurrence) + unsavedOccurrence = occurrence } - return shouldFire + return TriggerFireOutcome( + shouldFire: shouldFire, + unsavedOccurrence: unsavedOccurrence + ) } - return false + return TriggerFireOutcome(shouldFire: false) } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluatorParams.swift b/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluatorParams.swift index 53262bdf5..fd8ff0432 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluatorParams.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/Expression Evaluator/ExpressionEvaluatorParams.swift @@ -20,7 +20,7 @@ struct LiquidExpressionEvaluatorParams: Codable { } } -struct JavascriptExpressionEvaluatorParams: Codable { +struct JsExpressionEvaluatorParams: Codable { var expressionJs: String var values: JSON diff --git a/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/RuleLogic.swift b/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/RuleLogic.swift index edafb1f42..e0e7062d1 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/RuleLogic.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/RuleLogic.swift @@ -12,16 +12,17 @@ struct ConfirmableAssignment: Equatable { let variant: Experiment.Variant } +struct RuleEvaluationOutcome { + var confirmableAssignment: ConfirmableAssignment? + var unsavedOccurrence: TriggerRuleOccurrence? + var triggerResult: TriggerResult +} + struct RuleLogic { unowned let configManager: ConfigManager unowned let storage: Storage unowned let factory: RuleAttributesFactory - struct Outcome { - var confirmableAssignment: ConfirmableAssignment? - var triggerResult: TriggerResult - } - /// Determines the outcome of an event based on given triggers. It also determines /// whether there is an assignment to confirm based on the rule. /// @@ -44,22 +45,22 @@ struct RuleLogic { forEvent event: EventData, triggers: [String: Trigger], isPreemptive: Bool - ) async -> Outcome { + ) async -> RuleEvaluationOutcome { guard let trigger = triggers[event.name] else { - return Outcome(triggerResult: .eventNotFound) + return RuleEvaluationOutcome(triggerResult: .eventNotFound) } - guard let rule = await findMatchingRule( + guard let ruleOutcome = await findMatchingRule( for: event, withTrigger: trigger, isPreemptive: isPreemptive ) else { - return Outcome(triggerResult: .noRuleMatch) + return RuleEvaluationOutcome(triggerResult: .noRuleMatch) } let variant: Experiment.Variant var confirmableAssignment: ConfirmableAssignment? - + let rule = ruleOutcome.rule // For a matching rule there will be an unconfirmed (in-memory) or confirmed (on disk) variant assignment. // First check the disk, otherwise check memory. let confirmedAssignments = storage.getConfirmedAssignments() @@ -85,13 +86,14 @@ struct RuleLogic { code: 404, userInfo: userInfo ) - return Outcome(triggerResult: .error(error)) + return RuleEvaluationOutcome(triggerResult: .error(error)) } switch variant.type { case .holdout: - return Outcome( + return RuleEvaluationOutcome( confirmableAssignment: confirmableAssignment, + unsavedOccurrence: ruleOutcome.unsavedOccurrence, triggerResult: .holdout( Experiment( id: rule.experiment.id, @@ -101,8 +103,9 @@ struct RuleLogic { ) ) case .treatment: - return Outcome( + return RuleEvaluationOutcome( confirmableAssignment: confirmableAssignment, + unsavedOccurrence: ruleOutcome.unsavedOccurrence, triggerResult: .paywall( Experiment( id: rule.experiment.id, @@ -118,17 +121,24 @@ struct RuleLogic { for event: EventData, withTrigger trigger: Trigger, isPreemptive: Bool - ) async -> TriggerRule? { + ) async -> TriggerRuleOutcome? { let expressionEvaluator = ExpressionEvaluator( storage: storage, factory: factory ) - for rule in trigger.rules where await expressionEvaluator.evaluateExpression( - fromRule: rule, - eventData: event, - isPreemptive: isPreemptive - ) { - return rule + + for rule in trigger.rules { + let outcome = await expressionEvaluator.evaluateExpression( + fromRule: rule, + eventData: event, + isPreemptive: isPreemptive + ) + if outcome.shouldFire { + return TriggerRuleOutcome( + rule: rule, + unsavedOccurrence: outcome.unsavedOccurrence + ) + } } return nil } diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 995f40a9c..378f9ad0f 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -141,6 +141,10 @@ public class PaywallViewController: UIViewController, LoadingDelegate { /// Whether the survey was shown, not shown, or in a holdout. Defaults to not shown. private var surveyPresentationResult: SurveyPresentationResult = .noShow + /// If the user match a rule with an occurrence, this needs to be saved on + /// paywall presentation. + private var unsavedOccurrence: TriggerRuleOccurrence? + private unowned let factory: TriggerSessionManagerFactory & TriggerFactory private unowned let storage: Storage private unowned let deviceHelper: DeviceHelper @@ -460,15 +464,18 @@ public class PaywallViewController: UIViewController, LoadingDelegate { /// for callbacks. func set( request: PresentationRequest, - paywallStatePublisher: PassthroughSubject + paywallStatePublisher: PassthroughSubject, + unsavedOccurrence: TriggerRuleOccurrence? ) { self.request = request self.paywallStateSubject = paywallStatePublisher + self.unsavedOccurrence = unsavedOccurrence } func present( on presenter: UIViewController, request: PresentationRequest, + unsavedOccurrence: TriggerRuleOccurrence?, presentationStyleOverride: PaywallPresentationStyle?, paywallStatePublisher: PassthroughSubject, completion: @escaping (Bool) -> Void @@ -482,7 +489,8 @@ public class PaywallViewController: UIViewController, LoadingDelegate { set( request: request, - paywallStatePublisher: paywallStatePublisher + paywallStatePublisher: paywallStatePublisher, + unsavedOccurrence: unsavedOccurrence ) setPresentationStyle(withOverride: presentationStyleOverride) @@ -700,6 +708,10 @@ extension PaywallViewController { if let paywallStateSubject = paywallStateSubject { Superwall.shared.storePresentationObjects(request, paywallStateSubject) } + if let unsavedOccurrence = unsavedOccurrence { + storage.coreDataManager.save(triggerRuleOccurrence: unsavedOccurrence) + self.unsavedOccurrence = nil + } isPresented = true Superwall.shared.dependencyContainer.delegateAdapter.didPresentPaywall(withInfo: info) Task { diff --git a/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorLogicTests.swift b/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorLogicTests.swift index d21cebbea..d24630a10 100644 --- a/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorLogicTests.swift +++ b/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorLogicTests.swift @@ -18,12 +18,12 @@ final class ExpressionEvaluatorLogicTests: XCTestCase { storage: storage, factory: dependencyContainer ) - let shouldFire = await evaluator.shouldFire( + let outcome = await evaluator.shouldFire( forOccurrence: .stub(), ruleMatched: false, isPreemptive: false ) - XCTAssertFalse(shouldFire) + XCTAssertFalse(outcome.shouldFire) } func testShouldFire_noOccurrenceRule() async { @@ -33,12 +33,12 @@ final class ExpressionEvaluatorLogicTests: XCTestCase { storage: storage, factory: dependencyContainer ) - let shouldFire = await evaluator.shouldFire( + let outcome = await evaluator.shouldFire( forOccurrence: nil, ruleMatched: true, isPreemptive: false ) - XCTAssertTrue(shouldFire) + XCTAssertTrue(outcome.shouldFire) } func testShouldFire_shouldntFire_maxCountGTCount() async { @@ -49,13 +49,13 @@ final class ExpressionEvaluatorLogicTests: XCTestCase { storage: storage, factory: dependencyContainer ) - let shouldFire = await evaluator.shouldFire( + let outcome = await evaluator.shouldFire( forOccurrence: .stub() .setting(\.maxCount, to: 1), ruleMatched: true, isPreemptive: false ) - XCTAssertFalse(shouldFire) + XCTAssertFalse(outcome.shouldFire) } func testShouldFire_shouldFire_maxCountEqualToCount() async { @@ -66,13 +66,13 @@ final class ExpressionEvaluatorLogicTests: XCTestCase { storage: storage, factory: dependencyContainer ) - let shouldFire = await evaluator.shouldFire( + let outcome = await evaluator.shouldFire( forOccurrence: .stub() .setting(\.maxCount, to: 1), ruleMatched: true, isPreemptive: false ) - XCTAssertTrue(shouldFire) + XCTAssertTrue(outcome.shouldFire) } func testShouldFire_shouldFire_maxCountLtCount() async { @@ -83,12 +83,12 @@ final class ExpressionEvaluatorLogicTests: XCTestCase { storage: storage, factory: dependencyContainer ) - let shouldFire = await evaluator.shouldFire( + let outcome = await evaluator.shouldFire( forOccurrence: .stub() .setting(\.maxCount, to: 4), ruleMatched: true, isPreemptive: false ) - XCTAssertTrue(shouldFire) + XCTAssertTrue(outcome.shouldFire) } } diff --git a/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorTests.swift b/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorTests.swift index 1d330257e..f9c166d76 100644 --- a/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorTests.swift +++ b/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorTests.swift @@ -25,7 +25,7 @@ final class ExpressionEvaluatorTests: XCTestCase { eventData: .stub(), isPreemptive: false ) - XCTAssertTrue(result) + XCTAssertTrue(result.shouldFire) } // MARK: - Expression @@ -44,7 +44,28 @@ final class ExpressionEvaluatorTests: XCTestCase { eventData: EventData(name: "ss", parameters: [:], createdAt: Date()), isPreemptive: false ) - XCTAssertTrue(result) + XCTAssertTrue(result.shouldFire) + XCTAssertNil(result.unsavedOccurrence) + } + + func testExpressionEvaluator_expression_withOccurrence() async { + let dependencyContainer = DependencyContainer() + dependencyContainer.storage.reset() + let evaluator = ExpressionEvaluator( + storage: dependencyContainer.storage, + factory: dependencyContainer + ) + dependencyContainer.identityManager.mergeUserAttributes(["a": "b"]) + let occurrence = TriggerRuleOccurrence(key: "a", maxCount: 1, interval: .infinity) + let result = await evaluator.evaluateExpression( + fromRule: .stub() + .setting(\.expression, to: "user.a == \"b\"") + .setting(\.occurrence, to: occurrence), + eventData: EventData(name: "ss", parameters: [:], createdAt: Date()), + isPreemptive: false + ) + XCTAssertTrue(result.shouldFire) + XCTAssertEqual(result.unsavedOccurrence, occurrence) } func testExpressionEvaluator_expressionParams() async { @@ -61,7 +82,7 @@ final class ExpressionEvaluatorTests: XCTestCase { eventData: EventData(name: "ss", parameters: ["a": "b"], createdAt: Date()), isPreemptive: false ) - XCTAssertTrue(result) + XCTAssertTrue(result.shouldFire) } func testExpressionEvaluator_expressionDeviceTrue() async { @@ -78,7 +99,7 @@ final class ExpressionEvaluatorTests: XCTestCase { eventData: EventData(name: "ss", parameters: ["a": "b"], createdAt: Date()), isPreemptive: false ) - XCTAssertTrue(result) + XCTAssertTrue(result.shouldFire) } func testExpressionEvaluator_expressionDeviceFalse() async { @@ -95,7 +116,7 @@ final class ExpressionEvaluatorTests: XCTestCase { eventData: EventData(name: "ss", parameters: ["a": "b"], createdAt: Date()), isPreemptive: false ) - XCTAssertFalse(result) + XCTAssertFalse(result.shouldFire) } func testExpressionEvaluator_expressionFalse() async { @@ -112,7 +133,7 @@ final class ExpressionEvaluatorTests: XCTestCase { eventData: .stub(), isPreemptive: false ) - XCTAssertFalse(result) + XCTAssertFalse(result.shouldFire) } /* func testExpressionEvaluator_events() { @@ -143,7 +164,7 @@ final class ExpressionEvaluatorTests: XCTestCase { eventData: .stub(), isPreemptive: false ) - XCTAssertTrue(result) + XCTAssertTrue(result.shouldFire) } func testExpressionEvaluator_expressionJSValues_true() async { @@ -158,7 +179,7 @@ final class ExpressionEvaluatorTests: XCTestCase { eventData: EventData(name: "ss", parameters: ["a": "b"], createdAt: Date()), isPreemptive: false ) - XCTAssertTrue(result) + XCTAssertTrue(result.shouldFire) } func testExpressionEvaluator_expressionJSValues_false() async { @@ -173,7 +194,7 @@ final class ExpressionEvaluatorTests: XCTestCase { eventData: EventData(name: "ss", parameters: ["a": "b"], createdAt: Date()), isPreemptive: false ) - XCTAssertTrue(result) + XCTAssertTrue(result.shouldFire) } func testExpressionEvaluator_expressionJSNumbers() async { @@ -188,7 +209,7 @@ final class ExpressionEvaluatorTests: XCTestCase { eventData: .stub(), isPreemptive: false ) - XCTAssertTrue(result) + XCTAssertTrue(result.shouldFire) } /* func testExpressionEvaluator_expressionJSValues_events() { @@ -217,6 +238,6 @@ final class ExpressionEvaluatorTests: XCTestCase { eventData: .stub(), isPreemptive: false ) - XCTAssertFalse(result) + XCTAssertFalse(result.shouldFire) } } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift index ed1cc7b56..7a5d397db 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift @@ -47,7 +47,7 @@ final class CheckPaywallPresentableOperatorTests: XCTestCase { do { try await Superwall.shared.getPresenter( for: paywallVc, - rulesOutput: EvaluateRulesOutput(triggerResult: .paywall(experiment)), + rulesOutcome: RuleEvaluationOutcome(triggerResult: .paywall(experiment)), request: request, debugInfo: [:], paywallStatePublisher: statePublisher @@ -106,7 +106,7 @@ final class CheckPaywallPresentableOperatorTests: XCTestCase { do { try await Superwall.shared.getPresenter( for: paywallVc, - rulesOutput: EvaluateRulesOutput(triggerResult: .paywall(experiment)), + rulesOutcome: RuleEvaluationOutcome(triggerResult: .paywall(experiment)), request: request, debugInfo: [:], paywallStatePublisher: statePublisher @@ -149,7 +149,7 @@ final class CheckPaywallPresentableOperatorTests: XCTestCase { do { try await Superwall.shared.getPresenter( for: paywallVc, - rulesOutput: EvaluateRulesOutput(triggerResult: .paywall(experiment)), + rulesOutcome: RuleEvaluationOutcome(triggerResult: .paywall(experiment)), request: request, debugInfo: [:], paywallStatePublisher: statePublisher diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift index 4d2773b63..b672f58ec 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift @@ -25,7 +25,7 @@ final class CheckUserSubscriptionOperatorTests: XCTestCase { type: .getPaywall(.stub()) ) - let input = EvaluateRulesOutput( + let input = RuleEvaluationOutcome( triggerResult: .holdout(.init(id: "", groupId: "", variant: .init(id: "", type: .treatment, paywallId: ""))) ) @@ -78,7 +78,7 @@ final class CheckUserSubscriptionOperatorTests: XCTestCase { type: .getPaywall(.stub()) ) - let input = EvaluateRulesOutput( + let input = RuleEvaluationOutcome( triggerResult: .paywall(.init(id: "", groupId: "", variant: .init(id: "", type: .treatment, paywallId: ""))) ) @@ -117,7 +117,7 @@ final class CheckUserSubscriptionOperatorTests: XCTestCase { type: .getPaywall(.stub()) ) - let input = EvaluateRulesOutput( + let input = RuleEvaluationOutcome( triggerResult: .holdout(.init(id: "", groupId: "", variant: .init(id: "", type: .treatment, paywallId: ""))) ) diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignmentTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignmentTests.swift index 89af135fa..564162eb5 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignmentTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignmentTests.swift @@ -28,12 +28,12 @@ final class ConfirmHoldoutAssignmentOperatorTests: XCTestCase { dependencyContainer.configManager = configManager - let input = EvaluateRulesOutput( + let input = RuleEvaluationOutcome( triggerResult: .paywall(.init(id: "", groupId: "", variant: .init(id: "", type: .treatment, paywallId: ""))) ) Superwall.shared.confirmHoldoutAssignment( - rulesOutput: input, + from: input, dependencyContainer: dependencyContainer ) XCTAssertFalse(configManager.confirmedAssignment) @@ -54,12 +54,12 @@ final class ConfirmHoldoutAssignmentOperatorTests: XCTestCase { dependencyContainer.configManager = configManager - let input = EvaluateRulesOutput( + let input = RuleEvaluationOutcome( triggerResult: .holdout(.init(id: "", groupId: "", variant: .init(id: "", type: .treatment, paywallId: ""))) ) Superwall.shared.confirmHoldoutAssignment( - rulesOutput: input, + from: input, dependencyContainer: dependencyContainer ) XCTAssertFalse(configManager.confirmedAssignment) @@ -79,13 +79,13 @@ final class ConfirmHoldoutAssignmentOperatorTests: XCTestCase { dependencyContainer.configManager = configManager - let input = EvaluateRulesOutput( - triggerResult: .holdout(.init(id: "", groupId: "", variant: .init(id: "", type: .treatment, paywallId: ""))), - confirmableAssignment: .init(experimentId: "", variant: .init(id: "", type: .treatment, paywallId: "")) + let input = RuleEvaluationOutcome( + confirmableAssignment: .init(experimentId: "", variant: .init(id: "", type: .treatment, paywallId: "")), + triggerResult: .holdout(.init(id: "", groupId: "", variant: .init(id: "", type: .treatment, paywallId: ""))) ) Superwall.shared.confirmHoldoutAssignment( - rulesOutput: input, + from: input, dependencyContainer: dependencyContainer ) XCTAssertTrue(configManager.confirmedAssignment) diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperatorTests.swift index b7c9c9f42..c481083af 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperatorTests.swift @@ -56,7 +56,6 @@ final class GetPaywallVcOperatorTests: XCTestCase { _ = try await Superwall.shared.getPaywallViewController( request: request, experiment: experiment, - rulesOutput: .init(triggerResult: .paywall(experiment)), debugInfo: [:], paywallStatePublisher: statePublisher, dependencyContainer: dependencyContainer @@ -112,7 +111,6 @@ final class GetPaywallVcOperatorTests: XCTestCase { _ = try await Superwall.shared.getPaywallViewController( request: request, experiment: experiment, - rulesOutput: .init(triggerResult: .paywall(experiment)), debugInfo: [:], paywallStatePublisher: statePublisher, dependencyContainer: dependencyContainer @@ -157,7 +155,6 @@ final class GetPaywallVcOperatorTests: XCTestCase { _ = try await Superwall.shared.getPaywallViewController( request: request, experiment: experiment, - rulesOutput: .init(triggerResult: .paywall(experiment)), debugInfo: [:], paywallStatePublisher: statePublisher, dependencyContainer: dependencyContainer diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift index fdc1065d4..988f03cf7 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift @@ -13,7 +13,7 @@ final class HandleTriggerResultOperatorTests: XCTestCase { var cancellables: [AnyCancellable] = [] func test_handleTriggerResult_paywall() async { - let input = EvaluateRulesOutput( + let input = RuleEvaluationOutcome( triggerResult: .paywall(.init(id: "", groupId: "", variant: .init(id: "", type: .treatment, paywallId: ""))) ) @@ -29,7 +29,7 @@ final class HandleTriggerResultOperatorTests: XCTestCase { do { _ = try await Superwall.shared.getExperiment( request: .stub(), - rulesOutput: input, + rulesOutcome: input, paywallStatePublisher: statePublisher ) } catch { @@ -42,7 +42,7 @@ final class HandleTriggerResultOperatorTests: XCTestCase { func test_handleTriggerResult_holdout() async { //TODO: THis doesn't take into account activateSession let experimentId = "abc" - let input = EvaluateRulesOutput( + let input = RuleEvaluationOutcome( triggerResult: .holdout(.init(id: experimentId, groupId: "", variant: .init(id: "", type: .treatment, paywallId: ""))) ) @@ -76,7 +76,7 @@ final class HandleTriggerResultOperatorTests: XCTestCase { do { _ = try await Superwall.shared.getExperiment( request: .stub(), - rulesOutput: input, + rulesOutcome: input, paywallStatePublisher: statePublisher ) XCTFail("Should fail") @@ -93,7 +93,7 @@ final class HandleTriggerResultOperatorTests: XCTestCase { } func test_handleTriggerResult_noRuleMatch() async { - let input = EvaluateRulesOutput( + let input = RuleEvaluationOutcome( triggerResult: .noRuleMatch ) @@ -126,7 +126,7 @@ final class HandleTriggerResultOperatorTests: XCTestCase { do { _ = try await Superwall.shared.getExperiment( request: .stub(), - rulesOutput: input, + rulesOutcome: input, paywallStatePublisher: statePublisher ) XCTFail("Should fail") @@ -143,7 +143,7 @@ final class HandleTriggerResultOperatorTests: XCTestCase { } func test_handleTriggerResult_eventNotFound() async { - let input = EvaluateRulesOutput( + let input = RuleEvaluationOutcome( triggerResult: .eventNotFound ) @@ -176,7 +176,7 @@ final class HandleTriggerResultOperatorTests: XCTestCase { do { _ = try await Superwall.shared.getExperiment( request: .stub(), - rulesOutput: input, + rulesOutcome: input, paywallStatePublisher: statePublisher ) XCTFail("Should fail") @@ -197,7 +197,7 @@ final class HandleTriggerResultOperatorTests: XCTestCase { domain: "Test", code: 1 ) - let input = EvaluateRulesOutput( + let input = RuleEvaluationOutcome( triggerResult: .error(outputError) ) @@ -226,7 +226,7 @@ final class HandleTriggerResultOperatorTests: XCTestCase { do { _ = try await Superwall.shared.getExperiment( request: .stub(), - rulesOutput: input, + rulesOutcome: input, paywallStatePublisher: statePublisher ) XCTFail("Should fail") diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift index edc00ef26..284d6f775 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift @@ -58,6 +58,7 @@ final class PresentPaywallOperatorTests: XCTestCase { _ = try await Superwall.shared.presentPaywallViewController( paywallVc, on: UIViewController(), + unsavedOccurrence: nil, debugInfo: [:], request: .stub(), paywallStatePublisher: statePublisher @@ -120,6 +121,7 @@ final class PresentPaywallOperatorTests: XCTestCase { _ = try await Superwall.shared.presentPaywallViewController( paywallVc, on: UIViewController(), + unsavedOccurrence: nil, debugInfo: [:], request: .stub(), paywallStatePublisher: statePublisher diff --git a/Tests/SuperwallKitTests/Paywall/View Controller/PaywallViewControllerMock.swift b/Tests/SuperwallKitTests/Paywall/View Controller/PaywallViewControllerMock.swift index caeb3d56c..411047540 100644 --- a/Tests/SuperwallKitTests/Paywall/View Controller/PaywallViewControllerMock.swift +++ b/Tests/SuperwallKitTests/Paywall/View Controller/PaywallViewControllerMock.swift @@ -14,7 +14,8 @@ final class PaywallViewControllerMock: PaywallViewController { override func present( on presenter: UIViewController, - request: PresentationRequest?, + request: PresentationRequest, + unsavedOccurrence: TriggerRuleOccurrence?, presentationStyleOverride: PaywallPresentationStyle?, paywallStatePublisher: PassthroughSubject, completion: @escaping (Bool) -> Void From 9e56452dbb5e25297315f0809bdd0382dcb7de1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 17 Aug 2023 16:07:15 +0800 Subject: [PATCH 09/12] Uses resources instead of resource_bundles in podspec - This fixes the issue where the resources from the bundle weren't being found. - Also renames the assets to be prefixed with SuperwallKit_ so there are no name collisions. --- CHANGELOG.md | 1 + ...te.cer => Superwall_AppleIncRootCertificate.cer} | Bin ...te.cer => Superwall_StoreKitTestCertificate.cer} | Bin .../Contents.json | 0 .../debugger.imageset/Contents.json | 0 .../debugger.imageset/debugger.pdf | Bin .../down_arrow.imageset/Contents.json | 0 .../down_arrow.imageset/Group 4.pdf | Bin .../exit.imageset/Contents.json | 0 .../exit.imageset/exit.pdf | Bin .../exit_paywall.imageset/Contents.json | 0 .../exit_paywall.imageset/exit-paywall.pdf | Bin .../paywall_placeholder.imageset/Contents.json | 0 .../paywall_placeholder.imageset/Group 4.pdf | Bin .../Artboard Copy.pdf | Bin .../Contents.json | 0 .../play_button.imageset/Contents.json | 0 .../play_button.imageset/Group.pdf | Bin .../reload_paywall.imageset/Contents.json | 0 .../reload_paywall.imageset/reload_paywall.pdf | Bin .../superwall_logo.imageset/Contents.json | 0 .../superwall_logo.imageset/superwall_logo.pdf | Bin .../Receipt Models/InAppReceipt.swift | 4 ++-- SuperwallKit.podspec | 12 +++++------- 24 files changed, 8 insertions(+), 9 deletions(-) rename Sources/SuperwallKit/Resources/Certificates/{AppleIncRootCertificate.cer => Superwall_AppleIncRootCertificate.cer} (100%) rename Sources/SuperwallKit/Resources/Certificates/{StoreKitTestCertificate.cer => Superwall_StoreKitTestCertificate.cer} (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/Contents.json (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/debugger.imageset/Contents.json (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/debugger.imageset/debugger.pdf (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/down_arrow.imageset/Contents.json (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/down_arrow.imageset/Group 4.pdf (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/exit.imageset/Contents.json (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/exit.imageset/exit.pdf (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/exit_paywall.imageset/Contents.json (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/exit_paywall.imageset/exit-paywall.pdf (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/paywall_placeholder.imageset/Contents.json (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/paywall_placeholder.imageset/Group 4.pdf (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/paywall_placeholder_landscape.imageset/Artboard Copy.pdf (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/paywall_placeholder_landscape.imageset/Contents.json (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/play_button.imageset/Contents.json (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/play_button.imageset/Group.pdf (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/reload_paywall.imageset/Contents.json (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/reload_paywall.imageset/reload_paywall.pdf (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/superwall_logo.imageset/Contents.json (100%) rename Sources/SuperwallKit/Resources/{Assets.xcassets => Superwall_Assets.xcassets}/superwall_logo.imageset/superwall_logo.pdf (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b84779745..9b40724ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup ### Fixes - Fixes rare issue when using limits on a campaign rule. If a paywall encountered an error preventing it from being presented, it may still have been counted as having been presented. This would then have affected future paywall presentation requests underneath the same rule. +- Fixes issue where resources weren't being accessed correctly when installing the SDK via CocoaPods. ## 3.3.0 diff --git a/Sources/SuperwallKit/Resources/Certificates/AppleIncRootCertificate.cer b/Sources/SuperwallKit/Resources/Certificates/Superwall_AppleIncRootCertificate.cer similarity index 100% rename from Sources/SuperwallKit/Resources/Certificates/AppleIncRootCertificate.cer rename to Sources/SuperwallKit/Resources/Certificates/Superwall_AppleIncRootCertificate.cer diff --git a/Sources/SuperwallKit/Resources/Certificates/StoreKitTestCertificate.cer b/Sources/SuperwallKit/Resources/Certificates/Superwall_StoreKitTestCertificate.cer similarity index 100% rename from Sources/SuperwallKit/Resources/Certificates/StoreKitTestCertificate.cer rename to Sources/SuperwallKit/Resources/Certificates/Superwall_StoreKitTestCertificate.cer diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/Contents.json diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/debugger.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/debugger.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/debugger.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/debugger.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/debugger.imageset/debugger.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/debugger.imageset/debugger.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/debugger.imageset/debugger.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/debugger.imageset/debugger.pdf diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/down_arrow.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/down_arrow.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/down_arrow.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/down_arrow.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/down_arrow.imageset/Group 4.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/down_arrow.imageset/Group 4.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/down_arrow.imageset/Group 4.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/down_arrow.imageset/Group 4.pdf diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/exit.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/exit.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/exit.imageset/exit.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit.imageset/exit.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/exit.imageset/exit.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit.imageset/exit.pdf diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/exit_paywall.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit_paywall.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/exit_paywall.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit_paywall.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/exit_paywall.imageset/exit-paywall.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit_paywall.imageset/exit-paywall.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/exit_paywall.imageset/exit-paywall.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit_paywall.imageset/exit-paywall.pdf diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/paywall_placeholder.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/paywall_placeholder.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/paywall_placeholder.imageset/Group 4.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder.imageset/Group 4.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/paywall_placeholder.imageset/Group 4.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder.imageset/Group 4.pdf diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/paywall_placeholder_landscape.imageset/Artboard Copy.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder_landscape.imageset/Artboard Copy.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/paywall_placeholder_landscape.imageset/Artboard Copy.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder_landscape.imageset/Artboard Copy.pdf diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/paywall_placeholder_landscape.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder_landscape.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/paywall_placeholder_landscape.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder_landscape.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/play_button.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/play_button.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/play_button.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/play_button.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/play_button.imageset/Group.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/play_button.imageset/Group.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/play_button.imageset/Group.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/play_button.imageset/Group.pdf diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/reload_paywall.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/reload_paywall.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/reload_paywall.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/reload_paywall.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/reload_paywall.imageset/reload_paywall.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/reload_paywall.imageset/reload_paywall.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/reload_paywall.imageset/reload_paywall.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/reload_paywall.imageset/reload_paywall.pdf diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/superwall_logo.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/superwall_logo.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/superwall_logo.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/superwall_logo.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Assets.xcassets/superwall_logo.imageset/superwall_logo.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/superwall_logo.imageset/superwall_logo.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Assets.xcassets/superwall_logo.imageset/superwall_logo.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/superwall_logo.imageset/superwall_logo.pdf diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppReceipt.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppReceipt.swift index a943fc071..43c48bc0c 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppReceipt.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppReceipt.swift @@ -67,9 +67,9 @@ final class InAppReceipt { self.rawData = receiptData #if DEBUG - let certificateName = "StoreKitTestCertificate" + let certificateName = "Superwall_StoreKitTestCertificate" #else - let certificateName = "AppleIncRootCertificate" + let certificateName = "Superwall_AppleIncRootCertificate" #endif self.rootCertificatePath = rootCertPath ?? Bundle.module.path(forResource: certificateName, ofType: "cer") diff --git a/SuperwallKit.podspec b/SuperwallKit.podspec index 7d313b593..ec8fce7d8 100644 --- a/SuperwallKit.podspec +++ b/SuperwallKit.podspec @@ -39,12 +39,10 @@ Pod::Spec.new do |s| s.requires_arc = true s.source_files = "Sources/**/*.{swift}" - s.resources = "Sources/SuperwallKit/**/*.cer" - s.resource_bundles = { - "SuperwallKit" => [ - "Sources/SuperwallKit/**/*.xcassets", - "Sources/SuperwallKit/**/*.xcdatamodeld" - ] - } + s.resources = [ + "Sources/SuperwallKit/**/*.xcassets", + "Sources/SuperwallKit/**/*.xcdatamodeld", + "Sources/SuperwallKit/**/*.cer" + ] end From 46fefd4c7d42382d4d6d9e7f8d2ab68655f70df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 17 Aug 2023 16:25:13 +0800 Subject: [PATCH 10/12] Prepend SuperwallKit_ to all assets to avoid collisions --- .../HomeViewController.swift | 76 +++++++----------- .../Debug/DebugViewController.swift | 16 ++-- .../Presentation/Rule Logic/RuleLogic.swift | 1 + .../View Controller/Loading/ShimmerView.swift | 4 +- .../PaywallViewController.swift | 4 +- .../Contents.json | 0 .../debugger.pdf | Bin .../Contents.json | 0 .../Group 4.pdf | Bin .../Contents.json | 0 .../exit.pdf | Bin .../Contents.json | 0 .../exit-paywall.pdf | Bin .../Contents.json | 0 .../Group 4.pdf | Bin .../Artboard Copy.pdf | Bin .../Contents.json | 0 .../Contents.json | 0 .../Group.pdf | Bin .../Contents.json | 0 .../reload_paywall.pdf | Bin .../Contents.json | 0 .../superwall_logo.pdf | Bin 23 files changed, 44 insertions(+), 57 deletions(-) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{debugger.imageset => SuperwallKit_debugger.imageset}/Contents.json (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{debugger.imageset => SuperwallKit_debugger.imageset}/debugger.pdf (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{down_arrow.imageset => SuperwallKit_down_arrow.imageset}/Contents.json (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{down_arrow.imageset => SuperwallKit_down_arrow.imageset}/Group 4.pdf (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{exit.imageset => SuperwallKit_exit.imageset}/Contents.json (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{exit.imageset => SuperwallKit_exit.imageset}/exit.pdf (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{exit_paywall.imageset => SuperwallKit_exit_paywall.imageset}/Contents.json (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{exit_paywall.imageset => SuperwallKit_exit_paywall.imageset}/exit-paywall.pdf (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{paywall_placeholder.imageset => SuperwallKit_paywall_placeholder.imageset}/Contents.json (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{paywall_placeholder.imageset => SuperwallKit_paywall_placeholder.imageset}/Group 4.pdf (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{paywall_placeholder_landscape.imageset => SuperwallKit_paywall_placeholder_landscape.imageset}/Artboard Copy.pdf (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{paywall_placeholder_landscape.imageset => SuperwallKit_paywall_placeholder_landscape.imageset}/Contents.json (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{play_button.imageset => SuperwallKit_play_button.imageset}/Contents.json (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{play_button.imageset => SuperwallKit_play_button.imageset}/Group.pdf (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{reload_paywall.imageset => SuperwallKit_reload_paywall.imageset}/Contents.json (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{reload_paywall.imageset => SuperwallKit_reload_paywall.imageset}/reload_paywall.pdf (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{superwall_logo.imageset => SuperwallKit_superwall_logo.imageset}/Contents.json (100%) rename Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/{superwall_logo.imageset => SuperwallKit_superwall_logo.imageset}/superwall_logo.pdf (100%) diff --git a/Examples/UIKit-Swift/Superwall-UIKit-Swift/HomeViewController.swift b/Examples/UIKit-Swift/Superwall-UIKit-Swift/HomeViewController.swift index e8f595a16..70bb76c48 100644 --- a/Examples/UIKit-Swift/Superwall-UIKit-Swift/HomeViewController.swift +++ b/Examples/UIKit-Swift/Superwall-UIKit-Swift/HomeViewController.swift @@ -58,44 +58,39 @@ final class HomeViewController: UIViewController { } @IBAction private func launchFeature() { - Task { - let paywall = try? await Superwall.shared.getPaywall(forEvent: "campaign_trigger", delegate: self) - self.present(paywall!, animated: true) + let handler = PaywallPresentationHandler() + handler.onDismiss { paywallInfo in + print("The paywall dismissed. PaywallInfo:", paywallInfo) + } + handler.onPresent { paywallInfo in + print("The paywall presented. PaywallInfo:", paywallInfo) + } + handler.onError { error in + print("The paywall presentation failed with error \(error)") + } + handler.onSkip { reason in + switch reason { + case .userIsSubscribed: + print("Paywall not shown because user is subscribed.") + case .holdout(let experiment): + print("Paywall not shown because user is in a holdout group in Experiment: \(experiment.id)") + case .noRuleMatch: + print("Paywall not shown because user doesn't match any rules.") + case .eventNotFound: + print("Paywall not shown because this event isn't part of a campaign.") + } } -// let handler = PaywallPresentationHandler() -// handler.onDismiss { paywallInfo in -// print("The paywall dismissed. PaywallInfo:", paywallInfo) -// } -// handler.onPresent { paywallInfo in -// print("The paywall presented. PaywallInfo:", paywallInfo) -// } -// handler.onError { error in -// print("The paywall presentation failed with error \(error)") -// } -// handler.onSkip { reason in -// switch reason { -// case .userIsSubscribed: -// print("Paywall not shown because user is subscribed.") -// case .holdout(let experiment): -// print("Paywall not shown because user is in a holdout group in Experiment: \(experiment.id)") -// case .noRuleMatch: -// print("Paywall not shown because user doesn't match any rules.") -// case .eventNotFound: -// print("Paywall not shown because this event isn't part of a campaign.") -// } -// } -// -// Superwall.shared.register(event: "campaign_trigger", handler: handler) { -// // code in here can be remotely configured to execute. Either -// // (1) always after presentation or -// // (2) only if the user pays -// // code is always executed if no paywall is configured to show -// self.presentAlert( -// title: "Feature Launched", -// message: "Wrap your awesome features in register calls like this to remotely paywall your app. You can remotely decide whether these are paid features." -// ) -// } + Superwall.shared.register(event: "campaign_trigger", handler: handler) { + // code in here can be remotely configured to execute. Either + // (1) always after presentation or + // (2) only if the user pays + // code is always executed if no paywall is configured to show + self.presentAlert( + title: "Feature Launched", + message: "Wrap your awesome features in register calls like this to remotely paywall your app. You can remotely decide whether these are paid features." + ) + } } private func presentAlert(title: String, message: String) { @@ -110,12 +105,3 @@ final class HomeViewController: UIViewController { self.present(alertController, animated: true) } } - -extension HomeViewController: PaywallViewControllerDelegate { - func paywall(_ paywall: PaywallViewController, didFinishWith result: PaywallResult, shouldDismiss: Bool) { - print(result) - if shouldDismiss { - paywall.dismiss(animated: true) - } - } -} diff --git a/Sources/SuperwallKit/Debug/DebugViewController.swift b/Sources/SuperwallKit/Debug/DebugViewController.swift index 5d723e856..24eec73c8 100644 --- a/Sources/SuperwallKit/Debug/DebugViewController.swift +++ b/Sources/SuperwallKit/Debug/DebugViewController.swift @@ -26,7 +26,7 @@ struct AlertOption { @MainActor final class DebugViewController: UIViewController { var logoImageView: UIImageView = { - let superwallLogo = UIImage(named: "superwall_logo", in: Bundle.module, compatibleWith: nil)! + let superwallLogo = UIImage(named: "SuperwallKit_superwall_logo", in: Bundle.module, compatibleWith: nil)! let imageView = UIImageView(image: superwallLogo) imageView.contentMode = .scaleAspectFit imageView.backgroundColor = .clear @@ -38,7 +38,7 @@ final class DebugViewController: UIViewController { lazy var exitButton: SWBounceButton = { let button = SWBounceButton() - let image = UIImage(named: "exit", in: Bundle.module, compatibleWith: nil)! + let image = UIImage(named: "SuperwallKit_exit", in: Bundle.module, compatibleWith: nil)! button.setImage(image, for: .normal) button.translatesAutoresizingMaskIntoConstraints = false button.imageView?.tintColor = UIColor.white.withAlphaComponent(0.5) @@ -48,7 +48,7 @@ final class DebugViewController: UIViewController { lazy var consoleButton: SWBounceButton = { let button = SWBounceButton() - let image = UIImage(named: "debugger", in: Bundle.module, compatibleWith: nil)! + let image = UIImage(named: "SuperwallKit_debugger", in: Bundle.module, compatibleWith: nil)! button.setImage(image, for: .normal) button.translatesAutoresizingMaskIntoConstraints = false button.imageView?.tintColor = UIColor.white.withAlphaComponent(0.5) @@ -64,7 +64,7 @@ final class DebugViewController: UIViewController { button.setTitleColor(primaryColor, for: .normal) button.translatesAutoresizingMaskIntoConstraints = false - let image = UIImage(named: "play_button", in: Bundle.module, compatibleWith: nil)! + let image = UIImage(named: "SuperwallKit_play_button", in: Bundle.module, compatibleWith: nil)! button.titleEdgeInsets = UIEdgeInsets(top: -1, left: 0, bottom: 0, right: 0) // button.imageEdgeInsets = UIEdgeInsets(top: 1, left: 5, bottom: -1, right: -3) button.setImage(image, for: .normal) @@ -86,7 +86,7 @@ final class DebugViewController: UIViewController { button.imageView?.tintColor = primaryColor button.layer.cornerRadius = 10 - let image = UIImage(named: "down_arrow", in: Bundle.module, compatibleWith: nil)! + let image = UIImage(named: "SuperwallKit_down_arrow", in: Bundle.module, compatibleWith: nil)! button.semanticContentAttribute = .forceRightToLeft button.setImage(image, for: .normal) button.imageView?.tintColor = primaryColor @@ -448,7 +448,7 @@ final class DebugViewController: UIViewController { case .presented: self.bottomButton.showLoading = false - let playButton = UIImage(named: "play_button", in: Bundle.module, compatibleWith: nil)! + let playButton = UIImage(named: "SuperwallKit_play_button", in: Bundle.module, compatibleWith: nil)! self.bottomButton.setImage( playButton, for: .normal @@ -473,7 +473,7 @@ final class DebugViewController: UIViewController { ) self.bottomButton.showLoading = false - let playButton = UIImage(named: "play_button", in: Bundle.module, compatibleWith: nil)! + let playButton = UIImage(named: "SuperwallKit_play_button", in: Bundle.module, compatibleWith: nil)! self.bottomButton.setImage(playButton, for: .normal) self.activityIndicator.stopAnimating() case .dismissed: @@ -492,7 +492,7 @@ final class DebugViewController: UIViewController { ) self.bottomButton.showLoading = false - let playButton = UIImage(named: "play_button", in: Bundle.module, compatibleWith: nil)! + let playButton = UIImage(named: "SuperwallKit_play_button", in: Bundle.module, compatibleWith: nil)! self.bottomButton.setImage(playButton, for: .normal) self.activityIndicator.stopAnimating() } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/RuleLogic.swift b/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/RuleLogic.swift index e0e7062d1..504ef9845 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/RuleLogic.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Rule Logic/RuleLogic.swift @@ -4,6 +4,7 @@ // // Created by Yusuf Tör on 09/08/2022. // +// swiftlint:disable function_body_length import Foundation diff --git a/Sources/SuperwallKit/Paywall/View Controller/Loading/ShimmerView.swift b/Sources/SuperwallKit/Paywall/View Controller/Loading/ShimmerView.swift index 73969976e..4f3c1c4f8 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Loading/ShimmerView.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Loading/ShimmerView.swift @@ -26,7 +26,7 @@ final class ShimmerView: UIImageView { private var placeholderImage: UIImage { if UIWindow.isLandscape { guard let placeholder = UIImage( - named: "paywall_placeholder_landscape", + named: "SuperwallKit_paywall_placeholder_landscape", in: Bundle.module, compatibleWith: nil ) else { @@ -36,7 +36,7 @@ final class ShimmerView: UIImageView { return placeholder } else { guard let placeholder = UIImage( - named: "paywall_placeholder", + named: "SuperwallKit_paywall_placeholder", in: Bundle.module, compatibleWith: nil ) else { diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 378f9ad0f..7f4748a20 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -103,7 +103,7 @@ public class PaywallViewController: UIViewController, LoadingDelegate { /// A button that refreshes the paywall presentation. private lazy var refreshPaywallButton: UIButton = { ButtonFactory.make( - imageNamed: "reload_paywall", + imageNamed: "SuperwallKit_reload_paywall", target: self, action: #selector(reloadWebView) ) @@ -112,7 +112,7 @@ public class PaywallViewController: UIViewController, LoadingDelegate { /// A button that exits the paywall. private lazy var exitButton: UIButton = { ButtonFactory.make( - imageNamed: "exit_paywall", + imageNamed: "SuperwallKit_exit_paywall", target: self, action: #selector(forceClose) ) diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/debugger.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_debugger.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/debugger.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_debugger.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/debugger.imageset/debugger.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_debugger.imageset/debugger.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/debugger.imageset/debugger.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_debugger.imageset/debugger.pdf diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/down_arrow.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_down_arrow.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/down_arrow.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_down_arrow.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/down_arrow.imageset/Group 4.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_down_arrow.imageset/Group 4.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/down_arrow.imageset/Group 4.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_down_arrow.imageset/Group 4.pdf diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_exit.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_exit.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit.imageset/exit.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_exit.imageset/exit.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit.imageset/exit.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_exit.imageset/exit.pdf diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit_paywall.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_exit_paywall.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit_paywall.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_exit_paywall.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit_paywall.imageset/exit-paywall.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_exit_paywall.imageset/exit-paywall.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/exit_paywall.imageset/exit-paywall.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_exit_paywall.imageset/exit-paywall.pdf diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_paywall_placeholder.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_paywall_placeholder.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder.imageset/Group 4.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_paywall_placeholder.imageset/Group 4.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder.imageset/Group 4.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_paywall_placeholder.imageset/Group 4.pdf diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder_landscape.imageset/Artboard Copy.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_paywall_placeholder_landscape.imageset/Artboard Copy.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder_landscape.imageset/Artboard Copy.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_paywall_placeholder_landscape.imageset/Artboard Copy.pdf diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder_landscape.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_paywall_placeholder_landscape.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/paywall_placeholder_landscape.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_paywall_placeholder_landscape.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/play_button.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_play_button.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/play_button.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_play_button.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/play_button.imageset/Group.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_play_button.imageset/Group.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/play_button.imageset/Group.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_play_button.imageset/Group.pdf diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/reload_paywall.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_reload_paywall.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/reload_paywall.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_reload_paywall.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/reload_paywall.imageset/reload_paywall.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_reload_paywall.imageset/reload_paywall.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/reload_paywall.imageset/reload_paywall.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_reload_paywall.imageset/reload_paywall.pdf diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/superwall_logo.imageset/Contents.json b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_superwall_logo.imageset/Contents.json similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/superwall_logo.imageset/Contents.json rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_superwall_logo.imageset/Contents.json diff --git a/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/superwall_logo.imageset/superwall_logo.pdf b/Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_superwall_logo.imageset/superwall_logo.pdf similarity index 100% rename from Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/superwall_logo.imageset/superwall_logo.pdf rename to Sources/SuperwallKit/Resources/Superwall_Assets.xcassets/SuperwallKit_superwall_logo.imageset/superwall_logo.pdf From 1524d0c8ad3b4fcb1ea2979310ea391e1015db40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 17 Aug 2023 16:30:25 +0800 Subject: [PATCH 11/12] Rename certificates' prepended word from Superwall to SuperwallKit --- ...cer => SuperwallKit_AppleIncRootCertificate.cer} | Bin ...cer => SuperwallKit_StoreKitTestCertificate.cer} | Bin .../Receipt Models/InAppReceipt.swift | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename Sources/SuperwallKit/Resources/Certificates/{Superwall_AppleIncRootCertificate.cer => SuperwallKit_AppleIncRootCertificate.cer} (100%) rename Sources/SuperwallKit/Resources/Certificates/{Superwall_StoreKitTestCertificate.cer => SuperwallKit_StoreKitTestCertificate.cer} (100%) diff --git a/Sources/SuperwallKit/Resources/Certificates/Superwall_AppleIncRootCertificate.cer b/Sources/SuperwallKit/Resources/Certificates/SuperwallKit_AppleIncRootCertificate.cer similarity index 100% rename from Sources/SuperwallKit/Resources/Certificates/Superwall_AppleIncRootCertificate.cer rename to Sources/SuperwallKit/Resources/Certificates/SuperwallKit_AppleIncRootCertificate.cer diff --git a/Sources/SuperwallKit/Resources/Certificates/Superwall_StoreKitTestCertificate.cer b/Sources/SuperwallKit/Resources/Certificates/SuperwallKit_StoreKitTestCertificate.cer similarity index 100% rename from Sources/SuperwallKit/Resources/Certificates/Superwall_StoreKitTestCertificate.cer rename to Sources/SuperwallKit/Resources/Certificates/SuperwallKit_StoreKitTestCertificate.cer diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppReceipt.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppReceipt.swift index 43c48bc0c..ac2eaa3aa 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppReceipt.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppReceipt.swift @@ -67,9 +67,9 @@ final class InAppReceipt { self.rawData = receiptData #if DEBUG - let certificateName = "Superwall_StoreKitTestCertificate" + let certificateName = "SuperwallKit_StoreKitTestCertificate" #else - let certificateName = "Superwall_AppleIncRootCertificate" + let certificateName = "SuperwallKit_AppleIncRootCertificate" #endif self.rootCertificatePath = rootCertPath ?? Bundle.module.path(forResource: certificateName, ofType: "cer") From 7ed0ef2cb0e9afe4a5938d9dc7dc883a6c2cadb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Fri, 18 Aug 2023 16:12:58 +0800 Subject: [PATCH 12/12] Fixes crash if you tried to save an object that didn't conform to NSSecureCoding in user attributes --- CHANGELOG.md | 3 ++- .../PaywallViewController.swift | 2 +- .../SuperwallKit/Storage/Cache/Cache.swift | 24 +++++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b40724ea..7b0a51609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup ### Fixes - Fixes rare issue when using limits on a campaign rule. If a paywall encountered an error preventing it from being presented, it may still have been counted as having been presented. This would then have affected future paywall presentation requests underneath the same rule. -- Fixes issue where resources weren't being accessed correctly when installing the SDK via CocoaPods. +- Fixes issue where assets weren't being accessed correctly when installing the SDK via CocoaPods. +- Fixes crash if you tried to save an object that didn't conform to NSSecureCoding in user attributes. ## 3.3.0 diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 7f4748a20..8bd4ffeb8 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -740,7 +740,7 @@ extension PaywallViewController { if isSafariVCPresented { return } - Task(priority: .utility) { + Task { await trackClose() } diff --git a/Sources/SuperwallKit/Storage/Cache/Cache.swift b/Sources/SuperwallKit/Storage/Cache/Cache.swift index bf741cb2d..c009cad21 100644 --- a/Sources/SuperwallKit/Storage/Cache/Cache.swift +++ b/Sources/SuperwallKit/Storage/Cache/Cache.swift @@ -139,7 +139,17 @@ class Cache { return } - let data = NSKeyedArchiver.archivedData(withRootObject: value) + guard let data = try? NSKeyedArchiver.archivedData( + withRootObject: value, + requiringSecureCoding: true + ) else { + Logger.debug( + logLevel: .warn, + scope: .cache, + message: "Could not write object that doesn't conform to NSSecureCoding to cache." + ) + return + } memCache.setObject(data as AnyObject, forKey: keyType.key as AnyObject) writeDataToDisk( @@ -163,7 +173,17 @@ class Cache { return } - let archivedData = NSKeyedArchiver.archivedData(withRootObject: data) + guard let archivedData = try? NSKeyedArchiver.archivedData( + withRootObject: data, + requiringSecureCoding: true + ) else { + Logger.debug( + logLevel: .warn, + scope: .cache, + message: "Could not write object that doesn't conform to NSSecureCoding to cache." + ) + return + } memCache.setObject(archivedData as AnyObject, forKey: keyType.key as AnyObject) writeDataToDisk(