diff --git a/CHANGELOG.md b/CHANGELOG.md index e4dfd4ff8..0fe1dacdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,18 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall-me/Superwall-iOS/releases) on GitHub. +## 3.4.1 + +### Fixes + +- Fixes issue where multiple events registered in quick succession may not be performed in serial, resulting in unexpected paywalls. +- Fixes issue where transaction data wouldn't be available for those who are using a purchase controller. + ## 3.4.0 ### Enhancements -- Adds `sdk_version`, `sdk_version_padded`, `app_build_string`, and `app_build_string_number` to the device object for use in rules. `sdk_version` is the version of the sdk, e.g. `3.4.0`. `sdk_version_padded` is the sdk version padded with zeros for use with string comparison. For example `003.004.000`. `app_build_string` is the build of your app and `app_build_string_number` is the build of your app casted as an Int. +- Adds `sdkVersion`, `sdkVersionPadded`, `appBuildString`, and `appBuildStringNumber` to the device object for use in rules. `sdkVersion` is the version of the sdk, e.g. `3.4.0`. `sdkVersionPadded` is the sdk version padded with zeros for use with string comparison. For example `003.004.000`. `appBuildString` is the build of your app and `appBuildStringNumber` is the build of your app casted as an Int (if possible). - When you experience `no_rule_match`, the `TriggerFire` event params will specify which part of the rules didn't match in the format `"unmatched_rule_": ""`. Where `outcome` will either be `OCCURRENCE`, referring to the limit applied to a rule, or `EXPRESSION`. The `id` is the experiment id. - Adds a `touches_began` implicit trigger. By adding the `touches_began` event to a campaign, you can show a paywall the first time a user touches anywhere in your app. - Adds the ability to include a close button on a survey. diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/Tracking.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/Tracking.swift index 6240f33b4..75714da49 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/Tracking.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/Tracking.swift @@ -20,7 +20,6 @@ extension Superwall { let eventCreatedAt = Date() let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: eventCreatedAt, appSessionId: dependencyContainer.appSessionManager.appSession.id ) @@ -74,6 +73,22 @@ extension Superwall { func handleImplicitTrigger( forEvent event: Trackable, withData eventData: EventData + ) async { + serialTaskManager.addTask { [weak self] in + guard let self = self else { + return + } + await self.internallyHandleImplicitTrigger( + forEvent: event, + withData: eventData + ) + } + } + + @MainActor + private func internallyHandleImplicitTrigger( + forEvent event: Trackable, + withData eventData: EventData ) async { let presentationInfo: PresentationInfo = .implicitTrigger(eventData) @@ -114,6 +129,6 @@ extension Superwall { request.flags.isPaywallPresented = isPaywallPresented - internallyPresent(request, statePublisher) + await internallyPresent(request, statePublisher) } } diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift index d2b69fc02..0f7b0ef08 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift @@ -18,7 +18,6 @@ enum TrackingLogic { static func processParameters( fromTrackableEvent trackableEvent: Trackable, - eventCreatedAt: Date, appSessionId: String ) async -> TrackingParameters { var superwallParameters = await trackableEvent.getSuperwallParameters() diff --git a/Sources/SuperwallKit/Debug/DebugViewController.swift b/Sources/SuperwallKit/Debug/DebugViewController.swift index 24eec73c8..8c5ef60e4 100644 --- a/Sources/SuperwallKit/Debug/DebugViewController.swift +++ b/Sources/SuperwallKit/Debug/DebugViewController.swift @@ -441,8 +441,10 @@ final class DebugViewController: UIViewController { type: .presentation ) - cancellable = Superwall.shared - .internallyPresent(presentationRequest) + + let publisher = PassthroughSubject() + cancellable = publisher + .receive(on: DispatchQueue.main) .sink { state in switch state { case .presented: @@ -497,6 +499,10 @@ final class DebugViewController: UIViewController { self.activityIndicator.stopAnimating() } } + + Task { + await Superwall.shared.internallyPresent(presentationRequest, publisher) + } } override func viewDidDisappear(_ animated: Bool) { diff --git a/Sources/SuperwallKit/Delegate/SuperwallDelegate.swift b/Sources/SuperwallKit/Delegate/SuperwallDelegate.swift index 24e1ba140..a1aaaf497 100644 --- a/Sources/SuperwallKit/Delegate/SuperwallDelegate.swift +++ b/Sources/SuperwallKit/Delegate/SuperwallDelegate.swift @@ -27,6 +27,7 @@ public protocol SuperwallDelegate: AnyObject { /// /// - Parameters: /// - newValue: The new value of ``Superwall/subscriptionStatus``. + @MainActor func subscriptionStatusDidChange(to newValue: SubscriptionStatus) /// Called whenever an internal analytics event is tracked. diff --git a/Sources/SuperwallKit/Delegate/SuperwallDelegateAdapter.swift b/Sources/SuperwallKit/Delegate/SuperwallDelegateAdapter.swift index e1189e907..30f7051c5 100644 --- a/Sources/SuperwallKit/Delegate/SuperwallDelegateAdapter.swift +++ b/Sources/SuperwallKit/Delegate/SuperwallDelegateAdapter.swift @@ -86,6 +86,7 @@ final class SuperwallDelegateAdapter { } } + @MainActor func subscriptionStatusDidChange(to newValue: SubscriptionStatus) { if let swiftDelegate = swiftDelegate { swiftDelegate.subscriptionStatusDidChange(to: newValue) diff --git a/Sources/SuperwallKit/Misc/Constants.swift b/Sources/SuperwallKit/Misc/Constants.swift index 7128ae2ad..cc0eb5a81 100644 --- a/Sources/SuperwallKit/Misc/Constants.swift +++ b/Sources/SuperwallKit/Misc/Constants.swift @@ -18,5 +18,5 @@ let sdkVersion = """ */ let sdkVersion = """ -3.4.0 +3.4.1 """ diff --git a/Sources/SuperwallKit/Misc/Data Structures/Queue.swift b/Sources/SuperwallKit/Misc/Data Structures/Queue.swift new file mode 100644 index 000000000..87f254103 --- /dev/null +++ b/Sources/SuperwallKit/Misc/Data Structures/Queue.swift @@ -0,0 +1,67 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 28/09/2023. +// +// https://github.com/kodecocodes/swift-algorithm-club/blob/master/Queue/README.markdown#a-more-efficient-queue + +import Foundation + +/** + First-in first-out queue (FIFO) + + Note: This isn't threadsafe. + New elements are added to the end of the queue. Dequeuing pulls elements from + the front of the queue. + + Enqueuing and dequeuing are O(1) operations. +*/ +struct Queue { + private var array: [T?] = [] + private var head = 0 + + var isEmpty: Bool { + // swiftlint:disable:next empty_count + return count == 0 + } + + var count: Int { + return array.count - head + } + + mutating func enqueue(_ element: T) { + array.append(element) + } + + mutating func dequeue() -> T? { + // Get element at head of queue + guard let element = array[guarded: head] else { + return nil + } + + // Replace the element with nil and increment the head + array[head] = nil + head += 1 + + // Calculate the percentage of the array that is nil + let percentage = Double(head) / Double(array.count) + + // If more than 25% of the queue is nil, chop off the head and reset + // head to 0 + if array.count > 50 && percentage > 0.25 { + array.removeFirst(head) + head = 0 + } + + return element + } + + var front: T? { + if isEmpty { + return nil + } else { + return array[head] + } + } +} diff --git a/Sources/SuperwallKit/Misc/Data Structures/SerialTaskManager.swift b/Sources/SuperwallKit/Misc/Data Structures/SerialTaskManager.swift new file mode 100644 index 000000000..8991a025d --- /dev/null +++ b/Sources/SuperwallKit/Misc/Data Structures/SerialTaskManager.swift @@ -0,0 +1,53 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 28/09/2023. +// + +import Foundation + +/// Serially executes tasks added to it. +final class SerialTaskManager { + private var taskQueue: Queue<() async -> Void> = Queue() + private var dispatchQueue = DispatchQueue(label: "com.superwall.serial-task-queue") + + func addTask(_ task: @escaping () async -> Void) { + dispatchQueue.async { [weak self] in + guard let self = self else { + return + } + + // Add the task to the queue + self.taskQueue.enqueue(task) + + // If there's only one task in the queue, start executing it + if self.taskQueue.count == 1 { + self.executeNextTask() + } + } + } + + private func executeNextTask() { + dispatchQueue.async { [weak self] in + guard let self = self else { + return + } + // Check if there are tasks in the queue + if taskQueue.isEmpty { + return + } + + // Get the next task from the queue + guard let nextTask = taskQueue.dequeue() else { + return + } + + Task { + await nextTask() + // After the task completes, recursively execute the next task + self.executeNextTask() + } + } + } +} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/GetPresentationResultLogic.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/GetPresentationResultLogic.swift index 9963f7556..bb58f4b0c 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/GetPresentationResultLogic.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/GetPresentationResultLogic.swift @@ -19,8 +19,8 @@ enum GetPresentationResultLogic { return .paywallNotAvailable case .noRuleMatch: return .noRuleMatch - case .paywall(let paywall): - return .paywall(paywall) + case .paywall(let experiment): + return .paywall(experiment) } } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift index 6812f7530..03d3986fe 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift @@ -77,7 +77,6 @@ extension Superwall { let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: eventCreatedAt, appSessionId: dependencyContainer.appSessionManager.appSession.id ) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/InternalPresentation.swift similarity index 53% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/InternalPresentation.swift index 9cc9f874e..1bb2e0650 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal/InternalPresentation.swift @@ -20,40 +20,32 @@ extension Superwall { /// /// - Parameters: /// - request: A presentation request of type `PresentationRequest` to feed into a presentation pipeline. - /// - paywallStatePublisher: A publisher fed into the pipeline that sends state updates. Defaults to `init()` and used by `presentAgain()` to pass in the existing state publisher. - /// - Returns: A publisher that outputs a ``PaywallState``. - @discardableResult + /// - paywallStatePublisher: A publisher fed into the pipeline that sends state updates. func internallyPresent( _ request: PresentationRequest, - _ publisher: PassthroughSubject = .init() - ) -> PaywallStatePublisher { - Task { - do { - try await checkNoPaywallAlreadyPresented(request, publisher) + _ publisher: PassthroughSubject + ) async { + do { + try await checkNoPaywallAlreadyPresented(request, publisher) - let paywallComponents = try await getPaywallComponents(request, publisher) + let paywallComponents = try await getPaywallComponents(request, publisher) - guard let presenter = paywallComponents.presenter else { - // Will never get here as an error would have already been thrown. - return - } - - try await presentPaywallViewController( - paywallComponents.viewController, - on: presenter, - unsavedOccurrence: paywallComponents.rulesOutcome.unsavedOccurrence, - debugInfo: paywallComponents.debugInfo, - request: request, - paywallStatePublisher: publisher - ) - } catch { - logErrors(from: request, error) + guard let presenter = paywallComponents.presenter else { + // Will never get here as an error would have already been thrown. + return } - } - return publisher - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() + try await presentPaywallViewController( + paywallComponents.viewController, + on: presenter, + unsavedOccurrence: paywallComponents.rulesOutcome.unsavedOccurrence, + debugInfo: paywallComponents.debugInfo, + request: request, + paywallStatePublisher: publisher + ) + } catch { + logErrors(from: request, error) + } } @MainActor diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentationLogic.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/InternalPresentationLogic.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentationLogic.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/InternalPresentationLogic.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/CheckDebuggerPresentation.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentation.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/CheckDebuggerPresentation.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/CheckNoPaywallAlreadyPresented.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/CheckNoPaywallAlreadyPresented.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscription.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/CheckUserSubscription.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscription.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/CheckUserSubscription.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/ConfirmHoldoutAssignment.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/ConfirmHoldoutAssignment.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignment.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/ConfirmPaywallAssignment.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignment.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/ConfirmPaywallAssignment.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/EvaluateRules.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/EvaluateRules.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/EvaluateRules.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/EvaluateRules.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetExperiment.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/GetExperiment.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetExperiment.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/GetExperiment.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVC.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/GetPaywallVC.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVC.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/GetPaywallVC.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPresenter.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/GetPresenter.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPresenter.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/GetPresenter.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogErrors.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/LogErrors.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogErrors.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/LogErrors.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/LogPresentation.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogPresentation.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/LogPresentation.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywall.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/PresentPaywall.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywall.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/PresentPaywall.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/StorePresentationObjects.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/StorePresentationObjects.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/StorePresentationObjects.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/StorePresentationObjects.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitForSubsStatusAndConfig.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/WaitForSubsStatusAndConfig.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitForSubsStatusAndConfig.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Operators/WaitForSubsStatusAndConfig.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/PaywallPresentationRequestStatus.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/PaywallPresentationRequestStatus.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/PaywallPresentationRequestStatus.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/PaywallPresentationRequestStatus.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PaywallOverrides.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Presentation Request/PaywallOverrides.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PaywallOverrides.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Presentation Request/PaywallOverrides.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationInfo.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Presentation Request/PresentationInfo.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationInfo.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Presentation Request/PresentationInfo.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationRequest.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Presentation Request/PresentationRequest.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationRequest.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Presentation Request/PresentationRequest.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Presentation State/PaywallSkippedReason.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Presentation State/PaywallSkippedReason.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal/Presentation State/PaywallState.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift rename to Sources/SuperwallKit/Paywall/Presentation/Internal/Presentation State/PaywallState.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index 4499ad910..937a7ddfc 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -86,9 +86,12 @@ extension Superwall { handler: PaywallPresentationHandler? = nil, feature: @escaping () -> Void ) { - Task { - await internallyRegister(event: event, params: params, handler: handler, feature: feature) - } + internallyRegister( + event: event, + params: params, + handler: handler, + feature: feature + ) } /// Registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. @@ -108,9 +111,7 @@ extension Superwall { params: [String: Any]? = nil, handler: PaywallPresentationHandler? = nil ) { - Task { - await internallyRegister(event: event, params: params, handler: handler) - } + internallyRegister(event: event, params: params, handler: handler) } private func internallyRegister( @@ -118,15 +119,11 @@ extension Superwall { params: [String: Any]? = nil, handler: PaywallPresentationHandler? = nil, feature completion: (() -> Void)? = nil - ) async { - let publisher = await asyncPublisher( - forEvent: event, - params: params, - paywallOverrides: nil, - isFeatureGatable: completion != nil - ) + ) { + let publisher = PassthroughSubject() publisher + .receive(on: DispatchQueue.main) .subscribe(Subscribers.Sink( receiveCompletion: { _ in }, receiveValue: { state in @@ -167,6 +164,16 @@ extension Superwall { } } )) + + serialTaskManager.addTask { + await self.trackAndPresentPaywall( + forEvent: event, + params: params, + paywallOverrides: nil, + isFeatureGatable: completion != nil, + publisher: publisher + ) + } } /// Objective-C-only convenience method. Registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. @@ -181,9 +188,7 @@ extension Superwall { /// - event: The name of the event you wish to register. @available(swift, obsoleted: 1.0) @objc public func register(event: String) { - Task { - await internallyRegister(event: event) - } + internallyRegister(event: event) } /// Objective-C-only convenience method. Registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. @@ -202,9 +207,7 @@ extension Superwall { event: String, params: [String: Any]? ) { - Task { - await internallyRegister(event: event, params: params) - } + internallyRegister(event: event, params: params) } /// Returns a publisher that registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. @@ -228,29 +231,35 @@ extension Superwall { isFeatureGatable: Bool ) -> PaywallStatePublisher { return Future { - return await self.asyncPublisher( + let publisher = PassthroughSubject() + + await self.trackAndPresentPaywall( forEvent: event, params: params, paywallOverrides: paywallOverrides, - isFeatureGatable: isFeatureGatable + isFeatureGatable: isFeatureGatable, + publisher: publisher ) + return publisher } .flatMap { publisher in return publisher } + .receive(on: DispatchQueue.main) .eraseToAnyPublisher() } - private func asyncPublisher( + private func trackAndPresentPaywall( forEvent event: String, params: [String: Any]? = nil, paywallOverrides: PaywallOverrides? = nil, - isFeatureGatable: Bool - ) async -> PaywallStatePublisher { + isFeatureGatable: Bool, + publisher: PassthroughSubject + ) async { do { try TrackingLogic.checkNotSuperwallEvent(event) } catch { - return Just(.presentationError(error)).eraseToAnyPublisher() + return } let trackableEvent = UserInitiatedEvent.Track( @@ -267,7 +276,6 @@ extension Superwall { isPaywallPresented: isPaywallPresented, type: .presentation ) - return internallyPresent(presentationRequest) - .eraseToAnyPublisher() + await internallyPresent(presentationRequest, publisher) } } diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index d554cf972..f9b88ce28 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -134,10 +134,6 @@ public class PaywallViewController: UIViewController, LoadingDelegate { /// `true` if there's a survey to complete and the paywall is displayed in a modal style. private var didDisableSwipeForSurvey = false - /// The presenting view controller, saved for presenting surveys from when - /// the view disappears. - private var internalPresentingViewController: UIViewController? - /// Whether the survey was shown, not shown, or in a holdout. Defaults to not shown. private var surveyPresentationResult: SurveyPresentationResult = .noShow @@ -707,10 +703,8 @@ extension PaywallViewController { presentationWillPrepare = false } - public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - internalPresentingViewController = presentingViewController presentationDidFinish() } diff --git a/Sources/SuperwallKit/Paywall/View Controller/Survey/SurveyPresentationResult.swift b/Sources/SuperwallKit/Paywall/View Controller/Survey/SurveyPresentationResult.swift index 16b50aa9b..7f5f03f68 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Survey/SurveyPresentationResult.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Survey/SurveyPresentationResult.swift @@ -7,6 +7,7 @@ import Foundation +/// **Do not edit the cases of this as it's used in events** enum SurveyPresentationResult: String { case show case holdout diff --git a/Sources/SuperwallKit/StoreKit/InternalPurchaseController.swift b/Sources/SuperwallKit/StoreKit/InternalPurchaseController.swift index 3c5b5b576..6ad5680b6 100644 --- a/Sources/SuperwallKit/StoreKit/InternalPurchaseController.swift +++ b/Sources/SuperwallKit/StoreKit/InternalPurchaseController.swift @@ -132,6 +132,9 @@ extension InternalPurchaseController { extension InternalPurchaseController { @MainActor func purchase(product: SKProduct) async -> PurchaseResult { + await productPurchaser.coordinator.beginPurchase( + of: product.productIdentifier + ) if let purchaseController = swiftPurchaseController { return await purchaseController.purchase(product: product) } else if let purchaseController = objcPurchaseController { diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserSK1.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserSK1.swift index 930a809f7..a92e1c555 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserSK1.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserSK1.swift @@ -48,8 +48,6 @@ final class ProductPurchaserSK1: NSObject { /// Purchases a product, waiting for the completion block to be fired and /// returning a purchase result. func purchase(product: SKProduct) async -> PurchaseResult { - await coordinator.beginPurchase(of: product.productIdentifier) - let task = Task { return await withCheckedContinuation { continuation in Task { diff --git a/Sources/SuperwallKit/Superwall.swift b/Sources/SuperwallKit/Superwall.swift index d15bc50e9..4f3b9539b 100644 --- a/Sources/SuperwallKit/Superwall.swift +++ b/Sources/SuperwallKit/Superwall.swift @@ -147,7 +147,6 @@ public final class Superwall: NSObject, ObservableObject { // MARK: - Non-public Properties private static var superwall: Superwall? - /// The presented paywall view controller. var paywallViewController: PaywallViewController? { return dependencyContainer.paywallManager.presentedViewController @@ -170,6 +169,9 @@ public final class Superwall: NSObject, ObservableObject { /// Handles all dependencies. let dependencyContainer: DependencyContainer + /// Used to serially execute register calls. + let serialTaskManager = SerialTaskManager() + // MARK: - Private Functions init(dependencyContainer: DependencyContainer = DependencyContainer()) { self.dependencyContainer = dependencyContainer @@ -236,9 +238,9 @@ public final class Superwall: NSObject, ObservableObject { return } self.dependencyContainer.storage.save(newValue, forType: ActiveSubscriptionStatus.self) - self.dependencyContainer.delegateAdapter.subscriptionStatusDidChange(to: newValue) Task { + await self.dependencyContainer.delegateAdapter.subscriptionStatusDidChange(to: newValue) let event = InternalSuperwallEvent.SubscriptionStatusDidChange(subscriptionStatus: newValue) await self.track(event) } diff --git a/SuperwallKit.podspec b/SuperwallKit.podspec index 71c25b952..8d9ce1deb 100644 --- a/SuperwallKit.podspec +++ b/SuperwallKit.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "SuperwallKit" - s.version = "3.4.0" + s.version = "3.4.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" diff --git a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackingLogicTests.swift b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackingLogicTests.swift index 752c8c026..7fd0f142f 100644 --- a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackingLogicTests.swift +++ b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackingLogicTests.swift @@ -18,7 +18,6 @@ final class TrackingLogicTests: XCTestCase { // When let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: Date(), appSessionId: "abc" ) @@ -55,7 +54,6 @@ final class TrackingLogicTests: XCTestCase { // When let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: Date(), appSessionId: "abc" ) @@ -98,7 +96,6 @@ final class TrackingLogicTests: XCTestCase { // When let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: event.eventData!.createdAt, appSessionId: "abc" ) @@ -121,7 +118,6 @@ final class TrackingLogicTests: XCTestCase { // When let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: Date(), appSessionId: "abc" ) @@ -148,7 +144,6 @@ final class TrackingLogicTests: XCTestCase { // When let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: Date(), appSessionId: "abc" ) @@ -175,7 +170,6 @@ final class TrackingLogicTests: XCTestCase { // When let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: Date(), appSessionId: "abc" ) @@ -202,7 +196,6 @@ final class TrackingLogicTests: XCTestCase { // When let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: Date(), appSessionId: "abc" ) @@ -230,7 +223,6 @@ final class TrackingLogicTests: XCTestCase { // When let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: Date(), appSessionId: "abc" ) @@ -258,7 +250,6 @@ final class TrackingLogicTests: XCTestCase { // When let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: Date(), appSessionId: "abc" ) @@ -285,7 +276,6 @@ final class TrackingLogicTests: XCTestCase { // When let parameters = await TrackingLogic.processParameters( fromTrackableEvent: event, - eventCreatedAt: Date(), appSessionId: "abc" )