From 85af0b899681a9364772be1b466b3a1d668a6742 Mon Sep 17 00:00:00 2001 From: jakemor Date: Fri, 28 Apr 2023 03:45:20 -0400 Subject: [PATCH 01/27] adds way to get raw paywallViewController for presentation --- .../AppDelegate.swift | 3 +- .../HomeViewController.swift | 83 +++++++++++++++++-- .../RCPurchaseController.swift | 2 +- .../PublicGetTrackResult.swift | 2 +- .../GetPaywallViewController.swift | 81 ++++++++++++++++++ .../InternalPresentation.swift | 24 ++++++ .../Operators/AwaitIdentityOperator.swift | 16 ++++ .../CheckDebuggerPresentationOperator.swift | 2 +- .../CheckNoPaywallAlreadyPresented.swift | 2 +- .../CheckPaywallPresentableOperator.swift | 5 +- .../CheckSubscriptionStatusOperator.swift | 65 +++++++++++++++ .../CheckUserSubscriptionOperator.swift | 2 +- .../HandleTriggerResultOperator.swift | 9 +- .../Operators/PresentationPipelineError.swift | 9 ++ .../Presentation/PublicPresentation.swift | 1 + .../PaywallViewController.swift | 56 ++++++++++--- 16 files changed, 333 insertions(+), 29 deletions(-) create mode 100644 Sources/SuperwallKit/Paywall/Presentation/GetPaywallViewController.swift create mode 100644 Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckSubscriptionStatusOperator.swift diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/AppDelegate.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/AppDelegate.swift index 3cea0354a..fb15da446 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/AppDelegate.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/AppDelegate.swift @@ -1,3 +1,4 @@ +// swiftlint:disable all // // AppDelegate.swift // SuperwallUIKitExample @@ -33,8 +34,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { /// Keep Superwall's subscription status up-to-date with RevenueCat's. purchaseController.syncSubscriptionStatus() - - return true } diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift index c045ed32a..2f122fdde 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift @@ -1,3 +1,4 @@ +// swiftlint:disable all // // TrackEventViewController.swift // SuperwallUIKitExample @@ -76,16 +77,80 @@ final class HomeViewController: UIViewController { handler.onError = { error in print("The paywall presentation failed with error \(error)") } - 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 choose if these are paid features remotely." - ) +// 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 choose if these are paid features remotely." +// ) +// } +// + + + + + + +// +// Superwall.shared.getPaywall(forEvent: "campaign_trigger") { paywall, error in +// guard let paywall = paywall else { +// // TODO: make an async version and add error handling +// // skipped or subscribed +// print("[!] skip", error) +// return +// } +// +// print("[!] got paywall") +// +// // prepare the vc +// paywall.presentationWillBegin() +// +// // present however you like +// self.present(paywall, animated: paywall.presentationIsAnimated) { +// +// // let the paywall know its presented +// paywall.presentationDidFinish() +// } +// +// // get its result +// paywall.onDismiss { state in +// print("[!] paywall state:", state) +// } +// } + + + Task { + + do { + let paywall = try await Superwall.shared.getPaywall(forEvent: "campaign_trigger") + print("[!] got paywall") + + // prepare the vc + paywall.presentationWillBegin() + + // present however you like + self.present(paywall, animated: paywall.presentationIsAnimated) { + // let the paywall know its presented + print("[!] paywall presented") + paywall.presentationDidFinish() + } + + // get its result + paywall.onDismiss { state in + print("[!] paywall state:", state) + } + + } catch let error { + print("[!] skipped because: ", error) + } + } + + + } private func presentAlert(title: String, message: String) { diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/RCPurchaseController.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/RCPurchaseController.swift index aff0c1718..329e7cd65 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/RCPurchaseController.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/RCPurchaseController.swift @@ -34,7 +34,7 @@ final class RCPurchaseController: PurchaseController { // Gets called whenever new CustomerInfo is available let hasActiveSubscription = !customerInfo.entitlements.active.isEmpty // Why? -> https://www.revenuecat.com/docs/entitlements#entitlements if hasActiveSubscription { - Superwall.shared.subscriptionStatus = .active + Superwall.shared.subscriptionStatus = .inactive //.active } else { Superwall.shared.subscriptionStatus = .inactive } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PublicGetTrackResult.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PublicGetTrackResult.swift index 266d8b669..ad95e94e6 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PublicGetTrackResult.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PublicGetTrackResult.swift @@ -83,7 +83,7 @@ extension Superwall { let result = await getPresentationResult(forEvent: event, params: params) completion(result) } - } + } /// Objective-C-only function to preemptively get the result of tracking an event. /// diff --git a/Sources/SuperwallKit/Paywall/Presentation/GetPaywallViewController.swift b/Sources/SuperwallKit/Paywall/Presentation/GetPaywallViewController.swift new file mode 100644 index 000000000..58d97ca35 --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Presentation/GetPaywallViewController.swift @@ -0,0 +1,81 @@ +// +// File.swift +// +// +// Created by Jake Mor on 10/9/21. +// +// swiftlint:disable line_length file_length function_body_length + +import Foundation +import Combine +import UIKit + +extension Superwall { + + public func getPaywall( + forEvent: String, + params: [String: Any]? = nil, + paywallOverrides: PaywallOverrides? = nil + ) async throws -> PaywallViewController { + + return try await withCheckedThrowingContinuation { continuation in + getPaywall(forEvent: forEvent, params: params, paywallOverrides: paywallOverrides) { paywallViewController, error in + if let error = error { + continuation.resume(throwing: error) + } else if let paywallViewController = paywallViewController { + continuation.resume(returning: paywallViewController) + } else { + continuation.resume(throwing: PresentationPipelineError.unknown) + } + } + } + } + + public func getPaywall( + forEvent: String, + params: [String: Any]? = nil, + paywallOverrides: PaywallOverrides? = nil, + completion: @escaping (PaywallViewController?, Error?) -> Void + ) { + + let trackableEvent = UserInitiatedEvent.Track( + rawName: forEvent, + canImplicitlyTriggerPaywall: false, + customParameters: params ?? [:], + isFeatureGatable: false + ) + + Task { + let trackResult = await self.track(trackableEvent) + + let presentationRequest = self.dependencyContainer.makePresentationRequest( + .explicitTrigger(trackResult.data), + paywallOverrides: paywallOverrides, + isPaywallPresented: false + ) + + let paywallStatePublisher: PassthroughSubject = .init() + + let presentablePublisher = self.internallyGetPaywallViewController(presentationRequest, paywallStatePublisher) + + presentablePublisher.subscribe(Subscribers.Sink( + receiveCompletion: { done in + switch done { + case .failure(let error): + completion(nil, error) + default: + break + } + }, + receiveValue: { @MainActor state in + + let paywall = state.paywallViewController + paywall.set(eventData: trackResult.data, presentationStyleOverride: paywallOverrides?.presentationStyle, paywallStatePublisher: paywallStatePublisher) + completion(state.paywallViewController, nil) + } + )) + } + + + } +} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift index 5f006e3b3..0954a8237 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift @@ -13,6 +13,7 @@ typealias PresentationSubject = CurrentValueSubject /// A publisher that emits ``PaywallState`` objects. public typealias PaywallStatePublisher = AnyPublisher +typealias PresentablePipelineOutputPublisher = AnyPublisher extension Superwall { /// Runs a combine pipeline to present a paywall, publishing ``PaywallState`` objects that provide updates on the lifecycle of the paywall. @@ -53,6 +54,29 @@ extension Superwall { .eraseToAnyPublisher() } + + @discardableResult + func internallyGetPaywallViewController( + _ request: PresentationRequest, + _ paywallStatePublisher: PassthroughSubject = .init() + ) -> PresentablePipelineOutputPublisher { + + let presentationSubject = PresentationSubject(request) + + return presentationSubject + .eraseToAnyPublisher() + .waitToPresent() + .logPresentation("Called Superwall.shared.getPaywallViewController") + .evaluateRules() + .confirmHoldoutAssignment() + .handleTriggerResult(paywallStatePublisher) + .getPaywallViewController(pipelineType: .presentation(paywallStatePublisher)) + .checkSubscriptionStatus(paywallStatePublisher) + .confirmPaywallAssignment() + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + /// Presents the paywall again by sending the previous presentation request to the presentation publisher. func presentAgain(cacheKey: String) { guard let lastPresentationItems = presentationItems.last else { diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift index e3054af61..5cd6343f5 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift @@ -29,4 +29,20 @@ extension AnyPublisher where Output == PresentationRequest, Failure == Error { } .eraseToAnyPublisher() } + + /// Waits for config to be received and the identity to be established + func waitUntilReady() -> AnyPublisher { + subscribe(on: DispatchQueue.global(qos: .userInitiated)) + .flatMap { request in + zip( + request.dependencyContainer.identityManager.hasIdentity, + request.dependencyContainer.configManager.hasConfig + ) + } + .first() + .map { request, _, _ in + return request + } + .eraseToAnyPublisher() + } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift index d454a0818..2f7299aab 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift @@ -30,7 +30,7 @@ extension AnyPublisher where Output == (PresentationRequest, DebugInfo), Failure let state: PaywallState = .skipped(.error(error)) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) - throw PresentationPipelineError.cancelled + throw PresentationPipelineError.debuggerPresented } } return (request, debugInfo) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift index 1a3ddc304..aff28b962 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift @@ -40,7 +40,7 @@ extension AnyPublisher where Output == PresentationRequest, Failure == Error { let state: PaywallState = .skipped(.error(error)) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) - throw PresentationPipelineError.cancelled + throw PresentationPipelineError.paywallAlreadyPresented } .eraseToAnyPublisher() } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift index decc67c4b..6a0f24228 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift @@ -29,6 +29,7 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error _ paywallStatePublisher: PassthroughSubject ) -> AnyPublisher { asyncMap { input in + let subscriptionStatus = await input.request.flags.subscriptionStatus.async() if await InternalPresentationLogic.userSubscribedAndNotOverridden( isUserSubscribed: subscriptionStatus == .active, @@ -47,7 +48,7 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error let state: PaywallState = .skipped(.userIsSubscribed) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) - throw PresentationPipelineError.cancelled + throw PresentationPipelineError.userIsSubscribed } if input.request.presenter == nil { @@ -80,7 +81,7 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error let state: PaywallState = .skipped(.error(error)) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) - throw PresentationPipelineError.cancelled + throw PresentationPipelineError.noPresenter } let sessionEventsManager = input.request.dependencyContainer.sessionEventsManager diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckSubscriptionStatusOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckSubscriptionStatusOperator.swift new file mode 100644 index 000000000..48be7a56f --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckSubscriptionStatusOperator.swift @@ -0,0 +1,65 @@ +// +// File.swift +// +// +// Created by Jake Mor on 4/28/23. +// +// swiftlint:disable strict_fileprivate function_body_length + +import UIKit +import Combine + +extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error { + /// Checks conditions for whether the paywall can present before accessing a window on + /// which the paywall can present. + /// + /// - Parameters: + /// - paywallStatePublisher: A `PassthroughSubject` that gets sent ``PaywallState`` objects. + /// + /// - Returns: A publisher that contains info for the next pipeline operator. + func checkSubscriptionStatus( + _ paywallStatePublisher: PassthroughSubject + ) -> AnyPublisher { + asyncMap { input in + + let subscriptionStatus = await input.request.flags.subscriptionStatus.async() + if await InternalPresentationLogic.userSubscribedAndNotOverridden( + isUserSubscribed: subscriptionStatus == .active, + overrides: .init( + isDebuggerLaunched: input.request.flags.isDebuggerLaunched, + shouldIgnoreSubscriptionStatus: input.request.paywallOverrides?.ignoreSubscriptionStatus, + presentationCondition: input.paywallViewController.paywall.presentation.condition + ) + ) { + Task.detached(priority: .utility) { + let trackedEvent = InternalSuperwallEvent.UnableToPresent( + state: .userIsSubscribed + ) + await Superwall.shared.track(trackedEvent) + } + let state: PaywallState = .skipped(.userIsSubscribed) + paywallStatePublisher.send(state) + paywallStatePublisher.send(completion: .finished) + throw PresentationPipelineError.userIsSubscribed + } + + + let sessionEventsManager = input.request.dependencyContainer.sessionEventsManager + await sessionEventsManager?.triggerSession.activateSession( + for: input.request.presentationInfo, + on: input.request.presenter, + paywall: input.paywallViewController.paywall, + triggerResult: input.triggerResult + ) + + return await PresentablePipelineOutput( + request: input.request, + debugInfo: input.debugInfo, + paywallViewController: input.paywallViewController, + presenter: UIViewController(), // TODO: Fic this + confirmableAssignment: input.confirmableAssignment + ) + } + .eraseToAnyPublisher() + } +} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperator.swift index b50c28232..8a2da45c6 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperator.swift @@ -27,7 +27,7 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro } paywallStatePublisher.send(.skipped(.userIsSubscribed)) paywallStatePublisher.send(completion: .finished) - throw PresentationPipelineError.cancelled + throw PresentationPipelineError.userIsSubscribed } return input } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift index 6ed75b06a..f539779f2 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift @@ -29,6 +29,9 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro _ paywallStatePublisher: PassthroughSubject ) -> AnyPublisher { asyncMap { input in + + var errorType: PresentationPipelineError = .cancelled + switch input.triggerResult { case .paywall(let experiment): return TriggerResultResponsePipelineOutput( @@ -51,6 +54,7 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro ) await Superwall.shared.track(trackedEvent) } + errorType = .holdout paywallStatePublisher.send(.skipped(.holdout(experiment))) case .noRuleMatch: let sessionEventsManager = input.request.dependencyContainer.sessionEventsManager @@ -63,12 +67,14 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noRuleMatch) await Superwall.shared.track(trackedEvent) } + errorType = .noRuleMatch paywallStatePublisher.send(.skipped(.noRuleMatch)) case .eventNotFound: Task.detached(priority: .utility) { let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .eventNotFound) await Superwall.shared.track(trackedEvent) } + errorType = .eventNotFound paywallStatePublisher.send(.skipped(.eventNotFound)) case let .error(error): Logger.debug( @@ -82,11 +88,12 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noPaywallViewController) await Superwall.shared.track(trackedEvent) } + errorType = .noPaywallViewController paywallStatePublisher.send(.skipped(.error(error))) } paywallStatePublisher.send(completion: .finished) - throw PresentationPipelineError.cancelled + throw errorType } .eraseToAnyPublisher() } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentationPipelineError.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentationPipelineError.swift index c497e93f4..54e6d03ae 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentationPipelineError.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentationPipelineError.swift @@ -8,5 +8,14 @@ import Foundation enum PresentationPipelineError: Error { + case debuggerPresented + case paywallAlreadyPresented + case userIsSubscribed + case holdout + case noRuleMatch + case eventNotFound + case noPaywallViewController + case noPresenter case cancelled + case unknown } diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index 0b33f141e..27aa7e8df 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -495,6 +495,7 @@ extension Superwall { .eraseToAnyPublisher() } + /// Converts dismissal result from enums with associated values, to old objective-c compatible way /// /// - Parameters: diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 6cf17f5a9..133d2953e 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -36,9 +36,9 @@ enum PaywallLoadingState { case ready } -class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegate { +public class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegate { // MARK: - Internal Properties - override var preferredStatusBarStyle: UIStatusBarStyle { + override public var preferredStatusBarStyle: UIStatusBarStyle { if let isDark = view.backgroundColor?.isDarkColor, isDark { return .lightContent } @@ -85,7 +85,7 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat /// the caller of ``Superwall/track(event:params:paywallOverrides:paywallHandler:)`` /// /// This publisher is set on presentation of the paywall. - private var paywallStatePublisher: PassthroughSubject! + public var paywallStatePublisher: PassthroughSubject! /// Defines whether the view controller is being presented or not. private var isPresented = false @@ -103,7 +103,7 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat private var presentationStyle: PaywallPresentationStyle /// Defines whether the presentation should animate based on the presentation style. - private var presentationIsAnimated: Bool { + public var presentationIsAnimated: Bool { return presentationStyle != .fullscreenNoAnimation } @@ -179,7 +179,7 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat fatalError("init(coder:) has not been implemented") } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() configureUI() loadPaywallWebpage() @@ -453,6 +453,42 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat // MARK: - Presentation Logic + func set( + eventData: EventData?, + presentationStyleOverride: PaywallPresentationStyle?, + paywallStatePublisher: PassthroughSubject = .init() + ) { + self.eventData = eventData + self.paywallStatePublisher = paywallStatePublisher + setPresentationStyle(withOverride: presentationStyleOverride) + } + + public func presentationWillBegin() { + addShimmerView(onPresent: true) + prepareForPresentation() + if loadingState == .ready { + webView.messageHandler.handle(.templateParamsAndUserAttributes) + } + } + + public func onDismiss ( + _ handler: @escaping (_ state: DismissState) -> Void + ) { + paywallStatePublisher.subscribe(Subscribers.Sink( + receiveCompletion: { _ in }, + receiveValue: { paywallState in + switch paywallState { + case .dismissed(_, let dismissState): + handler(dismissState) + default: + break + } + } + )) + } + + + func present( on presenter: UIViewController, eventData: EventData?, @@ -530,7 +566,7 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat Superwall.shared.dependencyContainer.delegateAdapter.willPresentPaywall(withInfo: paywallInfo) } - private func presentationDidFinish() { + public func presentationDidFinish() { isPresented = true Superwall.shared.dependencyContainer.delegateAdapter.didPresentPaywall(withInfo: paywallInfo) Task(priority: .utility) { @@ -614,7 +650,7 @@ extension PaywallViewController: PaywallMessageHandlerDelegate { // MARK: - View Lifecycle extension PaywallViewController { - override func viewWillAppear(_ animated: Bool) { + override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) guard isActive else { return @@ -632,7 +668,7 @@ extension PaywallViewController { } } - override func viewWillDisappear(_ animated: Bool) { + override public func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) guard isPresented else { return @@ -646,7 +682,7 @@ extension PaywallViewController { willDismiss() } - override func viewDidDisappear(_ animated: Bool) { + override public func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) guard isPresented else { return @@ -729,7 +765,7 @@ extension PaywallViewController { // MARK: - SFSafariViewControllerDelegate extension PaywallViewController: SFSafariViewControllerDelegate { - func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + public func safariViewControllerDidFinish(_ controller: SFSafariViewController) { isSafariVCPresented = false } } From 9a4fc735658a3d24b797e8c87f2238588841817e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Fri, 28 Apr 2023 18:11:14 +0100 Subject: [PATCH 02/27] Changes the way getPaywall is created --- .../HomeViewController.swift | 1 - .../RCPurchaseController.swift | 2 +- .../PublicGetTrackResult.swift | 2 +- .../GetPaywallViewController.swift | 78 +++++++------------ .../InternalPresentation.swift | 13 +++- .../CheckPaywallPresentableOperator.swift | 1 - .../HandleTriggerResultOperator.swift | 7 +- 7 files changed, 46 insertions(+), 58 deletions(-) diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift index e14d46f54..77fe2fd17 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift @@ -1,4 +1,3 @@ -// swiftlint:disable all // // TrackEventViewController.swift // SuperwallUIKitExample diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/RCPurchaseController.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/RCPurchaseController.swift index 5156a3f61..7b1ec43e4 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/RCPurchaseController.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/RCPurchaseController.swift @@ -32,7 +32,7 @@ final class RCPurchaseController: PurchaseController { // Gets called whenever new CustomerInfo is available let hasActiveSubscription = !customerInfo.entitlements.active.isEmpty // Why? -> https://www.revenuecat.com/docs/entitlements#entitlements if hasActiveSubscription { - Superwall.shared.subscriptionStatus = .inactive //.active + Superwall.shared.subscriptionStatus = .active } else { Superwall.shared.subscriptionStatus = .inactive } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PublicGetTrackResult.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PublicGetTrackResult.swift index ad95e94e6..266d8b669 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PublicGetTrackResult.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PublicGetTrackResult.swift @@ -83,7 +83,7 @@ extension Superwall { let result = await getPresentationResult(forEvent: event, params: params) completion(result) } - } + } /// Objective-C-only function to preemptively get the result of tracking an event. /// diff --git a/Sources/SuperwallKit/Paywall/Presentation/GetPaywallViewController.swift b/Sources/SuperwallKit/Paywall/Presentation/GetPaywallViewController.swift index 58d97ca35..9ac303b80 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/GetPaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/GetPaywallViewController.swift @@ -11,22 +11,22 @@ import Combine import UIKit extension Superwall { - public func getPaywall( - forEvent: String, - params: [String: Any]? = nil, - paywallOverrides: PaywallOverrides? = nil - ) async throws -> PaywallViewController { - - return try await withCheckedThrowingContinuation { continuation in - getPaywall(forEvent: forEvent, params: params, paywallOverrides: paywallOverrides) { paywallViewController, error in - if let error = error { - continuation.resume(throwing: error) - } else if let paywallViewController = paywallViewController { - continuation.resume(returning: paywallViewController) - } else { - continuation.resume(throwing: PresentationPipelineError.unknown) - } + forEvent event: String, + params: [String: Any]? = nil, + paywallOverrides: PaywallOverrides? = nil, + completion: @escaping (Result) -> Void + ) { + Task { + do { + let paywallViewController = try await getPaywall( + forEvent: event, + params: params, + paywallOverrides: paywallOverrides + ) + completion(.success(paywallViewController)) + } catch { + completion(.failure(error)) } } } @@ -34,20 +34,19 @@ extension Superwall { public func getPaywall( forEvent: String, params: [String: Any]? = nil, - paywallOverrides: PaywallOverrides? = nil, - completion: @escaping (PaywallViewController?, Error?) -> Void - ) { - - let trackableEvent = UserInitiatedEvent.Track( - rawName: forEvent, - canImplicitlyTriggerPaywall: false, - customParameters: params ?? [:], - isFeatureGatable: false - ) - - Task { + paywallOverrides: PaywallOverrides? = nil + ) async throws -> PaywallViewController { + return try await Future { + let trackableEvent = UserInitiatedEvent.Track( + rawName: forEvent, + canImplicitlyTriggerPaywall: false, + customParameters: params ?? [:], + isFeatureGatable: false + ) let trackResult = await self.track(trackableEvent) - + return trackResult + } + .flatMap { trackResult in let presentationRequest = self.dependencyContainer.makePresentationRequest( .explicitTrigger(trackResult.data), paywallOverrides: paywallOverrides, @@ -56,26 +55,9 @@ extension Superwall { let paywallStatePublisher: PassthroughSubject = .init() - let presentablePublisher = self.internallyGetPaywallViewController(presentationRequest, paywallStatePublisher) - - presentablePublisher.subscribe(Subscribers.Sink( - receiveCompletion: { done in - switch done { - case .failure(let error): - completion(nil, error) - default: - break - } - }, - receiveValue: { @MainActor state in - - let paywall = state.paywallViewController - paywall.set(eventData: trackResult.data, presentationStyleOverride: paywallOverrides?.presentationStyle, paywallStatePublisher: paywallStatePublisher) - completion(state.paywallViewController, nil) - } - )) + return self.internallyGetPaywallViewController(presentationRequest, paywallStatePublisher) } - - + .eraseToAnyPublisher() + .throwableAsync() } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift index 7a4400b2f..ae91ab326 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift @@ -54,12 +54,11 @@ extension Superwall { .eraseToAnyPublisher() } - @discardableResult func internallyGetPaywallViewController( _ request: PresentationRequest, _ paywallStatePublisher: PassthroughSubject = .init() - ) -> PresentablePipelineOutputPublisher { + ) -> AnyPublisher { let presentationSubject = PresentationSubject(request) @@ -73,7 +72,17 @@ extension Superwall { .getPaywallViewController(pipelineType: .presentation(paywallStatePublisher)) .checkSubscriptionStatus(paywallStatePublisher) .confirmPaywallAssignment() + .storePresentationObjects(presentationSubject, paywallStatePublisher) .receive(on: DispatchQueue.main) + .map { input in + let paywallViewController = input.paywallViewController + paywallViewController.set( + eventData: input.request.presentationInfo.eventData, + presentationStyleOverride: input.request.paywallOverrides?.presentationStyle, + paywallStatePublisher: paywallStatePublisher + ) + return input.paywallViewController + } .eraseToAnyPublisher() } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift index 6a0f24228..1af33c487 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift @@ -29,7 +29,6 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error _ paywallStatePublisher: PassthroughSubject ) -> AnyPublisher { asyncMap { input in - let subscriptionStatus = await input.request.flags.subscriptionStatus.async() if await InternalPresentationLogic.userSubscribedAndNotOverridden( isUserSubscribed: subscriptionStatus == .active, diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift index f539779f2..add28c0f9 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift @@ -29,7 +29,6 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro _ paywallStatePublisher: PassthroughSubject ) -> AnyPublisher { asyncMap { input in - var errorType: PresentationPipelineError = .cancelled switch input.triggerResult { @@ -67,14 +66,14 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noRuleMatch) await Superwall.shared.track(trackedEvent) } - errorType = .noRuleMatch + errorType = .noRuleMatch paywallStatePublisher.send(.skipped(.noRuleMatch)) case .eventNotFound: Task.detached(priority: .utility) { let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .eventNotFound) await Superwall.shared.track(trackedEvent) } - errorType = .eventNotFound + errorType = .eventNotFound paywallStatePublisher.send(.skipped(.eventNotFound)) case let .error(error): Logger.debug( @@ -88,7 +87,7 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noPaywallViewController) await Superwall.shared.track(trackedEvent) } - errorType = .noPaywallViewController + errorType = .noPaywallViewController paywallStatePublisher.send(.skipped(.error(error))) } From 0d66c51532e2f00ddfa5372a084ae2c2e793f677 Mon Sep 17 00:00:00 2001 From: jakemor Date: Tue, 2 May 2023 00:36:52 +0200 Subject: [PATCH 03/27] removes new dismissal enum in favor of CloseReason on paywallInfo --- CHANGELOG.md | 2 +- .../Presentation State/PaywallState.swift | 5 ----- .../Presentation/PaywallCloseReason.swift | 20 +++++++++++++++++++ .../Paywall/Presentation/PaywallInfo.swift | 3 +++ .../Presentation/PublicPresentation.swift | 10 ++++------ .../PaywallViewController.swift | 3 ++- 6 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c7aeb18..3b6a08d7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup - You no longer need to have swiftlint installed to run our example apps. - If you're not using a `PurchaseController` and a user comes across the "You're already subscribed to this product" popup, we will now correctly identify this as a restoration and not a purchase. This can happen when testing in sandbox if you purchase a product -> delete and reinstall the app -> open a paywall and purchase. - Adds static variable `Superwall.isInitialized` which is `true` when initialization is complete and `Superwall.shared` can be accessed. -- Adds `transaction_abandon` and `transaction_fail` as potential triggers. This comes with a new `DismissState` case `closedForNextPaywall`, which is returned when dismissing one paywall for another. +- Adds `transaction_abandon` and `transaction_fail` as potential triggers. This comes with a new `PaywallInfo` property `closeReason`, which can either be `nil`, `.userInteraction`, or `.forNextPaywall`. ### Fixes diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift index 3d58609ff..ebce8ed08 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift @@ -18,10 +18,6 @@ public enum DismissState: Equatable, Sendable { /// The paywall was dismissed by the user manually pressing the close button. case closed - /// The paywall was dismissed so that another paywall can be shown. This happens when - /// `transaction_abandon` or `transaction_fail` is added as a trigger. - case closedForNextPaywall - /// The paywall was dismissed due to the user restoring their purchases. case restored } @@ -31,7 +27,6 @@ public enum DismissState: Equatable, Sendable { public enum DismissStateObjc: Int, Sendable { case purchased case closed - case closedForNextPaywall case restored } diff --git a/Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift b/Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift new file mode 100644 index 000000000..b8e5c4626 --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift @@ -0,0 +1,20 @@ +// +// File.swift +// +// +// Created by Jake Mor on 4/29/23. +// + +import Foundation + +/// An enum whose cases indicate whether the paywall was closed by user +/// interaction or for another paywall to show +@objc(SWKPaywallCloseReason) +public enum PaywallCloseReason: Int, Codable { + case userInteraction + + /// Prevents the ``Superwall/register(event:params:handler:feature:)`` `feature` + /// block from executing on dismiss of the paywall, because another paywall is set to show + case forNextPaywall +} + diff --git a/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift b/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift index 4a15f283f..60beafa7c 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift @@ -29,6 +29,9 @@ public final class PaywallInfo: NSObject { /// An experiment is a set of paywall variants determined by probabilities. An experiment will result in a user seeing a paywall unless they are in a holdout group. public let experiment: Experiment? + /// An enum describing why this paywall was last closed. `nil` if not yet closed. + public var closeReason: PaywallCloseReason? + /// The products associated with the paywall. public let products: [Product] diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index 3c81a7e34..1969f6392 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -53,10 +53,11 @@ extension Superwall { guard let paywallViewController = paywallViewController else { return } + paywallViewController.paywallInfo.closeReason = .forNextPaywall await withCheckedContinuation { continuation in dismiss( paywallViewController, - state: .closedForNextPaywall, + state: .closed, // .closedForNextPaywall shouldSendDismissedState: true, shouldCompleteStatePublisher: false ) { @@ -467,14 +468,13 @@ extension Superwall { completion?() } case .closed: + let closeReason = paywallInfo.closeReason let featureGating = paywallInfo.featureGatingBehavior - if featureGating == .nonGated { + if closeReason != .forNextPaywall && featureGating == .nonGated { DispatchQueue.main.async { completion?() } } - case .closedForNextPaywall: - break } case .skipped(let reason): switch reason { @@ -552,8 +552,6 @@ extension Superwall { switch state { case .closed: completion(.closed, nil, paywallInfo) - case .closedForNextPaywall: - completion(.closedForNextPaywall, nil, paywallInfo) case .purchased(productId: let productId): completion(.purchased, productId, paywallInfo) case .restored: diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 73d755648..c21be22aa 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -468,6 +468,7 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat addShimmerView(onPresent: true) prepareForPresentation() + self.eventData = eventData self.paywallStatePublisher = paywallStatePublisher @@ -526,7 +527,7 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat view.alpha = 1.0 view.transform = .identity webView.scrollView.contentOffset = CGPoint.zero - + paywallInfo.closeReason = nil Superwall.shared.dependencyContainer.delegateAdapter.willPresentPaywall(withInfo: paywallInfo) } From 0199d4908c963d359cd4ae528312ac12cfb6d3ff Mon Sep 17 00:00:00 2001 From: jakemor Date: Tue, 2 May 2023 01:24:16 +0200 Subject: [PATCH 04/27] bug fixes --- Sources/SuperwallKit/Models/Paywall/Paywall.swift | 5 ++++- .../Paywall/Presentation/PaywallInfo.swift | 10 ++++++---- .../Paywall/Presentation/PublicPresentation.swift | 4 +++- .../View Controller/PaywallViewController.swift | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Sources/SuperwallKit/Models/Paywall/Paywall.swift b/Sources/SuperwallKit/Models/Paywall/Paywall.swift index c1cec1b59..95713bdd2 100644 --- a/Sources/SuperwallKit/Models/Paywall/Paywall.swift +++ b/Sources/SuperwallKit/Models/Paywall/Paywall.swift @@ -84,6 +84,8 @@ struct Paywall: Decodable { /// Determines whether a free trial is available or not. var isFreeTrialAvailable = false + var closeReason: PaywallCloseReason? = nil + /// Determines whether a paywall executes the /// ``Superwall/register(event:params:handler:feature:)`` feature block if the /// user does not purchase. @@ -235,7 +237,8 @@ struct Paywall: Decodable { paywalljsVersion: paywalljsVersion, isFreeTrialAvailable: isFreeTrialAvailable, sessionEventsManager: sessionEventsManager, - featureGatingBehavior: featureGating + featureGatingBehavior: featureGating, + closeReason: closeReason ) } diff --git a/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift b/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift index 60beafa7c..25c8a9fd0 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift @@ -29,9 +29,6 @@ public final class PaywallInfo: NSObject { /// An experiment is a set of paywall variants determined by probabilities. An experiment will result in a user seeing a paywall unless they are in a holdout group. public let experiment: Experiment? - /// An enum describing why this paywall was last closed. `nil` if not yet closed. - public var closeReason: PaywallCloseReason? - /// The products associated with the paywall. public let products: [Product] @@ -99,6 +96,9 @@ public final class PaywallInfo: NSObject { public let featureGatingBehavior: FeatureGatingBehavior + /// An enum describing why this paywall was last closed. `nil` if not yet closed. + public let closeReason: PaywallCloseReason? + private unowned let sessionEventsManager: SessionEventsManager init( @@ -121,7 +121,8 @@ public final class PaywallInfo: NSObject { paywalljsVersion: String? = nil, isFreeTrialAvailable: Bool, sessionEventsManager: SessionEventsManager, - featureGatingBehavior: FeatureGatingBehavior = .nonGated + featureGatingBehavior: FeatureGatingBehavior = .nonGated, + closeReason: PaywallCloseReason? = nil ) { self.databaseId = databaseId self.identifier = identifier @@ -176,6 +177,7 @@ public final class PaywallInfo: NSObject { self.productsLoadDuration = nil } self.sessionEventsManager = sessionEventsManager + self.closeReason = closeReason } func eventParams( diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index 1969f6392..9fde4d3b5 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -38,6 +38,7 @@ extension Superwall { guard let paywallViewController = paywallViewController else { return } + paywallViewController.paywall.closeReason = .userInteraction await withCheckedContinuation { continuation in dismiss( paywallViewController, @@ -53,8 +54,9 @@ extension Superwall { guard let paywallViewController = paywallViewController else { return } - paywallViewController.paywallInfo.closeReason = .forNextPaywall + await withCheckedContinuation { continuation in + paywallViewController.paywall.closeReason = .forNextPaywall dismiss( paywallViewController, state: .closed, // .closedForNextPaywall diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index c21be22aa..fd737a5ba 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -527,7 +527,7 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat view.alpha = 1.0 view.transform = .identity webView.scrollView.contentOffset = CGPoint.zero - paywallInfo.closeReason = nil + paywall.closeReason = nil Superwall.shared.dependencyContainer.delegateAdapter.willPresentPaywall(withInfo: paywallInfo) } From 6616edb242b4516221933cfe7feb1e20589eb154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Tue, 2 May 2023 10:35:52 +0100 Subject: [PATCH 05/27] Updated documentation --- .../Subscription Controller/PurchaseController.swift | 4 ++-- .../Subscription Controller/PurchaseControllerObjc.swift | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/SuperwallKit/Delegate/Subscription Controller/PurchaseController.swift b/Sources/SuperwallKit/Delegate/Subscription Controller/PurchaseController.swift index 3ef41014d..53976cc08 100644 --- a/Sources/SuperwallKit/Delegate/Subscription Controller/PurchaseController.swift +++ b/Sources/SuperwallKit/Delegate/Subscription Controller/PurchaseController.swift @@ -38,8 +38,8 @@ public protocol PurchaseController: AnyObject { /// Add your restore logic here, making sure that the user's subscription status is updated after restore, /// and return its result. /// - /// - Returns: A boolean that's `true` if the user's purchases were restored or `false` if they weren't. - /// Note: `true` does not imply the user has an active subscription, it just mean the restore had no errors + /// - Returns: A ``RestorationResult`` that's `.restored` if the user's purchases were restored or `.failed(Error?)` if they weren't. + /// **Note**: `restored` does not imply the user has an active subscription, it just mean the restore had no errors. @MainActor func restorePurchases() async -> RestorationResult } diff --git a/Sources/SuperwallKit/Delegate/Subscription Controller/PurchaseControllerObjc.swift b/Sources/SuperwallKit/Delegate/Subscription Controller/PurchaseControllerObjc.swift index 06fdfbc1c..381823f5b 100644 --- a/Sources/SuperwallKit/Delegate/Subscription Controller/PurchaseControllerObjc.swift +++ b/Sources/SuperwallKit/Delegate/Subscription Controller/PurchaseControllerObjc.swift @@ -41,10 +41,11 @@ public protocol PurchaseControllerObjc: AnyObject { /// Called when the user initiates a restore. /// /// Add your restore logic here, making sure that the user's subscription status is updated after restore, - /// and return its result. + /// and call the completion block. /// /// - Parameters: - /// - completion: Call the completion with `true` if the user's purchases were restored or `false` if they weren't. + /// - completion: A completion block that accepts two objects. 1. A ``RestorationResultObjc`` that's `.restored` if the user's purchases were restored or `.failed` if they weren't. 2. An optional error that you can return when the restore failed. + /// **Note**: `restored` does not imply the user has an active subscription, it just mean the restore had no errors. @MainActor @objc func restorePurchases(completion: @escaping (RestorationResultObjc, Error?) -> Void) } From b19af61842ef8eda17cf5a4939d96873b351e2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Tue, 2 May 2023 10:46:10 +0100 Subject: [PATCH 06/27] Paywalls now show even if products are missing --- CHANGELOG.md | 6 ++++++ README.md | 2 +- Sources/SuperwallKit/Misc/Constants.swift | 2 +- .../Products/ProductsFetcherSK1.swift | 21 ++++++------------- SuperwallKit.podspec | 2 +- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c7aeb18..82d998b13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall-me/Superwall-iOS/releases) on GitHub. +## 3.0.0-rc.4 + +### Fixes + +- Paywalls will now show even if you are missing products. + ## 3.0.0-rc.3 ### Breaking Changes diff --git a/README.md b/README.md index 141480963..39524695d 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ The preferred installation method is with [Swift Package Manager](https://swift. To include the *Superwall* SDK in your app, add the following to your Podfile: ``` -pod 'SuperwallKit', '3.0.0-rc.3' +pod 'SuperwallKit', '3.0.0-rc.4' ``` If you don't want to use the v3 beta, you'll need to add this instead: diff --git a/Sources/SuperwallKit/Misc/Constants.swift b/Sources/SuperwallKit/Misc/Constants.swift index 8064c9be7..c14390a35 100644 --- a/Sources/SuperwallKit/Misc/Constants.swift +++ b/Sources/SuperwallKit/Misc/Constants.swift @@ -18,5 +18,5 @@ let sdkVersion = """ */ let sdkVersion = """ -3.0.0-rc.3 +3.0.0-rc.4 """ diff --git a/Sources/SuperwallKit/StoreKit/Products/ProductsFetcherSK1.swift b/Sources/SuperwallKit/StoreKit/Products/ProductsFetcherSK1.swift index f39354fdd..f75b04414 100644 --- a/Sources/SuperwallKit/StoreKit/Products/ProductsFetcherSK1.swift +++ b/Sources/SuperwallKit/StoreKit/Products/ProductsFetcherSK1.swift @@ -21,9 +21,6 @@ class ProductsFetcherSK1: NSObject, ProductsFetcher { private var paywallNameByRequest: [SKRequest: String] = [:] typealias ProductRequestCompletionBlock = (Result, Error>) -> Void private var completionHandlers: [Set: [ProductRequestCompletionBlock]] = [:] - enum ProductError: Error { - case didNotRetrieve - } // MARK: - ProductsFetcher /// Gets StoreKit 1 products from identifiers. @@ -159,7 +156,7 @@ extension ProductsFetcherSK1: SKProductsRequestDelegate { self.productsByRequest.removeValue(forKey: request) if response.products.isEmpty, - !requestProducts.isEmpty { + !requestProducts.isEmpty { var errorMessage = "Could not load products" if let paywallName = paywallNameByRequest[request] { errorMessage += " from paywall \"\(paywallName)\"" @@ -170,18 +167,12 @@ extension ProductsFetcherSK1: SKProductsRequestDelegate { message: "\(errorMessage). Visit https://superwall.com/l/missing-products to diagnose.", info: ["product_ids": requestProducts.description] ) - for completion in completionBlocks { - DispatchQueue.main.async { - completion(.failure(ProductError.didNotRetrieve)) - } - } - } else { - self.cacheProducts(response.products) + } + self.cacheProducts(response.products) - for completion in completionBlocks { - DispatchQueue.main.async { - completion(.success(Set(response.products))) - } + for completion in completionBlocks { + DispatchQueue.main.async { + completion(.success(Set(response.products))) } } self.paywallNameByRequest.removeValue(forKey: request) diff --git a/SuperwallKit.podspec b/SuperwallKit.podspec index c7e075f12..58fdccf7e 100644 --- a/SuperwallKit.podspec +++ b/SuperwallKit.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "SuperwallKit" - s.version = "3.0.0-rc.3" + s.version = "3.0.0-rc.4" 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 204ee8d3f0bf3c47a6641d187952882478ddd161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Tue, 2 May 2023 18:48:24 +0100 Subject: [PATCH 07/27] Bug fixes - Changes DismissState to PaywallResult - Sets default logging level to INFO - Cleans up some code - Prints out errors (although this needs changing) - Adds sendable to some structs - Adds missing documentation --- CHANGELOG.md | 13 +- .../AppDelegate.swift | 1 - .../HomeViewController.swift | 82 +++++------ .../Config/Options/SuperwallOptions.swift | 2 +- .../Extensions/SuperwallExtension.md | 9 +- Sources/SuperwallKit/Logger/Logger.swift | 3 +- .../SuperwallKit/Models/Paywall/Paywall.swift | 1 + .../Models/Paywall/PaywallProducts.swift | 17 ++- .../InternalGetPaywallViewController.swift | 38 +++++ .../CheckSubscriptionStatusOperator.swift | 5 +- ...ExtractPaywallViewControllerOperator.swift | 33 +++++ .../PublicGetPaywallViewController.swift} | 39 ++++- .../InternalPresentation.swift | 41 +----- .../Operators/AwaitIdentityOperator.swift | 20 +-- .../CheckNoPaywallAlreadyPresented.swift | 1 + .../CheckPaywallPresentableOperator.swift | 2 +- .../ConfirmPaywallAssignmentOperator.swift | 2 +- .../Operators/GetPaywallVcOperator.swift | 1 - .../Operators/PresentPaywallOperator.swift | 9 +- .../Operators/PrintErrorsOperator.swift | 27 ++++ .../StorePresentationObjectsOperator.swift | 2 +- .../PaywallOverrides.swift | 2 +- .../Presentation State/PaywallState.swift | 12 +- .../Presentation/PaywallCloseReason.swift | 10 +- .../Paywall/Presentation/PaywallInfo.swift | 4 + .../PaywallPresentationHandler.swift | 30 +++- .../Presentation/PublicPresentation.swift | 24 ++-- .../PaywallViewController.swift | 136 ++++++++++-------- .../StoreProduct/PriceFormatterProvider.swift | 68 +++++---- .../StoreProduct/StoreProduct.swift | 2 +- .../StoreProduct/StoreProductType.swift | 2 +- .../StoreKit/StoreKitManager.swift | 2 +- .../Transactions/RestorationManager.swift | 2 +- .../Transactions/TransactionManager.swift | 2 +- Sources/SuperwallKit/Superwall.swift | 2 +- .../CheckForPaywallResultTests.swift | 2 +- 36 files changed, 396 insertions(+), 252 deletions(-) create mode 100644 Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywallViewController.swift rename Sources/SuperwallKit/Paywall/Presentation/{Internal Presentation => Get Paywall}/Operators/CheckSubscriptionStatusOperator.swift (95%) create mode 100644 Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/ExtractPaywallViewControllerOperator.swift rename Sources/SuperwallKit/Paywall/Presentation/{GetPaywallViewController.swift => Get Paywall/PublicGetPaywallViewController.swift} (51%) create mode 100644 Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PrintErrorsOperator.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 914c73e49..e3a70bce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup ## 3.0.0-rc.4 +### Breaking Changes + +- Changes `DismissState` to `PaywallResult` +- Removes the `closedForNextPaywall` `PaywallResult` and moves it to a new `PaywallInfo` property `closeReason`, which can either be `nil`, `.userInteraction`, or `.forNextPaywall`. +- Changes the `PaywallPresentationHandler` variables to functions. +- Removes `Superwall.shared.track`. We're going all in on `Superwall.shared.register` baby! + +### Enhancements + +- Sets default logging level to `INFO`. + ### Fixes - Paywalls will now show even if you are missing products. @@ -28,7 +39,7 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup - You no longer need to have swiftlint installed to run our example apps. - If you're not using a `PurchaseController` and a user comes across the "You're already subscribed to this product" popup, we will now correctly identify this as a restoration and not a purchase. This can happen when testing in sandbox if you purchase a product -> delete and reinstall the app -> open a paywall and purchase. - Adds static variable `Superwall.isInitialized` which is `true` when initialization is complete and `Superwall.shared` can be accessed. -- Adds `transaction_abandon` and `transaction_fail` as potential triggers. This comes with a new `PaywallInfo` property `closeReason`, which can either be `nil`, `.userInteraction`, or `.forNextPaywall`. +- Adds `transaction_abandon` and `transaction_fail` as potential triggers. This comes with a new `DismissState` case `closedForNextPaywall`, which is returned when dismissing one paywall for another. ### Fixes diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/AppDelegate.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/AppDelegate.swift index c028a9b6d..026c16097 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/AppDelegate.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/AppDelegate.swift @@ -1,4 +1,3 @@ -// swiftlint:disable all // // AppDelegate.swift // SuperwallUIKitExample diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift index 77fe2fd17..9b3287a46 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift @@ -65,26 +65,26 @@ final class HomeViewController: UIViewController { @IBAction private func launchFeature() { let handler = PaywallPresentationHandler() - handler.onDismiss = { paywallInfo in + handler.onDismiss { paywallInfo in print("The paywall dismissed. PaywallInfo:", paywallInfo) } - handler.onPresent = { paywallInfo in + handler.onPresent { paywallInfo in print("The paywall presented. PaywallInfo:", paywallInfo) } - handler.onError = { error in + handler.onError { error in print("The paywall presentation failed with error \(error)") } -// 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 choose if these are paid features remotely." -// ) -// } -// + 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 choose if these are paid features remotely." + ) + } + @@ -118,33 +118,33 @@ final class HomeViewController: UIViewController { // } // } - - Task { - - do { - let paywall = try await Superwall.shared.getPaywall(forEvent: "campaign_trigger") - print("[!] got paywall") - - // prepare the vc - paywall.presentationWillBegin() - - // present however you like - self.present(paywall, animated: paywall.presentationIsAnimated) { - // let the paywall know its presented - print("[!] paywall presented") - paywall.presentationDidFinish() - } - - // get its result - paywall.onDismiss { state in - print("[!] paywall state:", state) - } - - } catch let error { - print("[!] skipped because: ", error) - } - - } +// +// Task { +// +// do { +// let paywall = try await Superwall.shared.getPaywallViewController(forEvent: "campaign_trigger") +// print("[!] got paywall") +// +// // prepare the vc +// paywall.presentationWillBegin() +// +// // present however you like +// self.present(paywall, animated: paywall.presentationIsAnimated) { +// // let the paywall know its presented +// print("[!] paywall presented") +// paywall.presentationDidFinish() +// } +// +// // get its result +// paywall.onDismiss { state in +// print("[!] paywall state:", state) +// } +// +// } catch let error { +// print("[!] skipped because: ", error) +// } +// +// } diff --git a/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift b/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift index f204dd8a9..42e9a1f63 100644 --- a/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift +++ b/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift @@ -78,7 +78,7 @@ public final class SuperwallOptions: NSObject { /// Configuration for printing to the console. public final class Logging: NSObject { /// Defines the minimum log level to print to the console. Defaults to `warn`. - public var level: LogLevel = .warn + public var level: LogLevel = .info /// Defines the scope of logs to print to the console. Defaults to .all. public var scopes: Set = [.all] diff --git a/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md b/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md index dc5d1fa08..14f885098 100644 --- a/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md +++ b/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md @@ -30,13 +30,8 @@ The ``Superwall`` class is used to access all the features of the SDK. Before us - ``register(event:params:handler:)`` - ``register(event:params:handler:feature:)`` -- ``track(event:params:paywallOverrides:paywallHandler:)`` -- ``track(event:params:products:ignoreSubscriptionStatus:presentationStyleOverride:onSkip:onPresent:onDismiss:)`` -- ``track(event:)`` -- ``track(event:params:)`` -- ``track(event:onSkip:onPresent:onDismiss:)`` -- ``track(event:params:onSkip:onPresent:onDismiss:)`` -- ``publisher(forEvent:params:paywallOverrides:isFeatureGatable:)`` +- ``getPaywallViewController(forEvent:params:paywallOverrides:)`` +- ``getPaywallViewController(forEvent:params:paywallOverrides:completion:)`` - ``getPresentationResult(forEvent:)`` - ``getPresentationResult(forEvent:params:)-9ivi6`` - ``getPresentationResult(forEvent:params:)-60qtr`` diff --git a/Sources/SuperwallKit/Logger/Logger.swift b/Sources/SuperwallKit/Logger/Logger.swift index 307e1d323..59d5ae8dd 100644 --- a/Sources/SuperwallKit/Logger/Logger.swift +++ b/Sources/SuperwallKit/Logger/Logger.swift @@ -95,12 +95,11 @@ enum Logger: Loggable { return } - var name = "\n\(Date().isoString) \(logLevel.descriptionEmoji) [Superwall] [\(scope.description)] - \(logLevel.description)" + var name = "\(Date().isoString) \(logLevel.descriptionEmoji) [Superwall] [\(scope.description)] - \(logLevel.description)" if let message = message { name += ": \(message)" } - name += "\n" if dumping.isEmpty { print(name) diff --git a/Sources/SuperwallKit/Models/Paywall/Paywall.swift b/Sources/SuperwallKit/Models/Paywall/Paywall.swift index 95713bdd2..69d09c4bb 100644 --- a/Sources/SuperwallKit/Models/Paywall/Paywall.swift +++ b/Sources/SuperwallKit/Models/Paywall/Paywall.swift @@ -84,6 +84,7 @@ struct Paywall: Decodable { /// Determines whether a free trial is available or not. var isFreeTrialAvailable = false + /// The reason for closing the paywall. var closeReason: PaywallCloseReason? = nil /// Determines whether a paywall executes the diff --git a/Sources/SuperwallKit/Models/Paywall/PaywallProducts.swift b/Sources/SuperwallKit/Models/Paywall/PaywallProducts.swift index e3b25f3c7..69332b31d 100644 --- a/Sources/SuperwallKit/Models/Paywall/PaywallProducts.swift +++ b/Sources/SuperwallKit/Models/Paywall/PaywallProducts.swift @@ -13,19 +13,24 @@ import StoreKit /// Pass an instance of this to ``Superwall/track(event:params:paywallOverrides:paywallHandler:)`` to replace your remotely defined products. @objc(SWKPaywallProducts) @objcMembers -public class PaywallProducts: NSObject { +public final class PaywallProducts: NSObject, Sendable { /// The primary product for the paywall. - var primary: StoreProduct? + let primary: StoreProduct? /// The secondary product for the paywall. - var secondary: StoreProduct? + let secondary: StoreProduct? /// The tertiary product for the paywall. - var tertiary: StoreProduct? + let tertiary: StoreProduct? - var ids: [String] = [] + let ids: [String] - private override init() {} + private override init() { + primary = nil + secondary = nil + tertiary = nil + ids = [] + } /// Define one or more products to be substituted into the paywall. /// diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywallViewController.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywallViewController.swift new file mode 100644 index 000000000..b86083fb8 --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywallViewController.swift @@ -0,0 +1,38 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 02/05/2023. +// + +import Foundation +import Combine + +extension Superwall { + /// Runs a combine pipeline to get a paywall to present, publishing ``PaywallState`` objects that provide updates on the lifecycle of the paywall. + /// + /// - Parameters: + /// - request: A presentation request of type `PresentationRequest` to feed into a presentation pipeline. + /// + /// - Returns: A ``PaywallViewController`` to present. + @discardableResult + func internallyGetPaywallViewController( + _ request: PresentationRequest + ) -> AnyPublisher { + let paywallStatePublisher: PassthroughSubject = .init() + let presentationSubject = PresentationSubject(request) + + return presentationSubject + .eraseToAnyPublisher() + .waitToPresent() + .logPresentation("Called Superwall.shared.getPaywallViewController") + .evaluateRules() + .confirmHoldoutAssignment() + .handleTriggerResult(paywallStatePublisher) + .getPaywallViewController(pipelineType: .presentation(paywallStatePublisher)) + .checkSubscriptionStatus(paywallStatePublisher) + .confirmPaywallAssignment() + .storePresentationObjects(presentationSubject, paywallStatePublisher) + .extractPaywallViewController(paywallStatePublisher) + } +} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckSubscriptionStatusOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/CheckSubscriptionStatusOperator.swift similarity index 95% rename from Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckSubscriptionStatusOperator.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/CheckSubscriptionStatusOperator.swift index 48be7a56f..3debd04bc 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckSubscriptionStatusOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/CheckSubscriptionStatusOperator.swift @@ -19,9 +19,8 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error /// - Returns: A publisher that contains info for the next pipeline operator. func checkSubscriptionStatus( _ paywallStatePublisher: PassthroughSubject - ) -> AnyPublisher { + ) -> PresentablePipelineOutputPublisher { asyncMap { input in - let subscriptionStatus = await input.request.flags.subscriptionStatus.async() if await InternalPresentationLogic.userSubscribedAndNotOverridden( isUserSubscribed: subscriptionStatus == .active, @@ -56,7 +55,7 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error request: input.request, debugInfo: input.debugInfo, paywallViewController: input.paywallViewController, - presenter: UIViewController(), // TODO: Fic this + presenter: UIViewController(), // TODO: Fix this confirmableAssignment: input.confirmableAssignment ) } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/ExtractPaywallViewControllerOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/ExtractPaywallViewControllerOperator.swift new file mode 100644 index 000000000..f91c4664b --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/ExtractPaywallViewControllerOperator.swift @@ -0,0 +1,33 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 02/05/2023. +// + +import Foundation +import Combine + +extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Error { + /// Gets the ``PaywallViewController`` and sets it up before returning it. + /// + /// - Parameters: + /// - paywallStatePublisher: A `PassthroughSubject` that gets sent ``PaywallState`` objects. + /// + /// - Returns: A publisher that contains a ``PaywallViewController``. + func extractPaywallViewController( + _ paywallStatePublisher: PassthroughSubject + ) -> AnyPublisher { + receive(on: DispatchQueue.main) + .map { input in + let paywallViewController = input.paywallViewController + paywallViewController.set( + eventData: input.request.presentationInfo.eventData, + presentationStyleOverride: input.request.paywallOverrides?.presentationStyle, + paywallStatePublisher: paywallStatePublisher + ) + return input.paywallViewController + } + .eraseToAnyPublisher() + } +} diff --git a/Sources/SuperwallKit/Paywall/Presentation/GetPaywallViewController.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift similarity index 51% rename from Sources/SuperwallKit/Paywall/Presentation/GetPaywallViewController.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift index 9ac303b80..5af9dddd2 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/GetPaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift @@ -11,7 +11,21 @@ import Combine import UIKit extension Superwall { - public func getPaywall( + /// Gets the ``PaywallViewController`` object, which you can present + /// however you want. + /// + /// To use this you **must** follow the following steps: + /// + /// 1. Call this function to retrieve the ``PaywallViewController``. + /// 2. Call ``PaywallViewController/presentationWillBegin()`` when + /// you're about to present the view controller. + /// 3. Present the view controller. + /// 4. Call ``PaywallViewController/presentationDidFinish()`` after presentation + /// completes. + /// + /// - Note: The remotely configured presentation style will be ignored, it is up to you + /// to set it programmatically. + public func getPaywallViewController( forEvent event: String, params: [String: Any]? = nil, paywallOverrides: PaywallOverrides? = nil, @@ -19,7 +33,7 @@ extension Superwall { ) { Task { do { - let paywallViewController = try await getPaywall( + let paywallViewController = try await getPaywallViewController( forEvent: event, params: params, paywallOverrides: paywallOverrides @@ -31,7 +45,21 @@ extension Superwall { } } - public func getPaywall( + /// Gets the ``PaywallViewController`` object, which you can present + /// however you want. + /// + /// To use this you **must** follow the following steps: + /// + /// 1. Call this function to retrieve the ``PaywallViewController``. + /// 2. Call ``PaywallViewController/presentationWillBegin()`` when + /// you're about to present the view controller. + /// 3. Present the view controller. + /// 4. Call ``PaywallViewController/presentationDidFinish()`` after presentation + /// completes. + /// + /// - Note: The remotely configured presentation style will be ignored, it is up to you + /// to set it programmatically. + public func getPaywallViewController( forEvent: String, params: [String: Any]? = nil, paywallOverrides: PaywallOverrides? = nil @@ -52,10 +80,7 @@ extension Superwall { paywallOverrides: paywallOverrides, isPaywallPresented: false ) - - let paywallStatePublisher: PassthroughSubject = .init() - - return self.internallyGetPaywallViewController(presentationRequest, paywallStatePublisher) + return self.internallyGetPaywallViewController(presentationRequest) } .eraseToAnyPublisher() .throwableAsync() diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift index ae91ab326..af94899c7 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift @@ -44,6 +44,7 @@ extension Superwall { .confirmPaywallAssignment() .presentPaywall(paywallStatePublisher) .storePresentationObjects(presentationSubject, paywallStatePublisher) + .printErrors() .subscribe(Subscribers.Sink( receiveCompletion: { _ in }, receiveValue: { _ in } @@ -54,38 +55,6 @@ extension Superwall { .eraseToAnyPublisher() } - @discardableResult - func internallyGetPaywallViewController( - _ request: PresentationRequest, - _ paywallStatePublisher: PassthroughSubject = .init() - ) -> AnyPublisher { - - let presentationSubject = PresentationSubject(request) - - return presentationSubject - .eraseToAnyPublisher() - .waitToPresent() - .logPresentation("Called Superwall.shared.getPaywallViewController") - .evaluateRules() - .confirmHoldoutAssignment() - .handleTriggerResult(paywallStatePublisher) - .getPaywallViewController(pipelineType: .presentation(paywallStatePublisher)) - .checkSubscriptionStatus(paywallStatePublisher) - .confirmPaywallAssignment() - .storePresentationObjects(presentationSubject, paywallStatePublisher) - .receive(on: DispatchQueue.main) - .map { input in - let paywallViewController = input.paywallViewController - paywallViewController.set( - eventData: input.request.presentationInfo.eventData, - presentationStyleOverride: input.request.paywallOverrides?.presentationStyle, - paywallStatePublisher: paywallStatePublisher - ) - return input.paywallViewController - } - .eraseToAnyPublisher() - } - /// Presents the paywall again by sending the previous presentation request to the presentation publisher. func presentAgain(cacheKey: String) { guard let lastPresentationItems = presentationItems.last else { @@ -106,16 +75,16 @@ extension Superwall { @MainActor func dismiss( _ paywallViewController: PaywallViewController, - state: DismissState, - shouldSendDismissedState: Bool = true, + result: PaywallResult, + shouldSendPaywallResult: Bool = true, shouldCompleteStatePublisher: Bool = true, completion: (() -> Void)? = nil ) { let paywallInfo = paywallViewController.paywallInfo paywallViewController.dismiss( paywallInfo: paywallInfo, - state: state, - shouldSendDismissedState: shouldSendDismissedState, + result: result, + shouldSendPaywallResult: shouldSendPaywallResult, shouldCompleteStatePublisher: shouldCompleteStatePublisher ) { completion?() diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift index 5cd6343f5..a17f9a32a 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift @@ -13,6 +13,7 @@ extension AnyPublisher where Output == PresentationRequest, Failure == Error { /// be established. func waitToPresent() -> AnyPublisher { subscribe(on: DispatchQueue.global(qos: .userInitiated)) + // TODO: PRINT OUT HERE .flatMap { request in zip( request.dependencyContainer.identityManager.hasIdentity, @@ -25,22 +26,11 @@ extension AnyPublisher where Output == PresentationRequest, Failure == Error { } .first() .map { request, _, _, _ in - return request - } - .eraseToAnyPublisher() - } - - /// Waits for config to be received and the identity to be established - func waitUntilReady() -> AnyPublisher { - subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .flatMap { request in - zip( - request.dependencyContainer.identityManager.hasIdentity, - request.dependencyContainer.configManager.hasConfig + Logger.debug( + logLevel: .info, + scope: .paywallPresentation, + message: "Retrieved identity, configuration and subscription status." ) - } - .first() - .map { request, _, _ in return request } .eraseToAnyPublisher() diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift index aff28b962..ffd24d9dc 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift @@ -34,6 +34,7 @@ extension AnyPublisher where Output == PresentationRequest, Failure == Error { value: "You can only present one paywall at a time." ) Task.detached(priority: .utility) { + // TODO: MOVE TO COMPLETION BLOCK? SHOULD THAT BE ON EVERY PIPELINE OR JUST PRESENT let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .alreadyPresented) await Superwall.shared.track(trackedEvent) } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift index 1af33c487..881a6d13b 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift @@ -27,7 +27,7 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error /// - Returns: A publisher that contains info for the next pipeline operator. func checkPaywallIsPresentable( _ paywallStatePublisher: PassthroughSubject - ) -> AnyPublisher { + ) -> PresentablePipelineOutputPublisher { asyncMap { input in let subscriptionStatus = await input.request.flags.subscriptionStatus.async() if await InternalPresentationLogic.userSubscribedAndNotOverridden( diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperator.swift index 14497bec8..22b43957d 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperator.swift @@ -14,7 +14,7 @@ extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Err /// This is split from the holdout assignment because overrides can make the /// paywall present even if the user is subscribed. We only know the overrides /// at this point. - func confirmPaywallAssignment() -> AnyPublisher { + func confirmPaywallAssignment() -> PresentablePipelineOutputPublisher { map { input in // Debuggers shouldn't confirm assignments. if input.request.flags.isDebuggerLaunched { diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift index 794d7636f..b9ee2fb71 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift @@ -15,7 +15,6 @@ struct PaywallVcPipelineOutput { let confirmableAssignment: ConfirmableAssignment? } - extension AnyPublisher where Output == TriggerResultResponsePipelineOutput, Failure == Error { enum PipelineType { case getTrackResult diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift index 1df185aa6..019e091cf 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift @@ -18,11 +18,16 @@ extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Err /// - Returns: A publisher that contains info for the next pipeline operator. func presentPaywall( _ paywallStatePublisher: PassthroughSubject - ) -> AnyPublisher { + ) -> PresentablePipelineOutputPublisher { flatMap { input in Future { promise in Task { await MainActor.run { + Logger.debug( + logLevel: .info, + scope: .paywallPresentation, + message: "Presenting paywall" + ) input.paywallViewController.present( on: input.presenter, eventData: input.request.presentationInfo.eventData, @@ -52,7 +57,7 @@ extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Err } paywallStatePublisher.send(.skipped(.error(error))) paywallStatePublisher.send(completion: .finished) - promise(.failure(PresentationPipelineError.cancelled)) + promise(.failure(PresentationPipelineError.paywallAlreadyPresented)) } } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PrintErrorsOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PrintErrorsOperator.swift new file mode 100644 index 000000000..cc6799885 --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PrintErrorsOperator.swift @@ -0,0 +1,27 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 02/05/2023. +// + +import Foundation +import Combine + +extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Error { + func printErrors() -> PresentablePipelineOutputPublisher { + handleEvents(receiveCompletion: { completion in + switch completion { + case .failure(let error): + Logger.debug( + logLevel: .error, + scope: .paywallPresentation, + message: "Skipped paywall presentation: \(error)" + ) + case .finished: + break + } + }) + .eraseToAnyPublisher() + } +} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/StorePresentationObjectsOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/StorePresentationObjectsOperator.swift index 37e3df04d..d1fa3d106 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/StorePresentationObjectsOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/StorePresentationObjectsOperator.swift @@ -19,7 +19,7 @@ extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Err func storePresentationObjects( _ presentationSubject: PresentationSubject, _ paywallStatePublisher: PassthroughSubject - ) -> AnyPublisher { + ) -> PresentablePipelineOutputPublisher { map { input in let lastPaywallPresentation = LastPresentationItems( request: input.request, diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PaywallOverrides.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PaywallOverrides.swift index 373b28e6f..01f5cddd3 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PaywallOverrides.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PaywallOverrides.swift @@ -10,7 +10,7 @@ import Foundation /// Override the default behavior and products of a paywall. /// /// Provide an instance of this to ``Superwall/track(event:params:paywallOverrides:paywallHandler:)``. -public final class PaywallOverrides: NSObject { +public final class PaywallOverrides: NSObject, Sendable { /// Defines the products to override on the paywall. /// /// You can override one or more products of your choosing. diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift index ebce8ed08..d3f7cbee8 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift @@ -8,7 +8,7 @@ import Foundation /// Contains the possible reasons for the dismissal of a paywall. -public enum DismissState: Equatable, Sendable { +public enum PaywallResult: Equatable, Sendable { /// The paywall was dismissed because the user purchased a product /// /// - Parameters: @@ -22,9 +22,9 @@ public enum DismissState: Equatable, Sendable { case restored } -/// Objective-C compatible enum for ``DismissState`` -@objc(SWKDismissState) -public enum DismissStateObjc: Int, Sendable { +/// Objective-C compatible enum for ``PaywallResult`` +@objc(SWKPaywallResult) +public enum PaywallResultObjc: Int, Sendable { case purchased case closed case restored @@ -35,8 +35,8 @@ public enum PaywallState { /// The paywall was presented. Contains a ``PaywallInfo`` object with more information about the presented paywall. case presented(PaywallInfo) - /// The paywall was dismissed. Contains a ``PaywallInfo`` object with more information about the presented paywall and a ``DismissState`` object containing the paywall dismissal reason. - case dismissed(PaywallInfo, DismissState) + /// The paywall was dismissed. Contains a ``PaywallInfo`` object with more information about the presented paywall and a ``PaywallResult`` object containing the paywall dismissal reason. + case dismissed(PaywallInfo, PaywallResult) /// The paywall was skipped. Contains a ``PaywallSkippedReason`` enum whose cases state why the paywall was skipped. case skipped(PaywallSkippedReason) diff --git a/Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift b/Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift index b8e5c4626..a09f891c5 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift @@ -8,13 +8,15 @@ import Foundation /// An enum whose cases indicate whether the paywall was closed by user -/// interaction or for another paywall to show +/// interaction or because another paywall will show. @objc(SWKPaywallCloseReason) -public enum PaywallCloseReason: Int, Codable { +public enum PaywallCloseReason: Int, Codable, Equatable, Sendable { + /// The paywall was closed by user interaction. case userInteraction - /// Prevents the ``Superwall/register(event:params:handler:feature:)`` `feature` + /// The paywall was automatically closed becacuse another paywall will show. + /// + /// This prevents ``Superwall/register(event:params:handler:feature:)`` `feature` /// block from executing on dismiss of the paywall, because another paywall is set to show case forNextPaywall } - diff --git a/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift b/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift index 25c8a9fd0..e6ed84c15 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift @@ -92,8 +92,12 @@ public final class PaywallInfo: NSObject { /// The paywall.js version installed on the paywall website. public let paywalljsVersion: String? + /// Indicates whether the paywall is showing free trial content. public let isFreeTrialAvailable: Bool + /// A ``FeatureGatingBehavior`` case that indicates whether the + /// ``Superwall/register(event:params:handler:feature:)`` + /// `feature` block executes or not. public let featureGatingBehavior: FeatureGatingBehavior /// An enum describing why this paywall was last closed. `nil` if not yet closed. diff --git a/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift b/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift index 33f8abb8b..a9a023d3d 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift @@ -13,11 +13,35 @@ import Foundation @objcMembers public class PaywallPresentationHandler: NSObject { /// A block called when the paywall did present. - public var onPresent: ((_ paywallInfo: PaywallInfo) -> Void)? + var onPresentHandler: ((_ paywallInfo: PaywallInfo) -> Void)? /// A block called when the paywall did dismiss. - public var onDismiss: ((_ paywallInfo: PaywallInfo) -> Void)? + var onDismissHandler: ((_ paywallInfo: PaywallInfo) -> Void)? /// A block called when an error occurred while trying to present a paywall. - public var onError: ((_ error: Error) -> Void)? + var onErrorHandler: ((_ error: Error) -> Void)? + + /// Sets the handler that will be called when the paywall did presented. + /// + /// - Parameter handler: A block that accepts a ``PaywallInfo`` object associated with + /// the presented paywall. + public func onPresent(_ handler: @escaping (_ paywallInfo: PaywallInfo) -> Void) { + self.onPresentHandler = handler + } + + /// Sets the handler that will be called when the paywall did dismissed. + /// + /// - Parameter handler: A block that accepts a ``PaywallInfo`` object associated with + /// the dismissed paywall. + public func onDismiss(_ handler: @escaping (_ paywallInfo: PaywallInfo) -> Void) { + self.onDismissHandler = handler + } + + /// Sets the handler that will be called when an error occurred while trying to present a paywall. + /// + /// - Parameter handler: A block that accepts an `Error` indicating why the paywall + /// could not present. + public func onError(_ handler: @escaping (_ error: Error) -> Void) { + self.onErrorHandler = handler + } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index 57ca7e6d0..657492a05 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -42,7 +42,7 @@ extension Superwall { await withCheckedContinuation { continuation in dismiss( paywallViewController, - state: .closed + result: .closed ) { continuation.resume() } @@ -59,8 +59,8 @@ extension Superwall { paywallViewController.paywall.closeReason = .forNextPaywall dismiss( paywallViewController, - state: .closed, // .closedForNextPaywall - shouldSendDismissedState: true, + result: .closed, + shouldSendPaywallResult: true, shouldCompleteStatePublisher: false ) { continuation.resume() @@ -110,7 +110,7 @@ extension Superwall { presentationStyleOverride: PaywallPresentationStyle = .none, onSkip: ((PaywallSkippedReasonObjc, NSError) -> Void)? = nil, onPresent: ((PaywallInfo) -> Void)? = nil, - onDismiss: ((DismissStateObjc, String?, PaywallInfo) -> Void)? = nil + onDismiss: ((PaywallResultObjc, String?, PaywallInfo) -> Void)? = nil ) { objcTrack( event: event, @@ -132,7 +132,7 @@ extension Superwall { presentationStyleOverride: PaywallPresentationStyle = .none, onSkip: ((PaywallSkippedReasonObjc, NSError) -> Void)? = nil, onPresent: ((PaywallInfo) -> Void)? = nil, - onDismiss: ((DismissStateObjc, String?, PaywallInfo) -> Void)? = nil + onDismiss: ((PaywallResultObjc, String?, PaywallInfo) -> Void)? = nil ) { let overrides = PaywallOverrides( products: products, @@ -310,7 +310,7 @@ extension Superwall { event: String, onSkip: ((PaywallSkippedReasonObjc, NSError) -> Void)? = nil, onPresent: ((PaywallInfo) -> Void)? = nil, - onDismiss: ((DismissStateObjc, String?, PaywallInfo) -> Void)? = nil + onDismiss: ((PaywallResultObjc, String?, PaywallInfo) -> Void)? = nil ) { objcTrack( event: event, @@ -351,7 +351,7 @@ extension Superwall { params: [String: Any]? = nil, onSkip: ((PaywallSkippedReasonObjc, NSError) -> Void)? = nil, onPresent: ((PaywallInfo) -> Void)? = nil, - onDismiss: ((DismissStateObjc, String?, PaywallInfo) -> Void)? = nil + onDismiss: ((PaywallResultObjc, String?, PaywallInfo) -> Void)? = nil ) { objcTrack( event: event, @@ -457,11 +457,11 @@ extension Superwall { switch state { case .presented(let paywallInfo): DispatchQueue.main.async { - handler?.onPresent?(paywallInfo) + handler?.onPresentHandler?(paywallInfo) } case let .dismissed(paywallInfo, state): DispatchQueue.main.async { - handler?.onDismiss?(paywallInfo) + handler?.onDismissHandler?(paywallInfo) } switch state { case .purchased, @@ -482,7 +482,7 @@ extension Superwall { switch reason { case .error(let error): DispatchQueue.main.async { - handler?.onError?(error) // otherwise turning internet off would give unlimited access + handler?.onErrorHandler?(error) // otherwise turning internet off would give unlimited access } default: DispatchQueue.main.async { @@ -549,8 +549,8 @@ extension Superwall { /// - completion: A completion block that gets called when the paywall is dismissed by the user, by way of purchasing, restoring or manually dismissing. Accepts a `Bool` that is `true` if the user purchased a product and `false` if not, a `String?` equal to the product id of the purchased product (if any) and a ``PaywallInfo`` object containing information about the paywall. private func onDismissConverter( paywallInfo: PaywallInfo, - state: DismissState, - completion: (DismissStateObjc, String?, PaywallInfo) -> Void + state: PaywallResult, + completion: (PaywallResultObjc, String?, PaywallInfo) -> Void ) { switch state { case .closed: diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 3ad68fb4f..beba558d4 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -37,6 +37,17 @@ enum PaywallLoadingState { } public class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegate { + // MARK: - Public Properties + /// A publisher that emits ``PaywallState`` objects, which tell you the state of the presented paywall. + public var paywallStatePublisher: AnyPublisher? { + return paywallStateSubject?.eraseToAnyPublisher() + } + + /// Defines whether the presentation should animate based on the presentation style. + public var presentationIsAnimated: Bool { + return presentationStyle != .fullscreenNoAnimation + } + // MARK: - Internal Properties override public var preferredStatusBarStyle: UIStatusBarStyle { if let isDark = view.backgroundColor?.isDarkColor, isDark { @@ -79,13 +90,13 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading } // MARK: - Private Properties - private weak var delegate: PaywallViewControllerDelegate? - - /// A publisher that emits ``PaywallState`` objects. These state objects feed back to + /// Internal passthrough subject that emits ``PaywallState`` objects. These state objects feed back to /// the caller of ``Superwall/track(event:params:paywallOverrides:paywallHandler:)`` /// /// This publisher is set on presentation of the paywall. - public var paywallStatePublisher: PassthroughSubject! + private var paywallStateSubject: PassthroughSubject! + + private weak var delegate: PaywallViewControllerDelegate? /// Defines whether the view controller is being presented or not. private var isPresented = false @@ -102,11 +113,6 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading /// The presentation style for the paywall. private var presentationStyle: PaywallPresentationStyle - /// Defines whether the presentation should animate based on the presentation style. - public var presentationIsAnimated: Bool { - return presentationStyle != .fullscreenNoAnimation - } - /// A loading spinner that appears when making a purchase. private var loadingViewController: LoadingViewController? @@ -234,8 +240,8 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading @objc private func pressedRefreshPaywall() { dismiss( paywallInfo: paywallInfo, - state: .closed, - shouldSendDismissedState: false, + result: .closed, + shouldSendPaywallResult: false, shouldCompleteStatePublisher: false ) { [weak self] in guard let self = self else { @@ -248,7 +254,7 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading @objc private func pressedExitPaywall() { dismiss( paywallInfo: paywallInfo, - state: .closed + result: .closed ) { [weak self] in guard let self = self else { return @@ -459,36 +465,9 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading paywallStatePublisher: PassthroughSubject = .init() ) { self.eventData = eventData - self.paywallStatePublisher = paywallStatePublisher - setPresentationStyle(withOverride: presentationStyleOverride) - } - - public func presentationWillBegin() { - addShimmerView(onPresent: true) - prepareForPresentation() - if loadingState == .ready { - webView.messageHandler.handle(.templateParamsAndUserAttributes) - } + self.paywallStateSubject = paywallStatePublisher } - public func onDismiss ( - _ handler: @escaping (_ state: DismissState) -> Void - ) { - paywallStatePublisher.subscribe(Subscribers.Sink( - receiveCompletion: { _ in }, - receiveValue: { paywallState in - switch paywallState { - case .dismissed(_, let dismissState): - handler(dismissState) - default: - break - } - } - )) - } - - - func present( on presenter: UIViewController, eventData: EventData?, @@ -504,9 +483,8 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading addShimmerView(onPresent: true) prepareForPresentation() - self.eventData = eventData - self.paywallStatePublisher = paywallStatePublisher + self.paywallStateSubject = paywallStatePublisher setPresentationStyle(withOverride: presentationStyleOverride) @@ -567,15 +545,6 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading Superwall.shared.dependencyContainer.delegateAdapter.willPresentPaywall(withInfo: paywallInfo) } - public func presentationDidFinish() { - isPresented = true - Superwall.shared.dependencyContainer.delegateAdapter.didPresentPaywall(withInfo: paywallInfo) - Task(priority: .utility) { - await trackOpen() - } - GameControllerManager.shared.setDelegate(self) - } - @MainActor func presentAlert( title: String? = nil, @@ -607,6 +576,47 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading } } +// MARK: - Public API +extension PaywallViewController { + /// Prepares the view controller for presentation. This **must** be called before + /// you present the view controller. + public func presentationWillBegin() { + addShimmerView(onPresent: true) + prepareForPresentation() + if loadingState == .ready { + webView.messageHandler.handle(.templateParamsAndUserAttributes) + } + } + + /// Lets the view controller know that presentation has finished. This **must** be called immediately + /// after you present the view controller. + public func presentationDidFinish() { + isPresented = true + Superwall.shared.dependencyContainer.delegateAdapter.didPresentPaywall(withInfo: paywallInfo) + Task(priority: .utility) { + await trackOpen() + } + GameControllerManager.shared.setDelegate(self) + } + + /// A handler that returns a ``PaywallResult`` + public func onDismiss ( + _ handler: @escaping (_ result: PaywallResult) -> Void + ) { + paywallStateSubject.subscribe(Subscribers.Sink( + receiveCompletion: { _ in }, + receiveValue: { paywallState in + switch paywallState { + case .dismissed(_, let dismissState): + handler(dismissState) + default: + break + } + } + )) + } +} + // MARK: - PaywallMessageHandlerDelegate extension PaywallViewController: PaywallMessageHandlerDelegate { func eventDidOccur(_ paywallEvent: PaywallWebEvent) { @@ -640,7 +650,7 @@ extension PaywallViewController: PaywallMessageHandlerDelegate { func openDeepLink(_ url: URL) { dismiss( paywallInfo: paywallInfo, - state: .closed + result: .closed ) { [weak self] in self?.eventDidOccur(.openedDeepLink(url: url)) UIApplication.shared.open(url) @@ -702,7 +712,7 @@ extension PaywallViewController { if !calledDismiss { didDismiss( paywallInfo: paywallInfo, - state: .closed + paywallResult: .closed ) } @@ -711,8 +721,8 @@ extension PaywallViewController { func dismiss( paywallInfo: PaywallInfo, - state: DismissState, - shouldSendDismissedState: Bool = true, + result: PaywallResult, + shouldSendPaywallResult: Bool = true, shouldCompleteStatePublisher: Bool = true, completion: (() -> Void)? = nil ) { @@ -725,8 +735,8 @@ extension PaywallViewController { } self.didDismiss( paywallInfo: paywallInfo, - state: state, - shouldSendDismissedState: shouldSendDismissedState, + paywallResult: result, + shouldSendPaywallResult: shouldSendPaywallResult, shouldSendCompletion: shouldCompleteStatePublisher, completion: completion ) @@ -740,8 +750,8 @@ extension PaywallViewController { private func didDismiss( paywallInfo: PaywallInfo, - state: DismissState, - shouldSendDismissedState: Bool = true, + paywallResult: PaywallResult, + shouldSendPaywallResult: Bool = true, shouldSendCompletion: Bool = true, completion: (() -> Void)? = nil ) { @@ -756,12 +766,12 @@ extension PaywallViewController { GameControllerManager.shared.clearDelegate(self) Superwall.shared.dependencyContainer.delegateAdapter.didDismissPaywall(withInfo: paywallInfo) - if shouldSendDismissedState { - paywallStatePublisher?.send(.dismissed(paywallInfo, state)) + if shouldSendPaywallResult { + paywallStateSubject?.send(.dismissed(paywallInfo, paywallResult)) } if shouldSendCompletion { - paywallStatePublisher?.send(completion: .finished) - paywallStatePublisher = nil + paywallStateSubject?.send(completion: .finished) + paywallStateSubject = nil } Superwall.shared.destroyPresentingWindow() completion?() diff --git a/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/PriceFormatterProvider.swift b/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/PriceFormatterProvider.swift index d8c9fb900..4e07a3d28 100644 --- a/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/PriceFormatterProvider.swift +++ b/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/PriceFormatterProvider.swift @@ -19,46 +19,54 @@ import Foundation final class PriceFormatterProvider { private var cachedPriceFormatterForSK1: NumberFormatter? private var cachedPriceFormatterForSK2: NumberFormatter? + private let queue = DispatchQueue(label: "com.superwall.priceformatterprovider", attributes: .concurrent) func priceFormatterForSK1(with locale: Locale) -> NumberFormatter { - func makePriceFormatterForSK1(with locale: Locale) -> NumberFormatter { - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.locale = locale - return formatter - } + queue.sync { + func makePriceFormatterForSK1(with locale: Locale) -> NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.locale = locale + return formatter + } - guard - let formatter = cachedPriceFormatterForSK1, - formatter.locale == locale - else { - let newFormatter = makePriceFormatterForSK1(with: locale) - cachedPriceFormatterForSK1 = newFormatter - return newFormatter + guard + let formatter = cachedPriceFormatterForSK1, + formatter.locale == locale + else { + let newFormatter = makePriceFormatterForSK1(with: locale) + cachedPriceFormatterForSK1 = newFormatter + return newFormatter + } + return formatter } - return formatter } @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) func priceFormatterForSK2(withCurrencyCode currencyCode: String) -> NumberFormatter { - func makePriceFormatterForSK2(with currencyCode: String) -> NumberFormatter { - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.locale = .autoupdatingCurrent - formatter.currencyCode = currencyCode - return formatter - } + queue.sync { + func makePriceFormatterForSK2(with currencyCode: String) -> NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.locale = .autoupdatingCurrent + formatter.currencyCode = currencyCode + return formatter + } - guard - let formatter = cachedPriceFormatterForSK2, - formatter.currencyCode == currencyCode - else { - let newFormatter = makePriceFormatterForSK2(with: currencyCode) - cachedPriceFormatterForSK2 = newFormatter + guard + let formatter = cachedPriceFormatterForSK2, + formatter.currencyCode == currencyCode + else { + let newFormatter = makePriceFormatterForSK2(with: currencyCode) + cachedPriceFormatterForSK2 = newFormatter - return newFormatter - } + return newFormatter + } - return formatter + return formatter + } } } + +// It's unchecked here because we're using a queue to serialise access to the caches. +extension PriceFormatterProvider: @unchecked Sendable {} diff --git a/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/StoreProduct.swift b/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/StoreProduct.swift index 0c0dc9d1c..246104a80 100644 --- a/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/StoreProduct.swift +++ b/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/StoreProduct.swift @@ -23,7 +23,7 @@ public typealias SK2Product = StoreKit.Product @objc(SWKStoreProduct) @objcMembers -public final class StoreProduct: NSObject, StoreProductType { +public final class StoreProduct: NSObject, StoreProductType, Sendable { let product: StoreProductType /// Returns the `SKProduct` if this `StoreProduct` represents a `StoreKit.SKProduct`. diff --git a/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/StoreProductType.swift b/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/StoreProductType.swift index 9a3841773..1c3db0ef5 100644 --- a/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/StoreProductType.swift +++ b/Sources/SuperwallKit/StoreKit/Abstractions/StoreProduct/StoreProductType.swift @@ -15,7 +15,7 @@ import Foundation /// Type that provides access to all of `StoreKit`'s product type's properties. -protocol StoreProductType { +protocol StoreProductType: Sendable { /// The string that identifies the product to the Apple App Store. var productIdentifier: String { get } diff --git a/Sources/SuperwallKit/StoreKit/StoreKitManager.swift b/Sources/SuperwallKit/StoreKit/StoreKitManager.swift index 70448aed9..75f302c13 100644 --- a/Sources/SuperwallKit/StoreKit/StoreKitManager.swift +++ b/Sources/SuperwallKit/StoreKit/StoreKitManager.swift @@ -198,7 +198,7 @@ extension StoreKitManager { } if Superwall.shared.options.paywalls.automaticallyDismiss { - Superwall.shared.dismiss(paywallViewController, state: .restored) + Superwall.shared.dismiss(paywallViewController, result: .restored) } } } diff --git a/Sources/SuperwallKit/StoreKit/Transactions/RestorationManager.swift b/Sources/SuperwallKit/StoreKit/Transactions/RestorationManager.swift index 65906c531..41ce3507b 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/RestorationManager.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/RestorationManager.swift @@ -78,7 +78,7 @@ final class RestorationManager { } if Superwall.shared.options.paywalls.automaticallyDismiss { - Superwall.shared.dismiss(paywallViewController, state: .restored) + Superwall.shared.dismiss(paywallViewController, result: .restored) } } } diff --git a/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift b/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift index 48744e9db..14c2766f4 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift @@ -147,7 +147,7 @@ final class TransactionManager { if Superwall.shared.options.paywalls.automaticallyDismiss { await Superwall.shared.dismiss( paywallViewController, - state: .purchased(productId: product.productIdentifier) + result: .purchased(productId: product.productIdentifier) ) } } diff --git a/Sources/SuperwallKit/Superwall.swift b/Sources/SuperwallKit/Superwall.swift index 592056945..dfdc0e500 100644 --- a/Sources/SuperwallKit/Superwall.swift +++ b/Sources/SuperwallKit/Superwall.swift @@ -461,7 +461,7 @@ extension Superwall: PaywallViewControllerDelegate { case .closed: dismiss( paywallViewController, - state: .closed + result: .closed ) case .initiatePurchase(let productId): await dependencyContainer.transactionManager.purchase( diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift index af804c73c..b5aa4ea22 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift @@ -43,7 +43,7 @@ final class CheckForPaywallResultTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } func test_checkForPaywallResult_noRuleMatch() async { From fbcc872c07c1a20f1255a8057a11de7dd0395a11 Mon Sep 17 00:00:00 2001 From: jakemor Date: Wed, 3 May 2023 01:31:27 +0200 Subject: [PATCH 08/27] wip --- CHANGELOG.md | 2 +- .../TrackableSuperwallEvent.swift | 11 +++++++++ .../Internal Tracking/TrackingLogic.swift | 23 ++++++++++++++++--- .../Superwall Event/SuperwallEvent.swift | 6 +++++ .../Superwall Event/SuperwallEventObjc.swift | 5 ++++ .../InternalPresentation.swift | 6 ++--- .../Presentation/PaywallCloseReason.swift | 5 ++-- .../Presentation/PublicPresentation.swift | 6 ++--- .../PaywallViewController.swift | 10 +++----- Sources/SuperwallKit/Superwall.swift | 10 ++++++++ 10 files changed, 64 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3a70bce1..0f3c3c6a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup ### Breaking Changes - Changes `DismissState` to `PaywallResult` -- Removes the `closedForNextPaywall` `PaywallResult` and moves it to a new `PaywallInfo` property `closeReason`, which can either be `nil`, `.userInteraction`, or `.forNextPaywall`. +- Removes the `closedForNextPaywall` `PaywallResult` and moves it to a new `PaywallInfo` property `closeReason`, which can either be `nil`, `.systemLogic`, or `.forNextPaywall`. - Changes the `PaywallPresentationHandler` variables to functions. - Removes `Superwall.shared.track`. We're going all in on `Superwall.shared.register` baby! diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift index 64ef6f42c..929c76a3c 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift @@ -274,6 +274,17 @@ enum InternalSuperwallEvent { var customParameters: [String: Any] = [:] } + struct PaywallDecline: TrackableSuperwallEvent { + var superwallEvent: SuperwallEvent { + return .paywallDecline(paywallInfo: paywallInfo) + } + let paywallInfo: PaywallInfo + func getSuperwallParameters() async -> [String: Any] { + return await paywallInfo.eventParams() + } + var customParameters: [String: Any] = [:] + } + struct Transaction: TrackableSuperwallEvent { enum State { case start(StoreProduct) diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift index eb1575d64..2a2468d97 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift @@ -148,10 +148,21 @@ enum TrackingLogic { return .dontTriggerPaywall } - let presentedEventName = paywallViewController?.paywallInfo.presentedByEventWithName + // referring events in this set are not able to trigger another + // another paywall. prevents loops from occurring + let notAllowedReferringEventNames: Set = [ + SuperwallEventObjc.transactionAbandon.description, + SuperwallEventObjc.transactionFail.description, + SuperwallEventObjc.paywallDecline.description, + ] + + if let referringEventName = paywallViewController?.paywallInfo.presentedByEventWithName, + notAllowedReferringEventNames.contains(referringEventName) { + return .dontTriggerPaywall + } + if let event = event as? TrackableSuperwallEvent, - case .transactionAbandon = event.superwallEvent, - presentedEventName != SuperwallEventObjc.transactionAbandon.description { + case .transactionAbandon = event.superwallEvent { return .closePaywallThenTriggerPaywall } @@ -160,6 +171,12 @@ enum TrackingLogic { return .closePaywallThenTriggerPaywall } + if let event = event as? TrackableSuperwallEvent, + case .paywallDecline = event.superwallEvent { + return .closePaywallThenTriggerPaywall + } + + if paywallViewController != nil { return .dontTriggerPaywall } diff --git a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift index ca42eeebd..6d4f1ac76 100644 --- a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift @@ -54,6 +54,9 @@ public enum SuperwallEvent { /// When a paywall is closed. case paywallClose(paywallInfo: PaywallInfo) + /// When a user dismisses a paywall instead of purchasing + case paywallDecline(paywallInfo: PaywallInfo) + /// When the payment sheet is displayed to the user. case transactionStart(product: StoreProduct, paywallInfo: PaywallInfo) @@ -131,6 +134,7 @@ public enum SuperwallEvent { .appLaunch, .deepLink, .transactionFail, + .paywallDecline, .transactionAbandon: return true default: @@ -183,6 +187,8 @@ extension SuperwallEvent { return .init(objcEvent: .paywallOpen) case .paywallClose: return .init(objcEvent: .paywallClose) + case .paywallDecline: + return .init(objcEvent: .paywallDecline) case .transactionStart: return .init(objcEvent: .transactionStart) case .transactionFail: diff --git a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEventObjc.swift b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEventObjc.swift index 19fcb4bca..1e30a5d9b 100644 --- a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEventObjc.swift +++ b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEventObjc.swift @@ -51,6 +51,9 @@ public enum SuperwallEventObjc: Int, CaseIterable { /// When a paywall is closed. case paywallClose + /// When a user dismisses a paywall instead of purchasing + case paywallDecline + /// When the payment sheet is displayed to the user. case transactionStart @@ -167,6 +170,8 @@ public enum SuperwallEventObjc: Int, CaseIterable { return "trigger_fire" case .paywallOpen: return "paywall_open" + case .paywallDecline: + return "paywall_decline" case .paywallClose: return "paywall_close" case .transactionStart: diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift index af94899c7..4bc18c937 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift @@ -78,14 +78,14 @@ extension Superwall { result: PaywallResult, shouldSendPaywallResult: Bool = true, shouldCompleteStatePublisher: Bool = true, + closeReason: PaywallCloseReason = .systemLogic, completion: (() -> Void)? = nil ) { - let paywallInfo = paywallViewController.paywallInfo paywallViewController.dismiss( - paywallInfo: paywallInfo, result: result, shouldSendPaywallResult: shouldSendPaywallResult, - shouldCompleteStatePublisher: shouldCompleteStatePublisher + shouldCompleteStatePublisher: shouldCompleteStatePublisher, + closeReason: closeReason ) { completion?() } diff --git a/Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift b/Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift index a09f891c5..e3968271e 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PaywallCloseReason.swift @@ -11,8 +11,9 @@ import Foundation /// interaction or because another paywall will show. @objc(SWKPaywallCloseReason) public enum PaywallCloseReason: Int, Codable, Equatable, Sendable { - /// The paywall was closed by user interaction. - case userInteraction + /// The paywall was closed by system logic, either after a purchase, because + /// a deeplink was presented, close button pressed, etc. + case systemLogic /// The paywall was automatically closed becacuse another paywall will show. /// diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index 657492a05..2481165af 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -38,7 +38,6 @@ extension Superwall { guard let paywallViewController = paywallViewController else { return } - paywallViewController.paywall.closeReason = .userInteraction await withCheckedContinuation { continuation in dismiss( paywallViewController, @@ -56,12 +55,11 @@ extension Superwall { } await withCheckedContinuation { continuation in - paywallViewController.paywall.closeReason = .forNextPaywall dismiss( paywallViewController, result: .closed, - shouldSendPaywallResult: true, - shouldCompleteStatePublisher: false + shouldCompleteStatePublisher: false, + closeReason: .forNextPaywall ) { continuation.resume() } diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index beba558d4..5dca1b297 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -239,7 +239,6 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading @objc private func pressedRefreshPaywall() { dismiss( - paywallInfo: paywallInfo, result: .closed, shouldSendPaywallResult: false, shouldCompleteStatePublisher: false @@ -253,7 +252,6 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading @objc private func pressedExitPaywall() { dismiss( - paywallInfo: paywallInfo, result: .closed ) { [weak self] in guard let self = self else { @@ -649,7 +647,6 @@ extension PaywallViewController: PaywallMessageHandlerDelegate { func openDeepLink(_ url: URL) { dismiss( - paywallInfo: paywallInfo, result: .closed ) { [weak self] in self?.eventDidOccur(.openedDeepLink(url: url)) @@ -711,7 +708,6 @@ extension PaywallViewController { if !calledDismiss { didDismiss( - paywallInfo: paywallInfo, paywallResult: .closed ) } @@ -720,13 +716,15 @@ extension PaywallViewController { } func dismiss( - paywallInfo: PaywallInfo, result: PaywallResult, shouldSendPaywallResult: Bool = true, shouldCompleteStatePublisher: Bool = true, + closeReason: PaywallCloseReason = .systemLogic, completion: (() -> Void)? = nil ) { + calledDismiss = true + paywall.closeReason = closeReason willDismiss() dismiss(animated: presentationIsAnimated) { [weak self] in @@ -734,7 +732,6 @@ extension PaywallViewController { return } self.didDismiss( - paywallInfo: paywallInfo, paywallResult: result, shouldSendPaywallResult: shouldSendPaywallResult, shouldSendCompletion: shouldCompleteStatePublisher, @@ -749,7 +746,6 @@ extension PaywallViewController { } private func didDismiss( - paywallInfo: PaywallInfo, paywallResult: PaywallResult, shouldSendPaywallResult: Bool = true, shouldSendCompletion: Bool = true, diff --git a/Sources/SuperwallKit/Superwall.swift b/Sources/SuperwallKit/Superwall.swift index dfdc0e500..65ef28c15 100644 --- a/Sources/SuperwallKit/Superwall.swift +++ b/Sources/SuperwallKit/Superwall.swift @@ -459,6 +459,16 @@ extension Superwall: PaywallViewControllerDelegate { switch paywallEvent { case .closed: + + let trackedEvent = InternalSuperwallEvent.PaywallDecline(paywallInfo: paywallViewController.paywallInfo) + + // TODO: if this results in another presentation, + // we need to stop the following dismiss call from + // completing the state publisher and we need this paywall + // to instead execute the feature block upon completion + // harder to handle this vs txn_abandon bc this is explicit ... + await Superwall.shared.track(trackedEvent) + dismiss( paywallViewController, result: .closed From 9e495e684e27e527f54d682f28b5136046db7e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Wed, 3 May 2023 16:45:54 +0100 Subject: [PATCH 09/27] Added logging for presentation pipelines --- .../HomeViewController.swift | 64 --------------- .../TrackableSuperwallEvent.swift | 18 ++++- .../Internal Tracking/Tracking.swift | 9 ++- .../Superwall Event/SuperwallEvent.swift | 31 +++---- .../Superwall Event/SuperwallEventObjc.swift | 44 +--------- .../Debug/DebugViewController.swift | 3 +- .../Dependencies/DependencyContainer.swift | 6 +- .../Dependencies/FactoryProtocols.swift | 3 +- .../SuperwallKit/Models/Paywall/Paywall.swift | 2 +- .../InternalGetPaywallViewController.swift | 1 + .../CheckSubscriptionStatusOperator.swift | 8 -- .../PublicGetPaywallViewController.swift | 30 ++++++- .../GetPresentationResultLogic.swift} | 2 +- .../InternalGetPresentationResult.swift} | 25 +++--- .../CheckForPaywallResultOperator.swift | 2 +- .../CheckPaywallIsPresentableOperator.swift | 2 +- .../ConvertToTrackResultOperator.swift | 4 +- .../GetPaywallVcNoChecksOperator.swift | 2 +- .../PresentationResult.swift | 0 .../PresentationValueObjc.swift | 0 .../PublicGetPresentationResult.swift} | 7 +- .../InternalPresentation.swift | 2 +- .../Operators/AwaitIdentityOperator.swift | 38 --------- .../CheckDebuggerPresentationOperator.swift | 4 - .../CheckNoPaywallAlreadyPresented.swift | 5 -- .../CheckPaywallPresentableOperator.swift | 12 +-- .../CheckUserSubscriptionOperator.swift | 4 - .../Operators/EvaluateRulesOperator.swift | 7 +- .../Operators/GetPaywallVcOperator.swift | 18 ++--- .../HandleTriggerResultOperator.swift | 22 +---- .../Operators/LogErrorsOperator.swift | 34 ++++++++ .../Operators/PresentPaywallOperator.swift | 18 ++--- .../Operators/PresentationPipelineError.swift | 21 ----- .../Operators/PrintErrorsOperator.swift | 27 ------- .../Operators/WaitToPresentOperator.swift | 80 +++++++++++++++++++ .../PaywallPresentationFailureReason.swift | 35 -------- .../PaywallPresentationRequestStatus.swift | 69 ++++++++++++++++ .../PresentationRequest.swift | 10 ++- .../Presentation/PublicPresentation.swift | 3 +- .../PaywallViewController.swift | 10 +-- .../Products/ProductsFetcherSK1.swift | 2 +- .../CheckForPaywallResultTests.swift | 10 +-- .../CheckPaywallIsPresentableTests.swift | 2 +- 43 files changed, 322 insertions(+), 374 deletions(-) rename Sources/SuperwallKit/Paywall/Presentation/{Get Track Result/GetTrackResultLogic.swift => Get Presentation Result/GetPresentationResultLogic.swift} (93%) rename Sources/SuperwallKit/Paywall/Presentation/{Get Track Result/InternalGetTrackResult.swift => Get Presentation Result/InternalGetPresentationResult.swift} (67%) rename Sources/SuperwallKit/Paywall/Presentation/{Get Track Result => Get Presentation Result}/Operators/CheckForPaywallResultOperator.swift (92%) rename Sources/SuperwallKit/Paywall/Presentation/{Get Track Result => Get Presentation Result}/Operators/CheckPaywallIsPresentableOperator.swift (94%) rename Sources/SuperwallKit/Paywall/Presentation/{Get Track Result => Get Presentation Result}/Operators/ConvertToTrackResultOperator.swift (67%) rename Sources/SuperwallKit/Paywall/Presentation/{Get Track Result => Get Presentation Result}/Operators/GetPaywallVcNoChecksOperator.swift (96%) rename Sources/SuperwallKit/Paywall/Presentation/{Get Track Result => Get Presentation Result}/PresentationResult.swift (100%) rename Sources/SuperwallKit/Paywall/Presentation/{Get Track Result => Get Presentation Result}/PresentationValueObjc.swift (100%) rename Sources/SuperwallKit/Paywall/Presentation/{Get Track Result/PublicGetTrackResult.swift => Get Presentation Result/PublicGetPresentationResult.swift} (95%) delete mode 100644 Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift create mode 100644 Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogErrorsOperator.swift delete mode 100644 Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentationPipelineError.swift delete mode 100644 Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PrintErrorsOperator.swift create mode 100644 Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperator.swift delete mode 100644 Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/PaywallPresentationFailureReason.swift create mode 100644 Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/PaywallPresentationRequestStatus.swift diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift index 9b3287a46..7218d626d 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift @@ -84,70 +84,6 @@ final class HomeViewController: UIViewController { message: "Wrap your awesome features in register calls like this to remotely paywall your app. You can choose if these are paid features remotely." ) } - - - - - - - -// -// Superwall.shared.getPaywall(forEvent: "campaign_trigger") { paywall, error in -// guard let paywall = paywall else { -// // TODO: make an async version and add error handling -// // skipped or subscribed -// print("[!] skip", error) -// return -// } -// -// print("[!] got paywall") -// -// // prepare the vc -// paywall.presentationWillBegin() -// -// // present however you like -// self.present(paywall, animated: paywall.presentationIsAnimated) { -// -// // let the paywall know its presented -// paywall.presentationDidFinish() -// } -// -// // get its result -// paywall.onDismiss { state in -// print("[!] paywall state:", state) -// } -// } - -// -// Task { -// -// do { -// let paywall = try await Superwall.shared.getPaywallViewController(forEvent: "campaign_trigger") -// print("[!] got paywall") -// -// // prepare the vc -// paywall.presentationWillBegin() -// -// // present however you like -// self.present(paywall, animated: paywall.presentationIsAnimated) { -// // let the paywall know its presented -// print("[!] paywall presented") -// paywall.presentationDidFinish() -// } -// -// // get its result -// paywall.onDismiss { state in -// print("[!] paywall state:", state) -// } -// -// } catch let error { -// print("[!] skipped because: ", error) -// } -// -// } - - - } private func presentAlert(title: String, message: String) { diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift index 64ef6f42c..66c835cc7 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift @@ -242,14 +242,24 @@ enum InternalSuperwallEvent { } } - struct UnableToPresent: TrackableSuperwallEvent { - let state: PaywallPresentationFailureReason + struct PresentationRequest: TrackableSuperwallEvent { + let eventData: EventData? + let type: PresentationRequestType + let status: PaywallPresentationRequestStatus + let statusReason: PaywallPresentationRequestStatusReason? var superwallEvent: SuperwallEvent { - return .paywallPresentationFail(reason: state) + return .paywallPresentationRequest(status: status, reason: statusReason) } var customParameters: [String: Any] = [:] - func getSuperwallParameters() async -> [String: Any] { [:] } + func getSuperwallParameters() async -> [String: Any] { + [ + "eventName": eventData?.name ?? "", + "pipelineType": type.rawValue, + "status": status.rawValue, + "statusReason": statusReason?.description ?? "" + ] + } } struct PaywallOpen: TrackableSuperwallEvent { diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/Tracking.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/Tracking.swift index d9bb693bf..dda75685a 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/Tracking.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/Tracking.swift @@ -92,7 +92,8 @@ extension Superwall { } let presentationRequest = dependencyContainer.makePresentationRequest( presentationInfo, - isPaywallPresented: isPaywallPresented + isPaywallPresented: isPaywallPresented, + type: .presentation ) await internallyPresent(presentationRequest).asyncNoValue() case .closePaywallThenTriggerPaywall: @@ -104,7 +105,8 @@ extension Superwall { } let presentationRequest = dependencyContainer.makePresentationRequest( presentationInfo, - isPaywallPresented: isPaywallPresented + isPaywallPresented: isPaywallPresented, + type: .presentation ) await internallyPresent( presentationRequest, @@ -113,7 +115,8 @@ extension Superwall { case .triggerPaywall: let presentationRequest = dependencyContainer.makePresentationRequest( presentationInfo, - isPaywallPresented: isPaywallPresented + isPaywallPresented: isPaywallPresented, + type: .presentation ) await internallyPresent(presentationRequest).asyncNoValue() case .disallowedEventAsTrigger: diff --git a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift index ca42eeebd..b25e3d6bd 100644 --- a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift @@ -121,8 +121,11 @@ public enum SuperwallEvent { /// When the request to load the paywall's products completed. case paywallProductsLoadComplete(triggeredEventName: String?) - /// When the paywall fails to present. - case paywallPresentationFail(reason: PaywallPresentationFailureReason) + /// Information about the paywall presentation request + case paywallPresentationRequest( + status: PaywallPresentationRequestStatus, + reason: PaywallPresentationRequestStatusReason? + ) var canImplicitlyTriggerPaywall: Bool { switch self { @@ -225,29 +228,13 @@ extension SuperwallEvent { return .init(objcEvent: .paywallProductsLoadFail) case .paywallProductsLoadComplete: return .init(objcEvent: .paywallProductsLoadComplete) - case .paywallPresentationFail(reason: let reason): - switch reason { - case .userIsSubscribed: - return .init(objcEvent: .paywallPresentationFailUserIsSubscribed) - case .holdout: - return .init(objcEvent: .paywallPresentationFailInHoldout) - case .noRuleMatch: - return .init(objcEvent: .paywallPresentationFailNoRuleMatch) - case .eventNotFound: - return .init(objcEvent: .paywallPresentationFailEventNotFound) - case .debuggerLaunched: - return .init(objcEvent: .paywallPresentationFailDebuggerLaunched) - case .alreadyPresented: - return .init(objcEvent: .paywallPresentationFailAlreadyPresented) - case .noPresenter: - return .init(objcEvent: .paywallPresentationFailNoPresenter) - case .noPaywallViewController: - return .init(objcEvent: .paywallPresentationFailNoPaywallViewController) - } + case .paywallPresentationRequest: + return .init(objcEvent: .paywallPresentationRequest) } } } +// Using this to silence warnings. // This is unchecked because of the use of `Any` in `[String: Any]` user attributes. -// Everything else is Sendable except that. +// Also, PaywallInfo is not Sendable. extension SuperwallEvent: @unchecked Sendable {} diff --git a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEventObjc.swift b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEventObjc.swift index 19fcb4bca..ed28d1f6b 100644 --- a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEventObjc.swift +++ b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEventObjc.swift @@ -4,7 +4,6 @@ // // Created by Yusuf Tör on 07/11/2022. // -// swiftlint:disable identifier_name import Foundation @@ -117,29 +116,8 @@ public enum SuperwallEventObjc: Int, CaseIterable { /// When the request to load the paywall's products completed. case paywallProductsLoadComplete - /// Trying to present paywall when debugger is launched. - case paywallPresentationFailDebuggerLaunched - - /// The user is subscribed. - case paywallPresentationFailUserIsSubscribed - - /// The user is in a holdout group. - case paywallPresentationFailInHoldout - - /// No rules defined in the campaign for the event matched. - case paywallPresentationFailNoRuleMatch - - /// The event provided was not found in any campaign on the dashboard. - case paywallPresentationFailEventNotFound - - /// There was an error getting the paywall view controller. - case paywallPresentationFailNoPaywallViewController - - /// There isn't a view to present the paywall on. - case paywallPresentationFailNoPresenter - - /// There's already a paywall presented. - case paywallPresentationFailAlreadyPresented + /// Information about a paywall presentation request + case paywallPresentationRequest public init(event: SuperwallEvent) { self = event.backingData.objcEvent @@ -211,22 +189,8 @@ public enum SuperwallEventObjc: Int, CaseIterable { return "paywallProductsLoad_fail" case .paywallProductsLoadComplete: return "paywallProductsLoad_complete" - case .paywallPresentationFailUserIsSubscribed: - return "paywallPresentationFail_userIsSubscribed" - case .paywallPresentationFailInHoldout: - return "paywallPresentationFail_holdout" - case .paywallPresentationFailNoRuleMatch: - return "paywallPresentationFail_noRuleMatch" - case .paywallPresentationFailEventNotFound: - return "paywallPresentationFail_eventNotFound" - case .paywallPresentationFailDebuggerLaunched: - return "paywallPresentationFail_debuggerLaunched" - case .paywallPresentationFailAlreadyPresented: - return "paywallPresentationFail_alreadyPresented" - case .paywallPresentationFailNoPresenter: - return "paywallPresentationFail_noPresenter" - case .paywallPresentationFailNoPaywallViewController: - return "paywallPresentationFail_noPaywallViewController" + case .paywallPresentationRequest: + return "paywallPresentationRequest" } } } diff --git a/Sources/SuperwallKit/Debug/DebugViewController.swift b/Sources/SuperwallKit/Debug/DebugViewController.swift index 3231a1b98..d3f28ea27 100644 --- a/Sources/SuperwallKit/Debug/DebugViewController.swift +++ b/Sources/SuperwallKit/Debug/DebugViewController.swift @@ -435,7 +435,8 @@ final class DebugViewController: UIViewController { presenter: self, isDebuggerLaunched: true, subscriptionStatus: inactiveSubscriptionPublisher, - isPaywallPresented: Superwall.shared.isPaywallPresented + isPaywallPresented: Superwall.shared.isPaywallPresented, + type: .presentation ) cancellable = Superwall.shared diff --git a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift index 5db438a80..e6931ba8a 100644 --- a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift +++ b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift @@ -243,7 +243,8 @@ extension DependencyContainer: RequestFactory { presenter: UIViewController? = nil, isDebuggerLaunched: Bool? = nil, subscriptionStatus: AnyPublisher? = nil, - isPaywallPresented: Bool + isPaywallPresented: Bool, + type: PresentationRequestType ) -> PresentationRequest { return PresentationRequest( presentationInfo: presentationInfo, @@ -252,7 +253,8 @@ extension DependencyContainer: RequestFactory { flags: .init( isDebuggerLaunched: isDebuggerLaunched ?? debugManager.isDebuggerLaunched, subscriptionStatus: subscriptionStatus ?? Superwall.shared.$subscriptionStatus.eraseToAnyPublisher(), - isPaywallPresented: isPaywallPresented + isPaywallPresented: isPaywallPresented, + type: type ), dependencyContainer: self ) diff --git a/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift b/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift index cd5821080..6c43f0fd2 100644 --- a/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift +++ b/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift @@ -41,7 +41,8 @@ protocol RequestFactory: AnyObject { presenter: UIViewController?, isDebuggerLaunched: Bool?, subscriptionStatus: AnyPublisher?, - isPaywallPresented: Bool + isPaywallPresented: Bool, + type: PresentationRequestType ) -> PresentationRequest } diff --git a/Sources/SuperwallKit/Models/Paywall/Paywall.swift b/Sources/SuperwallKit/Models/Paywall/Paywall.swift index 69d09c4bb..13c4e2518 100644 --- a/Sources/SuperwallKit/Models/Paywall/Paywall.swift +++ b/Sources/SuperwallKit/Models/Paywall/Paywall.swift @@ -85,7 +85,7 @@ struct Paywall: Decodable { var isFreeTrialAvailable = false /// The reason for closing the paywall. - var closeReason: PaywallCloseReason? = nil + var closeReason: PaywallCloseReason? /// Determines whether a paywall executes the /// ``Superwall/register(event:params:handler:feature:)`` feature block if the diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywallViewController.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywallViewController.swift index b86083fb8..59c6f2d98 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywallViewController.swift @@ -33,6 +33,7 @@ extension Superwall { .checkSubscriptionStatus(paywallStatePublisher) .confirmPaywallAssignment() .storePresentationObjects(presentationSubject, paywallStatePublisher) + .logErrors(from: request) .extractPaywallViewController(paywallStatePublisher) } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/CheckSubscriptionStatusOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/CheckSubscriptionStatusOperator.swift index 3debd04bc..dde9801c3 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/CheckSubscriptionStatusOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/CheckSubscriptionStatusOperator.swift @@ -4,7 +4,6 @@ // // Created by Jake Mor on 4/28/23. // -// swiftlint:disable strict_fileprivate function_body_length import UIKit import Combine @@ -30,19 +29,12 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error presentationCondition: input.paywallViewController.paywall.presentation.condition ) ) { - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent( - state: .userIsSubscribed - ) - await Superwall.shared.track(trackedEvent) - } let state: PaywallState = .skipped(.userIsSubscribed) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) throw PresentationPipelineError.userIsSubscribed } - let sessionEventsManager = input.request.dependencyContainer.sessionEventsManager await sessionEventsManager?.triggerSession.activateSession( for: input.request.presentationInfo, diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift index 5af9dddd2..7b562b25c 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift @@ -4,14 +4,13 @@ // // Created by Jake Mor on 10/9/21. // -// swiftlint:disable line_length file_length function_body_length import Foundation import Combine import UIKit extension Superwall { - /// Gets the ``PaywallViewController`` object, which you can present + /// Gets the ``PaywallViewController`` object for an event, which you can present /// however you want. /// /// To use this you **must** follow the following steps: @@ -24,7 +23,17 @@ extension Superwall { /// completes. /// /// - Note: The remotely configured presentation style will be ignored, it is up to you - /// to set it programmatically. + /// to set it programmatically. + /// + /// - Parameters: + /// - event: The name of the event, as you have defined on the Superwall dashboard. + /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules + /// of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any + /// JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will + /// be dropped. + /// - paywallOverrides: An optional ``PaywallOverrides`` object whose parameters override the paywall defaults. Use this to override products, presentation style, and whether it ignores the subscription status. Defaults to `nil`. + /// - completion: A completion block that contains a `Result` type, containing either a success case with a + /// ``PaywallViewController`` object, or a failure case with an `Error`. public func getPaywallViewController( forEvent event: String, params: [String: Any]? = nil, @@ -59,6 +68,18 @@ extension Superwall { /// /// - Note: The remotely configured presentation style will be ignored, it is up to you /// to set it programmatically. + /// + /// - Parameters: + /// - event: The name of the event, as you have defined on the Superwall dashboard. + /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules + /// of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any + /// JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will + /// be dropped. + /// - paywallOverrides: An optional ``PaywallOverrides`` object whose parameters override the paywall defaults. Use this to override products, presentation style, and whether it ignores the subscription status. Defaults to `nil`. + /// + /// - Returns A ``PaywallViewController`` object. + /// - Throws: An `Error` explaining why it couldn't get the view controller. + @MainActor public func getPaywallViewController( forEvent: String, params: [String: Any]? = nil, @@ -78,7 +99,8 @@ extension Superwall { let presentationRequest = self.dependencyContainer.makePresentationRequest( .explicitTrigger(trackResult.data), paywallOverrides: paywallOverrides, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) return self.internallyGetPaywallViewController(presentationRequest) } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/GetTrackResultLogic.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/GetPresentationResultLogic.swift similarity index 93% rename from Sources/SuperwallKit/Paywall/Presentation/Get Track Result/GetTrackResultLogic.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/GetPresentationResultLogic.swift index 19180f1d7..8342c54e0 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/GetTrackResultLogic.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/GetPresentationResultLogic.swift @@ -7,7 +7,7 @@ import Foundation -enum GetTrackResultLogic { +enum GetPresentationResultLogic { static func convertTriggerResult(_ triggerResult: TriggerResult) -> PresentationResult { switch triggerResult { case .eventNotFound: diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/InternalGetTrackResult.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/InternalGetPresentationResult.swift similarity index 67% rename from Sources/SuperwallKit/Paywall/Presentation/Get Track Result/InternalGetTrackResult.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/InternalGetPresentationResult.swift index e52b69887..ac3405bde 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/InternalGetTrackResult.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/InternalGetPresentationResult.swift @@ -8,12 +8,12 @@ import Foundation import Combine -enum GetTrackResultError: Error, Equatable { +enum GetPresentationResultError: Error, Equatable { case willNotPresent(TriggerResult) case userIsSubscribed case paywallNotAvailable - static func == (lhs: GetTrackResultError, rhs: GetTrackResultError) -> Bool { + static func == (lhs: GetPresentationResultError, rhs: GetPresentationResultError) -> Bool { switch (lhs, rhs) { case (.willNotPresent, .willNotPresent), (.userIsSubscribed, .userIsSubscribed), @@ -26,18 +26,18 @@ enum GetTrackResultError: Error, Equatable { } extension Superwall { - func getTrackResult(for request: PresentationRequest) async -> PresentationResult { + func getPresentationResult(for request: PresentationRequest) async -> PresentationResult { let presentationSubject = PresentationSubject(request) return await presentationSubject .eraseToAnyPublisher() .waitToPresent() - .logPresentation("Called Superwall.shared.getTrackResult") - .evaluateRules(isPreemptive: true) + .logPresentation("Called Superwall.shared.getPresentationResult") + .evaluateRules() .checkForPaywallResult() - .getPaywallViewController(pipelineType: .getTrackResult) + .getPaywallViewController(pipelineType: .getPresentationResult) .checkPaywallIsPresentable() - .convertToTrackResult() + .convertToPresentationResult() .async() } } @@ -46,7 +46,7 @@ extension Superwall { extension Publisher where Output == PresentationResult { /// Waits and returns the first value of the publisher. /// - /// This handles the error cases thrown by `getTrackResult(for:)`. + /// This handles the error cases thrown by `getPresentationResult(for:)`. @discardableResult func async() async -> Output { await withCheckedContinuation { continuation in @@ -56,10 +56,15 @@ extension Publisher where Output == PresentationResult { switch completion { case .failure(let error): switch error { - case let error as GetTrackResultError: + case let error as GetPresentationResultError: + Logger.debug( + logLevel: .info, + scope: .paywallPresentation, + message: "Skipped paywall presentation: \(error)" + ) switch error { case .willNotPresent(let result): - let trackResult = GetTrackResultLogic.convertTriggerResult(result) + let trackResult = GetPresentationResultLogic.convertTriggerResult(result) continuation.resume(with: .success(trackResult)) case .userIsSubscribed: continuation.resume(with: .success(.userIsSubscribed)) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/CheckForPaywallResultOperator.swift similarity index 92% rename from Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultOperator.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/CheckForPaywallResultOperator.swift index 8d0d29c8a..07d36cb04 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/CheckForPaywallResultOperator.swift @@ -28,7 +28,7 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro experiment: experiment ) default: - throw GetTrackResultError.willNotPresent(input.triggerResult) + throw GetPresentationResultError.willNotPresent(input.triggerResult) } } .eraseToAnyPublisher() diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/CheckPaywallIsPresentableOperator.swift similarity index 94% rename from Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableOperator.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/CheckPaywallIsPresentableOperator.swift index 685487579..9da44461a 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/CheckPaywallIsPresentableOperator.swift @@ -20,7 +20,7 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error presentationCondition: input.paywallViewController.paywall.presentation.condition ) ) { - throw GetTrackResultError.userIsSubscribed + throw GetPresentationResultError.userIsSubscribed } return input.triggerResult } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/ConvertToTrackResultOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/ConvertToTrackResultOperator.swift similarity index 67% rename from Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/ConvertToTrackResultOperator.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/ConvertToTrackResultOperator.swift index fd05c6a5e..d0a46b266 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/ConvertToTrackResultOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/ConvertToTrackResultOperator.swift @@ -10,9 +10,9 @@ import Combine extension AnyPublisher where Output == TriggerResult, Failure == Error { /// Checks whether the trigger result indicates that a paywall should show. - func convertToTrackResult() -> AnyPublisher { + func convertToPresentationResult() -> AnyPublisher { map { input in - return GetTrackResultLogic.convertTriggerResult(input) + return GetPresentationResultLogic.convertTriggerResult(input) } .eraseToAnyPublisher() } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/GetPaywallVcNoChecksOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/GetPaywallVcNoChecksOperator.swift similarity index 96% rename from Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/GetPaywallVcNoChecksOperator.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/GetPaywallVcNoChecksOperator.swift index c9d2c38b8..8b47194de 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/Operators/GetPaywallVcNoChecksOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/Operators/GetPaywallVcNoChecksOperator.swift @@ -42,7 +42,7 @@ extension AnyPublisher where Output == TriggerResultResponsePipelineOutput, Fail ) return output } catch { - throw GetTrackResultError.paywallNotAvailable + throw GetPresentationResultError.paywallNotAvailable } } .eraseToAnyPublisher() diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PresentationResult.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PresentationResult.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PresentationResult.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PresentationResult.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PresentationValueObjc.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PresentationValueObjc.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PresentationValueObjc.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PresentationValueObjc.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PublicGetTrackResult.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift similarity index 95% rename from Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PublicGetTrackResult.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift index 266d8b669..28e647486 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Track Result/PublicGetTrackResult.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift @@ -7,6 +7,8 @@ import Foundation +typealias PresentationPipelineError = PaywallPresentationRequestStatusReason + extension Superwall { /// Preemptively get the result of tracking an event. /// @@ -52,10 +54,11 @@ extension Superwall { let presentationRequest = dependencyContainer.makePresentationRequest( .explicitTrigger(eventData), isDebuggerLaunched: false, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPresentationResult ) - return await getTrackResult(for: presentationRequest) + return await getPresentationResult(for: presentationRequest) } /// Preemptively get the result of tracking an event. diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift index af94899c7..292f6619e 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift @@ -44,7 +44,7 @@ extension Superwall { .confirmPaywallAssignment() .presentPaywall(paywallStatePublisher) .storePresentationObjects(presentationSubject, paywallStatePublisher) - .printErrors() + .logErrors(from: request) .subscribe(Subscribers.Sink( receiveCompletion: { _ in }, receiveValue: { _ in } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift deleted file mode 100644 index a17f9a32a..000000000 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperator.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// File.swift -// -// -// Created by Yusuf Tör on 23/09/2022. -// - -import Foundation -import Combine - -extension AnyPublisher where Output == PresentationRequest, Failure == Error { - /// Waits for config to be received and the identity and subscription status of the user to - /// be established. - func waitToPresent() -> AnyPublisher { - subscribe(on: DispatchQueue.global(qos: .userInitiated)) - // TODO: PRINT OUT HERE - .flatMap { request in - zip( - request.dependencyContainer.identityManager.hasIdentity, - request.dependencyContainer.configManager.hasConfig, - request.flags.subscriptionStatus - .filter { $0 != .unknown } - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - ) - } - .first() - .map { request, _, _, _ in - Logger.debug( - logLevel: .info, - scope: .paywallPresentation, - message: "Retrieved identity, configuration and subscription status." - ) - return request - } - .eraseToAnyPublisher() - } -} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift index 2f7299aab..ed274625d 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift @@ -23,10 +23,6 @@ extension AnyPublisher where Output == (PresentationRequest, DebugInfo), Failure title: "Debugger Is Presented", value: "Trying to present paywall when debugger is launched." ) - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .debuggerLaunched) - await Superwall.shared.track(trackedEvent) - } let state: PaywallState = .skipped(.error(error)) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift index ffd24d9dc..24889f0db 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift @@ -33,11 +33,6 @@ extension AnyPublisher where Output == PresentationRequest, Failure == Error { title: "Paywall Already Presented", value: "You can only present one paywall at a time." ) - Task.detached(priority: .utility) { - // TODO: MOVE TO COMPLETION BLOCK? SHOULD THAT BE ON EVERY PIPELINE OR JUST PRESENT - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .alreadyPresented) - await Superwall.shared.track(trackedEvent) - } let state: PaywallState = .skipped(.error(error)) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift index 881a6d13b..72569b34d 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift @@ -4,7 +4,7 @@ // // Created by Yusuf Tör on 26/09/2022. // -// swiftlint:disable strict_fileprivate function_body_length +// swiftlint:disable strict_fileprivate import UIKit import Combine @@ -38,12 +38,6 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error presentationCondition: input.paywallViewController.paywall.presentation.condition ) ) { - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent( - state: .userIsSubscribed - ) - await Superwall.shared.track(trackedEvent) - } let state: PaywallState = .skipped(.userIsSubscribed) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) @@ -73,10 +67,6 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error title: "No UIViewController to present paywall on", value: "This usually happens when you call this method before a window was made key and visible." ) - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noPresenter) - await Superwall.shared.track(trackedEvent) - } let state: PaywallState = .skipped(.error(error)) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperator.swift index 8a2da45c6..26e6932ec 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperator.swift @@ -21,10 +21,6 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro default: let subscriptionStatus = await input.request.flags.subscriptionStatus.async() if subscriptionStatus == .active { - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .userIsSubscribed) - await Superwall.shared.track(trackedEvent) - } paywallStatePublisher.send(.skipped(.userIsSubscribed)) paywallStatePublisher.send(completion: .finished) throw PresentationPipelineError.userIsSubscribed diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/EvaluateRulesOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/EvaluateRulesOperator.swift index bb30fd27e..508c027c3 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/EvaluateRulesOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/EvaluateRulesOperator.swift @@ -21,9 +21,8 @@ extension AnyPublisher where Output == (PresentationRequest, DebugInfo), Failure /// - Parameters: /// - configManager: A `ConfigManager` object used for dependency injection. /// - storgate: A `Storage` object used for dependency injection. - /// - isPreemptive: A boolean that determines whether the rules are being evaluated before actually tracking an event. /// If `true`, then it doesn't save the occurrence count of the rule. - func evaluateRules(isPreemptive: Bool = false) -> AnyPublisher { + func evaluateRules() -> AnyPublisher { asyncMap { request, debugInfo in if let eventData = request.presentationInfo.eventData { let assignmentLogic = RuleLogic( @@ -34,7 +33,7 @@ extension AnyPublisher where Output == (PresentationRequest, DebugInfo), Failure let eventOutcome = await assignmentLogic.evaluateRules( forEvent: eventData, triggers: request.dependencyContainer.configManager.triggersByEventName, - isPreemptive: isPreemptive + isPreemptive: request.flags.type == .getPresentationResult ) let confirmableAssignment = eventOutcome.confirmableAssignment @@ -49,7 +48,7 @@ extension AnyPublisher where Output == (PresentationRequest, DebugInfo), Failure guard let paywallId = request.presentationInfo.identifier else { // This error will never be thrown. Just preferring this // to force unwrapping. - throw PresentationPipelineError.cancelled + throw PresentationPipelineError.noPaywallViewController } return AssignmentPipelineOutput( request: request, diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift index b9ee2fb71..eed7523da 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift @@ -17,7 +17,7 @@ struct PaywallVcPipelineOutput { extension AnyPublisher where Output == TriggerResultResponsePipelineOutput, Failure == Error { enum PipelineType { - case getTrackResult + case getPresentationResult case presentation(PassthroughSubject) } /// Requests the paywall view controller to present. If an error occurred during this, @@ -62,8 +62,8 @@ extension AnyPublisher where Output == TriggerResultResponsePipelineOutput, Fail return output } catch { switch pipelineType { - case .getTrackResult: - throw GetTrackResultError.paywallNotAvailable + case .getPresentationResult: + throw GetPresentationResultError.paywallNotAvailable case .presentation(let paywallStatePublisher): throw await presentationFailure(error, input, paywallStatePublisher) } @@ -85,14 +85,10 @@ extension AnyPublisher where Output == TriggerResultResponsePipelineOutput, Fail shouldIgnoreSubscriptionStatus: input.request.paywallOverrides?.ignoreSubscriptionStatus ) ) { - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .userIsSubscribed) - await Superwall.shared.track(trackedEvent) - } let state: PaywallState = .skipped(.userIsSubscribed) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) - return PresentationPipelineError.cancelled + return PresentationPipelineError.userIsSubscribed } Logger.debug( @@ -102,12 +98,8 @@ extension AnyPublisher where Output == TriggerResultResponsePipelineOutput, Fail info: input.debugInfo, error: error ) - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noPaywallViewController) - await Superwall.shared.track(trackedEvent) - } paywallStatePublisher.send(.skipped(.error(error))) paywallStatePublisher.send(completion: .finished) - return PresentationPipelineError.cancelled + return PresentationPipelineError.noPaywallViewController } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift index add28c0f9..b132eae0c 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift @@ -29,7 +29,7 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro _ paywallStatePublisher: PassthroughSubject ) -> AnyPublisher { asyncMap { input in - var errorType: PresentationPipelineError = .cancelled + let errorType: PresentationPipelineError switch input.triggerResult { case .paywall(let experiment): @@ -47,13 +47,7 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro on: input.request.presenter, triggerResult: input.triggerResult ) - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent( - state: .holdout(experiment) - ) - await Superwall.shared.track(trackedEvent) - } - errorType = .holdout + errorType = .holdout(experiment) paywallStatePublisher.send(.skipped(.holdout(experiment))) case .noRuleMatch: let sessionEventsManager = input.request.dependencyContainer.sessionEventsManager @@ -62,17 +56,9 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro on: input.request.presenter, triggerResult: input.triggerResult ) - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noRuleMatch) - await Superwall.shared.track(trackedEvent) - } errorType = .noRuleMatch paywallStatePublisher.send(.skipped(.noRuleMatch)) case .eventNotFound: - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .eventNotFound) - await Superwall.shared.track(trackedEvent) - } errorType = .eventNotFound paywallStatePublisher.send(.skipped(.eventNotFound)) case let .error(error): @@ -83,10 +69,6 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro info: input.debugInfo, error: error ) - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noPaywallViewController) - await Superwall.shared.track(trackedEvent) - } errorType = .noPaywallViewController paywallStatePublisher.send(.skipped(.error(error))) } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogErrorsOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogErrorsOperator.swift new file mode 100644 index 000000000..0e7f950ef --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/LogErrorsOperator.swift @@ -0,0 +1,34 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 02/05/2023. +// + +import Foundation +import Combine + +extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Error { + func logErrors(from request: PresentationRequest) -> PresentablePipelineOutputPublisher { + tryCatch { error -> PresentablePipelineOutputPublisher in + Task.detached { + if let reason = error as? PresentationPipelineError { + let trackedEvent = InternalSuperwallEvent.PresentationRequest( + eventData: request.presentationInfo.eventData, + type: request.flags.type, + status: .noPresentation, + statusReason: reason + ) + await Superwall.shared.track(trackedEvent) + } + } + Logger.debug( + logLevel: .info, + scope: .paywallPresentation, + message: "Skipped paywall presentation: \(error)" + ) + throw error + } + .eraseToAnyPublisher() + } +} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift index 019e091cf..0168bb100 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift @@ -22,12 +22,16 @@ extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Err flatMap { input in Future { promise in Task { - await MainActor.run { - Logger.debug( - logLevel: .info, - scope: .paywallPresentation, - message: "Presenting paywall" + Task.detached { + let trackedEvent = InternalSuperwallEvent.PresentationRequest( + eventData: input.request.presentationInfo.eventData, + type: input.request.flags.type, + status: .presentation, + statusReason: nil ) + await Superwall.shared.track(trackedEvent) + } + await MainActor.run { input.paywallViewController.present( on: input.presenter, eventData: input.request.presentationInfo.eventData, @@ -51,10 +55,6 @@ extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Err title: "Paywall Already Presented", value: "Trying to present paywall while another paywall is presented." ) - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .alreadyPresented) - await Superwall.shared.track(trackedEvent) - } paywallStatePublisher.send(.skipped(.error(error))) paywallStatePublisher.send(completion: .finished) promise(.failure(PresentationPipelineError.paywallAlreadyPresented)) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentationPipelineError.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentationPipelineError.swift deleted file mode 100644 index 54e6d03ae..000000000 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentationPipelineError.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// File.swift -// -// -// Created by Yusuf Tör on 26/09/2022. -// - -import Foundation - -enum PresentationPipelineError: Error { - case debuggerPresented - case paywallAlreadyPresented - case userIsSubscribed - case holdout - case noRuleMatch - case eventNotFound - case noPaywallViewController - case noPresenter - case cancelled - case unknown -} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PrintErrorsOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PrintErrorsOperator.swift deleted file mode 100644 index cc6799885..000000000 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PrintErrorsOperator.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// File.swift -// -// -// Created by Yusuf Tör on 02/05/2023. -// - -import Foundation -import Combine - -extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Error { - func printErrors() -> PresentablePipelineOutputPublisher { - handleEvents(receiveCompletion: { completion in - switch completion { - case .failure(let error): - Logger.debug( - logLevel: .error, - scope: .paywallPresentation, - message: "Skipped paywall presentation: \(error)" - ) - case .finished: - break - } - }) - .eraseToAnyPublisher() - } -} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperator.swift new file mode 100644 index 000000000..a1460eb9f --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperator.swift @@ -0,0 +1,80 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 23/09/2022. +// +// swiftlint:disable line_length + +import Foundation +import Combine + +extension AnyPublisher where Output == PresentationRequest, Failure == Error { + /// Waits for config to be received and the identity and subscription status of the user to + /// be established. + func waitToPresent() -> AnyPublisher { + subscribe(on: DispatchQueue.global(qos: .userInitiated)) + .flatMap { _ in + return startTimer() + } + .flatMap { request, timer in + zip( + request.dependencyContainer.identityManager.hasIdentity, + request.dependencyContainer.configManager.hasConfig, + request.flags.subscriptionStatus + .filter { $0 != .unknown } + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + ) + .map { (request, timer, $0.0, $0.1, $0.2) } + } + .first() + .map { request, timer, _, _, _ in + timer.invalidate() + return request + } + .eraseToAnyPublisher() + } + + private func startTimer() -> (AnyPublisher<(PresentationRequest, Timer), Failure>) { + map { request in + let timer = Timer( + timeInterval: 5, + repeats: false + ) { [request] _ in + Task.detached { + let trackedEvent = InternalSuperwallEvent.PresentationRequest( + eventData: request.presentationInfo.eventData, + type: request.flags.type, + status: .timeout, + statusReason: nil + ) + await Superwall.shared.track(trackedEvent) + + var timeoutReason = "" + let subscriptionStatus = await request.flags.subscriptionStatus.async() + if subscriptionStatus == .unknown { + timeoutReason += "\nSuperwall.shared.subscriptionStatus is currently \"unknown\". A paywall cannot show in this state." + } + if request.dependencyContainer.configManager.config == nil { + timeoutReason += "\nThe config for the user has not returned from the server." + } + + let hasIdentity = await request.dependencyContainer.identityManager.hasIdentity.async() + if !hasIdentity { + timeoutReason += "\nThe user's identity has not been set." + } + Logger.debug( + logLevel: .info, + scope: .paywallPresentation, + message: "Timeout: Waiting for >5 seconds to continue paywall request. Your paywall may not show because:\(timeoutReason)" + ) + } + } + RunLoop.main.add(timer, forMode: .default) + + return (request, timer) + } + .eraseToAnyPublisher() + } +} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/PaywallPresentationFailureReason.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/PaywallPresentationFailureReason.swift deleted file mode 100644 index 597dcc67f..000000000 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/PaywallPresentationFailureReason.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// File.swift -// -// -// Created by Yusuf Tör on 23/01/2023. -// - -import Foundation - -/// The reason to why the paywall couldn't present. -public enum PaywallPresentationFailureReason { - /// Trying to present paywall when debugger is launched. - case debuggerLaunched - - /// The user is subscribed. - case userIsSubscribed - - /// The user is in a holdout group. - case holdout(Experiment) - - /// No rules defined in the campaign for the event matched. - case noRuleMatch - - /// The event provided was not found in any campaign on the dashboard. - case eventNotFound - - /// There was an error getting the paywall view controller. - case noPaywallViewController - - /// There isn't a view to present the paywall on. - case noPresenter - - /// There's already a paywall presented. - case alreadyPresented -} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/PaywallPresentationRequestStatus.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/PaywallPresentationRequestStatus.swift new file mode 100644 index 000000000..2c9e35ade --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/PaywallPresentationRequestStatus.swift @@ -0,0 +1,69 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 26/09/2022. +// + +import Foundation + +/// The status of the paywall request +public enum PaywallPresentationRequestStatus: String { + /// The request will result in a paywall presentation. + case presentation + + /// The request won't result in a paywall presentation. + case noPresentation = "no_presentation" + + /// There was a timeout when trying to get the user's subscription status, identity + /// or configuration from the server. + case timeout +} + +/// The reason to why the paywall couldn't present. +public enum PaywallPresentationRequestStatusReason: Error, CustomStringConvertible { + /// Trying to present paywall when debugger is launched. + case debuggerPresented + + /// There's already a paywall presented. + case paywallAlreadyPresented + + /// The user is subscribed. + case userIsSubscribed + + /// The user is in a holdout group. + case holdout(Experiment) + + /// No rules defined in the campaign for the event matched. + case noRuleMatch + + /// The event provided was not found in any campaign on the dashboard. + case eventNotFound + + /// There was an error getting the paywall view controller. + case noPaywallViewController + + /// There isn't a view to present the paywall on. + case noPresenter + + public var description: String { + switch self { + case .debuggerPresented: + return "debugger_presented" + case .paywallAlreadyPresented: + return "paywall_already_presented" + case .userIsSubscribed: + return "user_is_subscribed" + case .holdout: + return "holdout" + case .noRuleMatch: + return "no_rule_match" + case .eventNotFound: + return "event_not_found" + case .noPaywallViewController: + return "no_paywall_view_controller" + case .noPresenter: + return "no_presenter" + } + } +} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationRequest.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationRequest.swift index e3beb20a7..d19564691 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationRequest.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationRequest.swift @@ -8,6 +8,12 @@ import UIKit import Combine +enum PresentationRequestType: String { + case presentation + case getPaywallViewController + case getPresentationResult +} + /// Defines the information needed to request the presentation of a paywall. struct PresentationRequest { /// The type of trigger (implicit/explicit/fromIdentifier), and associated data. @@ -23,6 +29,7 @@ struct PresentationRequest { var isDebuggerLaunched: Bool var subscriptionStatus: AnyPublisher var isPaywallPresented: Bool + var type: PresentationRequestType } var flags: Flags @@ -46,7 +53,8 @@ extension PresentationRequest: Stubbable { .explicitTrigger(.stub()), paywallOverrides: nil, isDebuggerLaunched: false, - isPaywallPresented: false + isPaywallPresented: false, + type: .presentation ) } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index 657492a05..48a13ff97 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -534,7 +534,8 @@ extension Superwall { let presentationRequest = self.dependencyContainer.makePresentationRequest( .explicitTrigger(trackResult.data), paywallOverrides: paywallOverrides, - isPaywallPresented: isPaywallPresented + isPaywallPresented: isPaywallPresented, + type: .presentation ) return self.internallyPresent(presentationRequest) } diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index beba558d4..09ae12252 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -607,10 +607,10 @@ extension PaywallViewController { receiveCompletion: { _ in }, receiveValue: { paywallState in switch paywallState { - case .dismissed(_, let dismissState): - handler(dismissState) - default: - break + case .dismissed(_, let dismissState): + handler(dismissState) + default: + break } } )) @@ -660,7 +660,7 @@ extension PaywallViewController: PaywallMessageHandlerDelegate { // MARK: - View Lifecycle extension PaywallViewController { - override public func viewWillAppear(_ animated: Bool) { + override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) guard isActive else { return diff --git a/Sources/SuperwallKit/StoreKit/Products/ProductsFetcherSK1.swift b/Sources/SuperwallKit/StoreKit/Products/ProductsFetcherSK1.swift index f75b04414..2a46aaae3 100644 --- a/Sources/SuperwallKit/StoreKit/Products/ProductsFetcherSK1.swift +++ b/Sources/SuperwallKit/StoreKit/Products/ProductsFetcherSK1.swift @@ -156,7 +156,7 @@ extension ProductsFetcherSK1: SKProductsRequestDelegate { self.productsByRequest.removeValue(forKey: request) if response.products.isEmpty, - !requestProducts.isEmpty { + !requestProducts.isEmpty { var errorMessage = "Could not load products" if let paywallName = paywallNameByRequest[request] { errorMessage += " from paywall \"\(paywallName)\"" diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift index b5aa4ea22..cba626788 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift @@ -30,10 +30,10 @@ final class CheckForPaywallResultTests: XCTestCase { receiveCompletion: { completion in switch completion { case .failure(let error): - guard let error = error as? GetTrackResultError else { + guard let error = error as? GetPresentationResultError else { return XCTFail("Wrong type of error") } - XCTAssertEqual(error, GetTrackResultError.willNotPresent(.eventNotFound)) + XCTAssertEqual(error, GetPresentationResultError.willNotPresent(.eventNotFound)) expectation.fulfill() case .finished: XCTFail("Shouldn't have finished") @@ -63,7 +63,7 @@ final class CheckForPaywallResultTests: XCTestCase { receiveCompletion: { completion in switch completion { case .failure(let error): - guard let error = error as? GetTrackResultError else { + guard let error = error as? GetPresentationResultError else { return XCTFail("Wrong type of error") } guard case .willNotPresent(let result) = error else { @@ -99,7 +99,7 @@ final class CheckForPaywallResultTests: XCTestCase { receiveCompletion: { completion in switch completion { case .failure(let error): - guard let error = error as? GetTrackResultError else { + guard let error = error as? GetPresentationResultError else { return XCTFail("Wrong type of error") } guard case .willNotPresent(let result) = error else { @@ -139,7 +139,7 @@ final class CheckForPaywallResultTests: XCTestCase { receiveCompletion: { completion in switch completion { case .failure(let error): - guard let error = error as? GetTrackResultError else { + guard let error = error as? GetPresentationResultError else { return XCTFail("Wrong type of error") } guard case .willNotPresent(let result) = error else { diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift index d4a1329f2..132a1798e 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift @@ -37,7 +37,7 @@ final class CheckPaywallIsPresentableTests: XCTestCase { receiveCompletion: { completion in switch completion { case .failure(let error): - guard let error = error as? GetTrackResultError else { + guard let error = error as? GetPresentationResultError else { return XCTFail("Wrong type of error") } XCTAssertEqual(error, .userIsSubscribed) From 60a003ee69682f0c15ae5f786953a4a7d5a5a576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Wed, 3 May 2023 17:08:32 +0100 Subject: [PATCH 10/27] Fix issue with dismissal of paywall with paywall_decline --- .../Internal Tracking/TrackingLogic.swift | 5 ++--- .../Superwall Event/SuperwallEvent.swift | 2 +- .../PaywallViewController.swift | 1 - Sources/SuperwallKit/Superwall.swift | 21 +++++++++---------- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift index 2a2468d97..1da0c0e32 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/TrackingLogic.swift @@ -153,11 +153,11 @@ enum TrackingLogic { let notAllowedReferringEventNames: Set = [ SuperwallEventObjc.transactionAbandon.description, SuperwallEventObjc.transactionFail.description, - SuperwallEventObjc.paywallDecline.description, + SuperwallEventObjc.paywallDecline.description ] if let referringEventName = paywallViewController?.paywallInfo.presentedByEventWithName, - notAllowedReferringEventNames.contains(referringEventName) { + notAllowedReferringEventNames.contains(referringEventName) { return .dontTriggerPaywall } @@ -176,7 +176,6 @@ enum TrackingLogic { return .closePaywallThenTriggerPaywall } - if paywallViewController != nil { return .dontTriggerPaywall } diff --git a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift index a2b42f2c1..98512c392 100644 --- a/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Superwall Event/SuperwallEvent.swift @@ -191,7 +191,7 @@ extension SuperwallEvent { case .paywallClose: return .init(objcEvent: .paywallClose) case .paywallDecline: - return .init(objcEvent: .paywallDecline) + return .init(objcEvent: .paywallDecline) case .transactionStart: return .init(objcEvent: .transactionStart) case .transactionFail: diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 48545c059..b3d0bb804 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -722,7 +722,6 @@ extension PaywallViewController { closeReason: PaywallCloseReason = .systemLogic, completion: (() -> Void)? = nil ) { - calledDismiss = true paywall.closeReason = closeReason willDismiss() diff --git a/Sources/SuperwallKit/Superwall.swift b/Sources/SuperwallKit/Superwall.swift index 65ef28c15..c0124d854 100644 --- a/Sources/SuperwallKit/Superwall.swift +++ b/Sources/SuperwallKit/Superwall.swift @@ -459,20 +459,19 @@ extension Superwall: PaywallViewControllerDelegate { switch paywallEvent { case .closed: - let trackedEvent = InternalSuperwallEvent.PaywallDecline(paywallInfo: paywallViewController.paywallInfo) - // TODO: if this results in another presentation, - // we need to stop the following dismiss call from - // completing the state publisher and we need this paywall - // to instead execute the feature block upon completion - // harder to handle this vs txn_abandon bc this is explicit ... - await Superwall.shared.track(trackedEvent) + let result = await getPresentationResult(forEvent: "paywall_decline") - dismiss( - paywallViewController, - result: .closed - ) + if case .paywall = result, + paywallViewController.paywallInfo.presentedByEventWithName == SuperwallEventObjc.paywallDecline.description { + dismiss( + paywallViewController, + result: .closed + ) + } + + await Superwall.shared.track(trackedEvent) case .initiatePurchase(let productId): await dependencyContainer.transactionManager.purchase( productId, From 29c1eff0e12768a77b894d796009f3df1dad40a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Wed, 3 May 2023 17:27:31 +0100 Subject: [PATCH 11/27] Update logic a bit --- Sources/SuperwallKit/Superwall.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/SuperwallKit/Superwall.swift b/Sources/SuperwallKit/Superwall.swift index c0124d854..56c58e8d3 100644 --- a/Sources/SuperwallKit/Superwall.swift +++ b/Sources/SuperwallKit/Superwall.swift @@ -464,7 +464,9 @@ extension Superwall: PaywallViewControllerDelegate { let result = await getPresentationResult(forEvent: "paywall_decline") if case .paywall = result, - paywallViewController.paywallInfo.presentedByEventWithName == SuperwallEventObjc.paywallDecline.description { + paywallViewController.paywallInfo.presentedByEventWithName != SuperwallEventObjc.paywallDecline.description { + + } else { dismiss( paywallViewController, result: .closed From 24306a64ef243ad8b51fd9267ec5dda24d485b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Wed, 3 May 2023 17:27:54 +0100 Subject: [PATCH 12/27] Update Superwall.swift --- Sources/SuperwallKit/Superwall.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SuperwallKit/Superwall.swift b/Sources/SuperwallKit/Superwall.swift index 56c58e8d3..cfb4703c2 100644 --- a/Sources/SuperwallKit/Superwall.swift +++ b/Sources/SuperwallKit/Superwall.swift @@ -465,7 +465,7 @@ extension Superwall: PaywallViewControllerDelegate { if case .paywall = result, paywallViewController.paywallInfo.presentedByEventWithName != SuperwallEventObjc.paywallDecline.description { - + // Do nothing, track will handle it. } else { dismiss( paywallViewController, From b108b58c806847a296f3374967f107354acd60cf Mon Sep 17 00:00:00 2001 From: jakemor Date: Wed, 3 May 2023 12:32:22 -0400 Subject: [PATCH 13/27] adds presentationError and removes error from PaywallSkippedReason --- .../HomeViewController.swift | 66 ++++---- .../Debug/DebugViewController.swift | 26 ++- .../PublicGetPaywallViewController.swift | 2 +- .../CheckDebuggerPresentationOperator.swift | 2 +- .../CheckNoPaywallAlreadyPresented.swift | 2 +- .../CheckPaywallPresentableOperator.swift | 2 +- .../Operators/GetPaywallVcOperator.swift | 2 +- .../HandleTriggerResultOperator.swift | 2 +- .../Operators/PresentPaywallOperator.swift | 2 +- .../PaywallSkippedReason.swift | 8 +- .../Presentation State/PaywallState.swift | 6 +- .../PaywallPresentationHandler.swift | 11 ++ .../Presentation/PublicPresentation.swift | 156 +++++++++--------- 13 files changed, 162 insertions(+), 125 deletions(-) diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift index 9b3287a46..4634c0682 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift @@ -71,9 +71,26 @@ final class HomeViewController: UIViewController { handler.onPresent { paywallInfo in print("The paywall presented. PaywallInfo:", paywallInfo) } + handler.onError { error in - print("The paywall presentation failed with error \(error)") + // internet doesn't work + // paywall view controller issue + // needs your attention } + + handler.onSkip { reason in + switch reason { + case .holdout: + break + case .noRuleMatch: + break + case .eventNotFound: + break + case .userIsSubscribed: + break + } + } + Superwall.shared.register(event: "campaign_trigger", handler: handler) { // code in here can be remotely configured to execute. Either // (1) always after presentation or @@ -85,40 +102,27 @@ final class HomeViewController: UIViewController { ) } - - - - - - -// -// Superwall.shared.getPaywall(forEvent: "campaign_trigger") { paywall, error in -// guard let paywall = paywall else { -// // TODO: make an async version and add error handling -// // skipped or subscribed +// Superwall.shared.getPaywallViewController(forEvent: "campaign_trigger") { result in +// switch result { +// case .success(let paywall): +// // prepare the vc +// paywall.presentationWillBegin() +// // present however you like +// self.present(paywall, animated: paywall.presentationIsAnimated) { +// // let the paywall know its presented +// print("[!] paywall presented") +// paywall.presentationDidFinish() +// } +// // get its result +// paywall.onDismiss { state in +// print("[!] paywall state:", state) +// } +// case .failure(let error): // print("[!] skip", error) -// return -// } -// -// print("[!] got paywall") -// -// // prepare the vc -// paywall.presentationWillBegin() -// -// // present however you like -// self.present(paywall, animated: paywall.presentationIsAnimated) { -// -// // let the paywall know its presented -// paywall.presentationDidFinish() -// } -// -// // get its result -// paywall.onDismiss { state in -// print("[!] paywall state:", state) // } // } -// + // Task { // // do { diff --git a/Sources/SuperwallKit/Debug/DebugViewController.swift b/Sources/SuperwallKit/Debug/DebugViewController.swift index 3231a1b98..385b51141 100644 --- a/Sources/SuperwallKit/Debug/DebugViewController.swift +++ b/Sources/SuperwallKit/Debug/DebugViewController.swift @@ -462,14 +462,6 @@ final class DebugViewController: UIViewController { errorMessage = "Couldn't find event" case .userIsSubscribed: errorMessage = "The user is subscribed." - case .error(let error): - errorMessage = error.localizedDescription - Logger.debug( - logLevel: .error, - scope: .debugViewController, - message: "Failed to Show Paywall", - info: nil - ) } self.presentAlert( title: "Paywall Skipped", @@ -483,7 +475,25 @@ final class DebugViewController: UIViewController { self.activityIndicator.stopAnimating() case .dismissed: break + case .presentationError(let error): + Logger.debug( + logLevel: .error, + scope: .debugViewController, + message: "Failed to Show Paywall", + info: nil + ) + self.presentAlert( + title: "Presentation Error", + message: error.localizedDescription, + on: self.view + ) + self.bottomButton.showLoading = false + + let playButton = UIImage(named: "play_button", in: Bundle.module, compatibleWith: nil)! + self.bottomButton.setImage(playButton, for: .normal) + self.activityIndicator.stopAnimating() } + } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift index 5af9dddd2..64f2275b4 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift @@ -31,7 +31,7 @@ extension Superwall { paywallOverrides: PaywallOverrides? = nil, completion: @escaping (Result) -> Void ) { - Task { + Task { @MainActor in do { let paywallViewController = try await getPaywallViewController( forEvent: event, diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift index 2f7299aab..609d04341 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift @@ -27,7 +27,7 @@ extension AnyPublisher where Output == (PresentationRequest, DebugInfo), Failure let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .debuggerLaunched) await Superwall.shared.track(trackedEvent) } - let state: PaywallState = .skipped(.error(error)) + let state: PaywallState = .presentationError(error) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) throw PresentationPipelineError.debuggerPresented diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift index ffd24d9dc..1ffa3196c 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift @@ -38,7 +38,7 @@ extension AnyPublisher where Output == PresentationRequest, Failure == Error { let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .alreadyPresented) await Superwall.shared.track(trackedEvent) } - let state: PaywallState = .skipped(.error(error)) + let state: PaywallState = .presentationError(error) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) throw PresentationPipelineError.paywallAlreadyPresented diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift index 881a6d13b..b33d93e71 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift @@ -77,7 +77,7 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noPresenter) await Superwall.shared.track(trackedEvent) } - let state: PaywallState = .skipped(.error(error)) + let state: PaywallState = .presentationError(error) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) throw PresentationPipelineError.noPresenter diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift index b9ee2fb71..3e3999473 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift @@ -106,7 +106,7 @@ extension AnyPublisher where Output == TriggerResultResponsePipelineOutput, Fail let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noPaywallViewController) await Superwall.shared.track(trackedEvent) } - paywallStatePublisher.send(.skipped(.error(error))) + paywallStatePublisher.send(.presentationError(error)) paywallStatePublisher.send(completion: .finished) return PresentationPipelineError.cancelled } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift index add28c0f9..df0d254d4 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperator.swift @@ -88,7 +88,7 @@ extension AnyPublisher where Output == AssignmentPipelineOutput, Failure == Erro await Superwall.shared.track(trackedEvent) } errorType = .noPaywallViewController - paywallStatePublisher.send(.skipped(.error(error))) + paywallStatePublisher.send(.presentationError(error)) } paywallStatePublisher.send(completion: .finished) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift index 019e091cf..4fe57a4e1 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift @@ -55,7 +55,7 @@ extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Err let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .alreadyPresented) await Superwall.shared.track(trackedEvent) } - paywallStatePublisher.send(.skipped(.error(error))) + paywallStatePublisher.send(.presentationError(error)) paywallStatePublisher.send(completion: .finished) promise(.failure(PresentationPipelineError.paywallAlreadyPresented)) } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift index fc4aae28d..1ff56f5c4 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift @@ -36,8 +36,8 @@ public enum PaywallSkippedReason: Sendable, Equatable { /// behavior in the paywall editor. case userIsSubscribed - /// An error occurred. - case error(Error) +// /// An error occurred. +// case error(Error) public static func == (lhs: PaywallSkippedReason, rhs: PaywallSkippedReason) -> Bool { switch (lhs, rhs) { @@ -47,8 +47,8 @@ public enum PaywallSkippedReason: Sendable, Equatable { return true case let (.holdout(experiment1), .holdout(experiment2)): return experiment1 == experiment2 - case let (.error(error1), .error(error2)): - return error1.localizedDescription == error2.localizedDescription +// case let (.error(error1), .error(error2)): +// return error1.localizedDescription == error2.localizedDescription default: return false } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift index d3f7cbee8..e7b09989d 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift @@ -35,9 +35,13 @@ public enum PaywallState { /// The paywall was presented. Contains a ``PaywallInfo`` object with more information about the presented paywall. case presented(PaywallInfo) + /// A paywall may have been configured to show, but did not due to an `Error`. + case presentationError(Error) + /// The paywall was dismissed. Contains a ``PaywallInfo`` object with more information about the presented paywall and a ``PaywallResult`` object containing the paywall dismissal reason. case dismissed(PaywallInfo, PaywallResult) - /// The paywall was skipped. Contains a ``PaywallSkippedReason`` enum whose cases state why the paywall was skipped. + /// The paywall was intentionally skipped. Contains a ``PaywallSkippedReason`` enum whose cases state why the paywall was skipped. case skipped(PaywallSkippedReason) + } diff --git a/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift b/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift index a9a023d3d..4c9818676 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift @@ -21,6 +21,9 @@ public class PaywallPresentationHandler: NSObject { /// A block called when an error occurred while trying to present a paywall. var onErrorHandler: ((_ error: Error) -> Void)? + /// A block called when an error occurred while trying to present a paywall. + var onSkipHandler: ((_ reason: PaywallSkippedReason) -> Void)? + /// Sets the handler that will be called when the paywall did presented. /// /// - Parameter handler: A block that accepts a ``PaywallInfo`` object associated with @@ -44,4 +47,12 @@ public class PaywallPresentationHandler: NSObject { public func onError(_ handler: @escaping (_ error: Error) -> Void) { self.onErrorHandler = handler } + + /// Sets the handler that will be called when a paywall is skipped, but no error has occurred. + /// + /// - Parameter handler: A block that accepts a `Error` indicating why the paywall + /// could not present. + public func onSkip(_ handler: @escaping (_ reason: PaywallSkippedReason) -> Void) { + self.onSkipHandler = handler + } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index 2481165af..61c767927 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -100,7 +100,7 @@ extension Superwall { /// - onSkip: A completion block that gets called when the paywall's presentation is skipped. Defaults to `nil`. Accepts a /// ``PaywallSkippedReasonObjc`` object and an `NSError` with more details. @available(swift, obsoleted: 1.0) - @objc public func track( + @objc private func track( event: String, params: [String: Any]? = nil, products: PaywallProducts? = nil, @@ -156,77 +156,86 @@ extension Superwall { } case .skipped(let reason): self?.onSkipConverter(reason: reason, completion: onSkip) + case .presentationError(let error): + self?.onSkipConverter(error: error, completion: onSkip) } } } private func onSkipConverter( - reason: PaywallSkippedReason, + reason: PaywallSkippedReason? = nil, + error: Error? = nil, completion: ((PaywallSkippedReasonObjc, NSError) -> Void)? ) { - switch reason { - case .holdout(let experiment): - let userInfo: [String: Any] = [ - "experimentId": experiment.id, - "variantId": experiment.variant.id, - "groupId": experiment.groupId, - NSLocalizedDescriptionKey: NSLocalizedString( - "Holdout", - value: "This user was assigned to a holdout. This means the paywall will not show.", - comment: "ExperimentId: \(experiment.id), VariantId: \(experiment.variant.id), GroupId: \(experiment.groupId)" + + if let reason = reason { + switch reason { + case .holdout(let experiment): + let userInfo: [String: Any] = [ + "experimentId": experiment.id, + "variantId": experiment.variant.id, + "groupId": experiment.groupId, + NSLocalizedDescriptionKey: NSLocalizedString( + "Holdout", + value: "This user was assigned to a holdout. This means the paywall will not show.", + comment: "ExperimentId: \(experiment.id), VariantId: \(experiment.variant.id), GroupId: \(experiment.groupId)" + ) + ] + let error = NSError( + domain: "com.superwall", + code: 4001, + userInfo: userInfo ) - ] - let error = NSError( - domain: "com.superwall", - code: 4001, - userInfo: userInfo - ) - completion?(.holdout, error) - case .noRuleMatch: - let userInfo: [String: Any] = [ - NSLocalizedDescriptionKey: NSLocalizedString( - "No rule match", - value: "The user did not match any rules configured for this trigger", - comment: "" + completion?(.holdout, error) + case .noRuleMatch: + let userInfo: [String: Any] = [ + NSLocalizedDescriptionKey: NSLocalizedString( + "No rule match", + value: "The user did not match any rules configured for this trigger", + comment: "" + ) + ] + let error = NSError( + domain: "com.superwall", + code: 4000, + userInfo: userInfo ) - ] - let error = NSError( - domain: "com.superwall", - code: 4000, - userInfo: userInfo - ) - completion?(.noRuleMatch, error) - case .eventNotFound: - let userInfo: [String: Any] = [ - NSLocalizedDescriptionKey: NSLocalizedString( - "Event Not Found", - value: "The specified event could not be found in a campaign", - comment: "" + completion?(.noRuleMatch, error) + case .eventNotFound: + let userInfo: [String: Any] = [ + NSLocalizedDescriptionKey: NSLocalizedString( + "Event Not Found", + value: "The specified event could not be found in a campaign", + comment: "" + ) + ] + let error = NSError( + domain: "com.superwall", + code: 404, + userInfo: userInfo ) - ] - let error = NSError( - domain: "com.superwall", - code: 404, - userInfo: userInfo - ) - completion?(.eventNotFound, error) - case .userIsSubscribed: - let userInfo: [String: Any] = [ - NSLocalizedDescriptionKey: NSLocalizedString( - "User Is Subscribed", - value: "The user subscription status is \"active\". By default, paywalls do not show to users who are already subscribed. You can override this behavior in the paywall editor.", - comment: "" + completion?(.eventNotFound, error) + case .userIsSubscribed: + let userInfo: [String: Any] = [ + NSLocalizedDescriptionKey: NSLocalizedString( + "User Is Subscribed", + value: "The user subscription status is \"active\". By default, paywalls do not show to users who are already subscribed. You can override this behavior in the paywall editor.", + comment: "" + ) + ] + let error = NSError( + domain: "com.superwall", + code: 4002, + userInfo: userInfo ) - ] - let error = NSError( - domain: "com.superwall", - code: 4002, - userInfo: userInfo - ) - completion?(.userIsSubscribed, error) - case .error(let error): + completion?(.userIsSubscribed, error) + } + } + + if let error = error { completion?(.error, error as NSError) } + } /// An Objective-C-only method that shows a paywall to the user when: An event you provide is tied to an @@ -247,7 +256,7 @@ extension Superwall { /// - Parameters: /// - event: The name of the event you wish to track. @available(swift, obsoleted: 1.0) - @objc public func track(event: String) { + @objc private func track(event: String) { objcTrack(event: event) } @@ -272,7 +281,7 @@ extension Superwall { /// Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. /// Arrays and dictionaries as values are not supported at this time, and will be dropped. @available(swift, obsoleted: 1.0) - @objc public func track( + @objc private func track( event: String, params: [String: Any]? = nil ) { @@ -304,7 +313,7 @@ extension Superwall { /// - onSkip: A completion block that gets called when the paywall's presentation is skipped. Defaults to `nil`. Accepts a /// ``PaywallSkippedReasonObjc`` object and an `NSError` with more details. @available(swift, obsoleted: 1.0) - @objc public func track( + @objc private func track( event: String, onSkip: ((PaywallSkippedReasonObjc, NSError) -> Void)? = nil, onPresent: ((PaywallInfo) -> Void)? = nil, @@ -344,7 +353,7 @@ extension Superwall { /// - onSkip: A completion block that gets called when the paywall's presentation is skipped. Defaults to `nil`. Accepts a /// ``PaywallSkippedReasonObjc`` object and an `NSError` with more details. @available(swift, obsoleted: 1.0) - @objc public func track( + @objc private func track( event: String, params: [String: Any]? = nil, onSkip: ((PaywallSkippedReasonObjc, NSError) -> Void)? = nil, @@ -375,7 +384,7 @@ extension Superwall { /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will be dropped. /// - paywallOverrides: An optional ``PaywallOverrides`` object whose parameters override the paywall defaults. Use this to override products, presentation style, and whether it ignores the subscription status. Defaults to `nil`. /// - paywallHandler: An optional callback that provides updates on the state of the paywall via a ``PaywallState`` object. - public func track( + private func track( event: String, params: [String: Any]? = nil, paywallOverrides: PaywallOverrides? = nil, @@ -477,17 +486,16 @@ extension Superwall { } } case .skipped(let reason): - switch reason { - case .error(let error): - DispatchQueue.main.async { - handler?.onErrorHandler?(error) // otherwise turning internet off would give unlimited access - } - default: - DispatchQueue.main.async { - completion?() - } + DispatchQueue.main.async { + handler?.onSkipHandler?(reason) + completion?() + } + case .presentationError(let error): + DispatchQueue.main.async { + handler?.onErrorHandler?(error) // otherwise turning internet off would give unlimited access } } + } )) } @@ -515,7 +523,7 @@ extension Superwall { do { try TrackingLogic.checkNotSuperwallEvent(event) } catch { - return Just(.skipped(.error(error))).eraseToAnyPublisher() + return Just(.presentationError(error)).eraseToAnyPublisher() } return Future { From 3eecd07fe9952201decbe6775a8139d6ea4cfcfa Mon Sep 17 00:00:00 2001 From: jakemor Date: Wed, 3 May 2023 12:38:56 -0400 Subject: [PATCH 14/27] merge conflicts --- .../Operators/CheckDebuggerPresentationOperator.swift | 4 ---- .../Operators/CheckNoPaywallAlreadyPresented.swift | 5 ----- .../Operators/CheckPaywallPresentableOperator.swift | 4 ---- .../Operators/GetPaywallVcOperator.swift | 4 ---- .../Operators/PresentPaywallOperator.swift | 4 ---- 5 files changed, 21 deletions(-) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift index 609d04341..6ae267872 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperator.swift @@ -23,10 +23,6 @@ extension AnyPublisher where Output == (PresentationRequest, DebugInfo), Failure title: "Debugger Is Presented", value: "Trying to present paywall when debugger is launched." ) - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .debuggerLaunched) - await Superwall.shared.track(trackedEvent) - } let state: PaywallState = .presentationError(error) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift index 1ffa3196c..db36dc15c 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckNoPaywallAlreadyPresented.swift @@ -33,11 +33,6 @@ extension AnyPublisher where Output == PresentationRequest, Failure == Error { title: "Paywall Already Presented", value: "You can only present one paywall at a time." ) - Task.detached(priority: .utility) { - // TODO: MOVE TO COMPLETION BLOCK? SHOULD THAT BE ON EVERY PIPELINE OR JUST PRESENT - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .alreadyPresented) - await Superwall.shared.track(trackedEvent) - } let state: PaywallState = .presentationError(error) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift index 00e942891..3c43ba399 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift @@ -67,10 +67,6 @@ extension AnyPublisher where Output == PaywallVcPipelineOutput, Failure == Error title: "No UIViewController to present paywall on", value: "This usually happens when you call this method before a window was made key and visible." ) - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noPresenter) - await Superwall.shared.track(trackedEvent) - } let state: PaywallState = .presentationError(error) paywallStatePublisher.send(state) paywallStatePublisher.send(completion: .finished) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift index e3d30c965..c1ccf2af5 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift @@ -98,10 +98,6 @@ extension AnyPublisher where Output == TriggerResultResponsePipelineOutput, Fail info: input.debugInfo, error: error ) - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .noPaywallViewController) - await Superwall.shared.track(trackedEvent) - } paywallStatePublisher.send(.presentationError(error)) paywallStatePublisher.send(completion: .finished) return PresentationPipelineError.noPaywallViewController diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift index 2b59ce981..c2952d2d2 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperator.swift @@ -55,10 +55,6 @@ extension AnyPublisher where Output == PresentablePipelineOutput, Failure == Err title: "Paywall Already Presented", value: "Trying to present paywall while another paywall is presented." ) - Task.detached(priority: .utility) { - let trackedEvent = InternalSuperwallEvent.UnableToPresent(state: .alreadyPresented) - await Superwall.shared.track(trackedEvent) - } paywallStatePublisher.send(.presentationError(error)) paywallStatePublisher.send(completion: .finished) promise(.failure(PresentationPipelineError.paywallAlreadyPresented)) From d15b1f55328a272cad71be6514a607770c078818 Mon Sep 17 00:00:00 2001 From: jakemor Date: Wed, 3 May 2023 17:29:24 -0400 Subject: [PATCH 15/27] fixes example projects --- .../SwiftUI/Superwall-SwiftUI/HomeView.swift | 23 ++++++- .../HomeViewController.swift | 65 ++----------------- .../HomeViewController.swift | 18 ++++- 3 files changed, 42 insertions(+), 64 deletions(-) diff --git a/Examples/SwiftUI/Superwall-SwiftUI/HomeView.swift b/Examples/SwiftUI/Superwall-SwiftUI/HomeView.swift index 376dc5b57..ae52d1285 100644 --- a/Examples/SwiftUI/Superwall-SwiftUI/HomeView.swift +++ b/Examples/SwiftUI/Superwall-SwiftUI/HomeView.swift @@ -41,15 +41,32 @@ struct HomeView: View { VStack(spacing: 25) { BrandedButton(title: "Launch Feature") { let handler = PaywallPresentationHandler() - handler.onDismiss = { paywallInfo in + + handler.onDismiss { paywallInfo in print("The paywall dismissed. PaywallInfo:", paywallInfo) } - handler.onPresent = { paywallInfo in + + handler.onPresent { paywallInfo in print("The paywall presented. PaywallInfo:", paywallInfo) } - handler.onError = { error in + + 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 diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift index ed5284dfe..b2754b965 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift @@ -73,21 +73,19 @@ final class HomeViewController: UIViewController { } handler.onError { error in - // internet doesn't work - // paywall view controller issue - // needs your attention + print("The paywall presentation failed with error \(error)") } handler.onSkip { reason in switch reason { - case .holdout: - break + 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: - break + print("Paywall not shown because user doesn't match any rules.") case .eventNotFound: - break - case .userIsSubscribed: - break + print("Paywall not shown because this event isn't part of a campaign.") } } @@ -101,55 +99,6 @@ final class HomeViewController: UIViewController { message: "Wrap your awesome features in register calls like this to remotely paywall your app. You can choose if these are paid features remotely." ) } - -// Superwall.shared.getPaywallViewController(forEvent: "campaign_trigger") { result in -// switch result { -// case .success(let paywall): -// // prepare the vc -// paywall.presentationWillBegin() -// // present however you like -// self.present(paywall, animated: paywall.presentationIsAnimated) { -// // let the paywall know its presented -// print("[!] paywall presented") -// paywall.presentationDidFinish() -// } -// // get its result -// paywall.onDismiss { state in -// print("[!] paywall state:", state) -// } -// case .failure(let error): -// print("[!] skip", error) -// } -// } - - -// Task { -// -// do { -// let paywall = try await Superwall.shared.getPaywallViewController(forEvent: "campaign_trigger") -// print("[!] got paywall") -// -// // prepare the vc -// paywall.presentationWillBegin() -// -// // present however you like -// self.present(paywall, animated: paywall.presentationIsAnimated) { -// // let the paywall know its presented -// print("[!] paywall presented") -// paywall.presentationDidFinish() -// } -// -// // get its result -// paywall.onDismiss { state in -// print("[!] paywall state:", state) -// } -// -// } catch let error { -// print("[!] skipped because: ", error) -// } -// -// } - } private func presentAlert(title: String, message: String) { diff --git a/Examples/UIKit-Swift/Superwall-UIKit-Swift/HomeViewController.swift b/Examples/UIKit-Swift/Superwall-UIKit-Swift/HomeViewController.swift index 460ad7b41..10ff4e188 100644 --- a/Examples/UIKit-Swift/Superwall-UIKit-Swift/HomeViewController.swift +++ b/Examples/UIKit-Swift/Superwall-UIKit-Swift/HomeViewController.swift @@ -60,15 +60,27 @@ final class HomeViewController: UIViewController { @IBAction private func launchFeature() { let handler = PaywallPresentationHandler() - handler.onDismiss = { paywallInfo in + handler.onDismiss { paywallInfo in print("The paywall dismissed. PaywallInfo:", paywallInfo) } - handler.onPresent = { paywallInfo in + handler.onPresent { paywallInfo in print("The paywall presented. PaywallInfo:", paywallInfo) } - handler.onError = { error in + 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 From 547e68473b8a102ce2b0d44b8bf29ecbcfa899a7 Mon Sep 17 00:00:00 2001 From: jakemor Date: Wed, 3 May 2023 20:04:30 -0400 Subject: [PATCH 16/27] Update CHANGELOG.md --- CHANGELOG.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3c3c6a7..004aacd3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,24 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup ### Breaking Changes -- Changes `DismissState` to `PaywallResult` -- Removes the `closedForNextPaywall` `PaywallResult` and moves it to a new `PaywallInfo` property `closeReason`, which can either be `nil`, `.systemLogic`, or `.forNextPaywall`. +- Changes `DismissState` to `PaywallResult`. +- Removes the `closedForNextPaywall` case from `PaywallResult` in favor of a new `PaywallInfo` property called `closeReason`, which can either be `nil`, `.systemLogic`, or `.forNextPaywall`. - Changes the `PaywallPresentationHandler` variables to functions. - Removes `Superwall.shared.track`. We're going all in on `Superwall.shared.register` baby! +- Removes .error(Error) from `PaywallSkippedReason` in favor of a new `PaywallState` case `.presentationError(Error)`. +- Removes `PaywallPresentationHandler` completion block variables removed in favor of function calls with the same names. +- Changes `.onError` of `PaywallPresentationHandler` to no longer be called when a paywall is intentionally not shown (i.e. user is subscribed, user is in holdout, no rule match, event not configured) +- Adds `.onSkip(reason:)` to `PaywallPresentationHandler` to handle cases where paywall isn't shown because user is subscribed, user is in holdout, no rules match, event not configured ### Enhancements -- Sets default logging level to `INFO`. +- Adds `getPaywallViewController`! You can no request an actual view controller to present however you like. Check function documentation in Xcode for instructions – follow directions closely. +- Changes default logging level to `INFO`. +- Adds new automatically tracked `paywall_decline` event that can be used to present a new paywall when a user dismisses a paywall. +- Allows `transaction_abandon` to trigger new paywalls when added to a campaign – called when a user abandons checkout (did you know 75% of the time, users abandon checkout when Apple's payment sheet comes up?!). +- Adds `.onSkip` to `PaywallPresentationHandler` which is passed a `PaywallSkippedReason` when a paywall is not supposed to show. +- Adds logging at `INFO` level, mansplaining exactly why a paywall is not shown when calling `register` or `getPaywallViewController`. +- Adds new automatically tracked event `presentation_request` that gets sent with properties explaining why a paywall was or was not shown. ### Fixes From fec7e3a15ec271979621055caddb89dd5ff65fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 4 May 2023 11:03:57 +0100 Subject: [PATCH 17/27] Added getImplicitPresentationResult to prevent unnecessary logging --- .../InternalGetPresentationResult.swift | 16 +- .../PublicGetPresentationResult.swift | 75 ++-- .../Operators/WaitToPresentOperator.swift | 9 +- .../PresentationRequest.swift | 1 + .../Presentation/PublicPresentation.swift | 359 +----------------- Sources/SuperwallKit/Superwall.swift | 2 +- 6 files changed, 67 insertions(+), 395 deletions(-) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/InternalGetPresentationResult.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/InternalGetPresentationResult.swift index ac3405bde..960b57800 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/InternalGetPresentationResult.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/InternalGetPresentationResult.swift @@ -38,7 +38,7 @@ extension Superwall { .getPaywallViewController(pipelineType: .getPresentationResult) .checkPaywallIsPresentable() .convertToPresentationResult() - .async() + .async(request: request) } } @@ -48,7 +48,7 @@ extension Publisher where Output == PresentationResult { /// /// This handles the error cases thrown by `getPresentationResult(for:)`. @discardableResult - func async() async -> Output { + func async(request: PresentationRequest) async -> Output { await withCheckedContinuation { continuation in var cancellable: AnyCancellable? cancellable = first() @@ -57,11 +57,13 @@ extension Publisher where Output == PresentationResult { case .failure(let error): switch error { case let error as GetPresentationResultError: - Logger.debug( - logLevel: .info, - scope: .paywallPresentation, - message: "Skipped paywall presentation: \(error)" - ) + if request.flags.type != .getImplicitPresentationResult { + Logger.debug( + logLevel: .info, + scope: .paywallPresentation, + message: "Skipped paywall presentation: \(error)" + ) + } switch error { case .willNotPresent(let result): let trackResult = GetPresentationResultLogic.convertTriggerResult(result) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift index 28e647486..1fd35622e 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift @@ -30,35 +30,11 @@ extension Superwall { forEvent event: String, params: [String: Any]? = nil ) async -> PresentationResult { - let eventCreatedAt = Date() - - let trackableEvent = UserInitiatedEvent.Track( - rawName: event, - canImplicitlyTriggerPaywall: false, - customParameters: params ?? [:], - isFeatureGatable: false - ) - - let parameters = await TrackingLogic.processParameters( - fromTrackableEvent: trackableEvent, - eventCreatedAt: eventCreatedAt, - appSessionId: dependencyContainer.appSessionManager.appSession.id - ) - - let eventData = EventData( - name: event, - parameters: JSON(parameters.eventParams), - createdAt: eventCreatedAt - ) - - let presentationRequest = dependencyContainer.makePresentationRequest( - .explicitTrigger(eventData), - isDebuggerLaunched: false, - isPaywallPresented: false, + return await internallyGetPresentationResult( + forEvent: event, + params: params, type: .getPresentationResult ) - - return await getPresentationResult(for: presentationRequest) } /// Preemptively get the result of tracking an event. @@ -88,6 +64,51 @@ extension Superwall { } } + /// Call when you need to get the presentation result from an implicit event. This prevents logs being + /// fired. + func getImplicitPresentationResult(forEvent event: String) async -> PresentationResult { + return await internallyGetPresentationResult( + forEvent: event, + type: .getImplicitPresentationResult + ) + } + + private func internallyGetPresentationResult( + forEvent event: String, + params: [String: Any]? = nil, + type: PresentationRequestType + ) async -> PresentationResult { + let eventCreatedAt = Date() + + let trackableEvent = UserInitiatedEvent.Track( + rawName: event, + canImplicitlyTriggerPaywall: false, + customParameters: params ?? [:], + isFeatureGatable: false + ) + + let parameters = await TrackingLogic.processParameters( + fromTrackableEvent: trackableEvent, + eventCreatedAt: eventCreatedAt, + appSessionId: dependencyContainer.appSessionManager.appSession.id + ) + + let eventData = EventData( + name: event, + parameters: JSON(parameters.eventParams), + createdAt: eventCreatedAt + ) + + let presentationRequest = dependencyContainer.makePresentationRequest( + .explicitTrigger(eventData), + isDebuggerLaunched: false, + isPaywallPresented: false, + type: type + ) + + return await getPresentationResult(for: presentationRequest) + } + /// Objective-C-only function to preemptively get the result of tracking an event. /// /// This is useful for when you want to know whether a particular event will diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperator.swift index a1460eb9f..90591189a 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperator.swift @@ -30,14 +30,19 @@ extension AnyPublisher where Output == PresentationRequest, Failure == Error { } .first() .map { request, timer, _, _, _ in - timer.invalidate() + timer?.invalidate() return request } .eraseToAnyPublisher() } - private func startTimer() -> (AnyPublisher<(PresentationRequest, Timer), Failure>) { + /// Starts a 5 sec timer. If pipeline above progresses, it'll get cancelled. Otherwise will log a + /// timeout for the user. + private func startTimer() -> (AnyPublisher<(PresentationRequest, Timer?), Failure>) { map { request in + guard request.flags.type != .getImplicitPresentationResult else { + return (request, nil) + } let timer = Timer( timeInterval: 5, repeats: false diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationRequest.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationRequest.swift index d19564691..77fcd5c8a 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationRequest.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PresentationRequest.swift @@ -12,6 +12,7 @@ enum PresentationRequestType: String { case presentation case getPaywallViewController case getPresentationResult + case getImplicitPresentationResult } /// Defines the information needed to request the presentation of a paywall. diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index c132184ca..cd9799d75 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -66,343 +66,7 @@ extension Superwall { } } - // MARK: - Objective-C-only Track - /// An Objective-C-only method that shows a paywall to the user when: An event you provide is tied to an - /// active trigger inside a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); - /// and the user matches a rule in the campaign. **Note**: Please use ``Superwall/setUserAttributes(_:)-1wql2`` - /// if you’re using Swift. - /// - /// Triggers enable you to retroactively decide where or when to show a specific paywall in your app. Use this method - /// when you want to remotely control paywall presentation in response to your own analytics event and utilize - /// completion handlers associated with the paywall presentation state. - /// - /// Before using this method, you'll first need to create a campaign and add a trigger associated with the event name - /// on the [Superwall Dashboard](https://superwall.com/dashboard). - /// - /// The paywall shown to the user is determined by the rules defined in the campaign. Paywalls are sticky, in that when - /// a user is assigned a paywall within a rule, they will continue to see that paywall unless you remove the paywall from the rule. - /// - /// - Parameters: - /// - event: The name of the event you wish to track. - /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. - /// Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. - /// Arrays and dictionaries as values are not supported at this time, and will be dropped. - /// - products: An optional ``PaywallProducts`` object whose products replace the remotely defined paywall products. Defaults - /// to `nil`. - /// - ignoreSubscriptionStatus: Presents the paywall regardless of subscription status if `true`. Defaults to `false`. - /// - presentationStyleOverride: A `PaywallPresentationStyle` object that overrides the presentation style of the paywall - /// set on the dashboard. Defaults to `.none`. - /// - onPresent: A completion block that gets called immediately after the paywall is presented. Defaults to `nil`. Accepts a - /// ``PaywallInfo`` object containing information about the paywall. - /// - onDismiss: A completion block that gets called when the paywall is dismissed by the user, by way of purchasing, restoring or manually - /// dismissing. Defaults to `nil`. Accepts a `Bool` that is `true` if the user purchased a product and `false` if not, a `String?` equal - /// to the product id of the purchased product (if any) and a ``PaywallInfo`` object containing information about the paywall. - /// - onSkip: A completion block that gets called when the paywall's presentation is skipped. Defaults to `nil`. Accepts a - /// ``PaywallSkippedReasonObjc`` object and an `NSError` with more details. - @available(swift, obsoleted: 1.0) - @objc private func track( - event: String, - params: [String: Any]? = nil, - products: PaywallProducts? = nil, - ignoreSubscriptionStatus: Bool = false, - presentationStyleOverride: PaywallPresentationStyle = .none, - onSkip: ((PaywallSkippedReasonObjc, NSError) -> Void)? = nil, - onPresent: ((PaywallInfo) -> Void)? = nil, - onDismiss: ((PaywallResultObjc, String?, PaywallInfo) -> Void)? = nil - ) { - objcTrack( - event: event, - params: params, - products: products, - ignoreSubscriptionStatus: ignoreSubscriptionStatus, - presentationStyleOverride: presentationStyleOverride, - onSkip: onSkip, - onPresent: onPresent, - onDismiss: onDismiss - ) - } - - private func objcTrack( - event: String, - params: [String: Any]? = nil, - products: PaywallProducts? = nil, - ignoreSubscriptionStatus: Bool = false, - presentationStyleOverride: PaywallPresentationStyle = .none, - onSkip: ((PaywallSkippedReasonObjc, NSError) -> Void)? = nil, - onPresent: ((PaywallInfo) -> Void)? = nil, - onDismiss: ((PaywallResultObjc, String?, PaywallInfo) -> Void)? = nil - ) { - let overrides = PaywallOverrides( - products: products, - ignoreSubscriptionStatus: ignoreSubscriptionStatus, - presentationStyleOverride: presentationStyleOverride - ) - - track( - event: event, - params: params, - paywallOverrides: overrides - ) { [weak self] state in - switch state { - case .presented(let paywallInfo): - onPresent?(paywallInfo) - case let .dismissed(paywallInfo, state): - if let onDismiss = onDismiss { - self?.onDismissConverter( - paywallInfo: paywallInfo, - state: state, - completion: onDismiss - ) - } - case .skipped(let reason): - self?.onSkipConverter(reason: reason, completion: onSkip) - case .presentationError(let error): - self?.onSkipConverter(error: error, completion: onSkip) - } - } - } - - private func onSkipConverter( - reason: PaywallSkippedReason? = nil, - error: Error? = nil, - completion: ((PaywallSkippedReasonObjc, NSError) -> Void)? - ) { - - if let reason = reason { - switch reason { - case .holdout(let experiment): - let userInfo: [String: Any] = [ - "experimentId": experiment.id, - "variantId": experiment.variant.id, - "groupId": experiment.groupId, - NSLocalizedDescriptionKey: NSLocalizedString( - "Holdout", - value: "This user was assigned to a holdout. This means the paywall will not show.", - comment: "ExperimentId: \(experiment.id), VariantId: \(experiment.variant.id), GroupId: \(experiment.groupId)" - ) - ] - let error = NSError( - domain: "com.superwall", - code: 4001, - userInfo: userInfo - ) - completion?(.holdout, error) - case .noRuleMatch: - let userInfo: [String: Any] = [ - NSLocalizedDescriptionKey: NSLocalizedString( - "No rule match", - value: "The user did not match any rules configured for this trigger", - comment: "" - ) - ] - let error = NSError( - domain: "com.superwall", - code: 4000, - userInfo: userInfo - ) - completion?(.noRuleMatch, error) - case .eventNotFound: - let userInfo: [String: Any] = [ - NSLocalizedDescriptionKey: NSLocalizedString( - "Event Not Found", - value: "The specified event could not be found in a campaign", - comment: "" - ) - ] - let error = NSError( - domain: "com.superwall", - code: 404, - userInfo: userInfo - ) - completion?(.eventNotFound, error) - case .userIsSubscribed: - let userInfo: [String: Any] = [ - NSLocalizedDescriptionKey: NSLocalizedString( - "User Is Subscribed", - value: "The user subscription status is \"active\". By default, paywalls do not show to users who are already subscribed. You can override this behavior in the paywall editor.", - comment: "" - ) - ] - let error = NSError( - domain: "com.superwall", - code: 4002, - userInfo: userInfo - ) - completion?(.userIsSubscribed, error) - } - } - - if let error = error { - completion?(.error, error as NSError) - } - - } - - /// An Objective-C-only method that shows a paywall to the user when: An event you provide is tied to an - /// active trigger inside a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); - /// and the user matches a rule in the campaign. **Note**: Please use ``Superwall/setUserAttributes(_:)-1wql2`` - /// if you’re using Swift. - /// - /// Triggers enable you to retroactively decide where or when to show a specific paywall in your app. Use this method - /// when you want to remotely control paywall presentation in response to your own analytics event and utilize - /// completion handlers associated with the paywall presentation state. - /// - /// Before using this method, you'll first need to create a campaign and add a trigger associated with the event name - /// on the [Superwall Dashboard](https://superwall.com/dashboard). - /// - /// The paywall shown to the user is determined by the rules defined in the campaign. Paywalls are sticky, in that when - /// a user is assigned a paywall within a rule, they will continue to see that paywall unless you remove the paywall from the rule. - /// - /// - Parameters: - /// - event: The name of the event you wish to track. - @available(swift, obsoleted: 1.0) - @objc private func track(event: String) { - objcTrack(event: event) - } - - /// An Objective-C-only method that shows a paywall to the user when: An event you provide is tied to an - /// active trigger inside a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); - /// and the user matches a rule in the campaign. **Note**: Please use ``Superwall/setUserAttributes(_:)-1wql2`` - /// if you’re using Swift. - /// - /// Triggers enable you to retroactively decide where or when to show a specific paywall in your app. Use this method - /// when you want to remotely control paywall presentation in response to your own analytics event and utilize - /// completion handlers associated with the paywall presentation state. - /// - /// Before using this method, you'll first need to create a campaign and add a trigger associated with the event name - /// on the [Superwall Dashboard](https://superwall.com/dashboard). - /// - /// The paywall shown to the user is determined by the rules defined in the campaign. Paywalls are sticky, in that when - /// a user is assigned a paywall within a rule, they will continue to see that paywall unless you remove the paywall from the rule. - /// - /// - Parameters: - /// - event: The name of the event you wish to track. - /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. - /// Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. - /// Arrays and dictionaries as values are not supported at this time, and will be dropped. - @available(swift, obsoleted: 1.0) - @objc private func track( - event: String, - params: [String: Any]? = nil - ) { - objcTrack(event: event, params: params) - } - - /// An Objective-C-only method that shows a paywall to the user when: An event you provide is tied to an - /// active trigger inside a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); - /// and the user matches a rule in the campaign. **Note**: Please use ``Superwall/setUserAttributes(_:)-1wql2`` - /// if you’re using Swift. - /// - /// Triggers enable you to retroactively decide where or when to show a specific paywall in your app. Use this method - /// when you want to remotely control paywall presentation in response to your own analytics event and utilize - /// completion handlers associated with the paywall presentation state. - /// - /// Before using this method, you'll first need to create a campaign and add a trigger associated with the event name - /// on the [Superwall Dashboard](https://superwall.com/dashboard). - /// - /// The paywall shown to the user is determined by the rules defined in the campaign. Paywalls are sticky, in that when - /// a user is assigned a paywall within a rule, they will continue to see that paywall unless you remove the paywall from the rule. - /// - /// - Parameters: - /// - event: The name of the event you wish to track. - /// - onPresent: A completion block that gets called immediately after the paywall is presented. Defaults to `nil`. Accepts a - /// ``PaywallInfo`` object containing information about the paywall. - /// - onDismiss: A completion block that gets called when the paywall is dismissed by the user, by way of purchasing, restoring or manually - /// dismissing. Defaults to `nil`. Accepts a `Bool` that is `true` if the user purchased a product and `false` if not, a `String?` equal - /// to the product id of the purchased product (if any) and a ``PaywallInfo`` object containing information about the paywall. - /// - onSkip: A completion block that gets called when the paywall's presentation is skipped. Defaults to `nil`. Accepts a - /// ``PaywallSkippedReasonObjc`` object and an `NSError` with more details. - @available(swift, obsoleted: 1.0) - @objc private func track( - event: String, - onSkip: ((PaywallSkippedReasonObjc, NSError) -> Void)? = nil, - onPresent: ((PaywallInfo) -> Void)? = nil, - onDismiss: ((PaywallResultObjc, String?, PaywallInfo) -> Void)? = nil - ) { - objcTrack( - event: event, - onSkip: onSkip, - onPresent: onPresent, - onDismiss: onDismiss - ) - } - - /// An Objective-C-only method that shows a paywall to the user when: An event you provide is tied to an - /// active trigger inside a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); - /// and the user matches a rule in the campaign. **Note**: Please use ``Superwall/setUserAttributes(_:)-1wql2`` - /// if you’re using Swift. - /// - /// Triggers enable you to retroactively decide where or when to show a specific paywall in your app. Use this method - /// when you want to remotely control paywall presentation in response to your own analytics event and utilize - /// completion handlers associated with the paywall presentation state. - /// - /// Before using this method, you'll first need to create a campaign and add a trigger associated with the event name - /// on the [Superwall Dashboard](https://superwall.com/dashboard). - /// - /// The paywall shown to the user is determined by the rules defined in the campaign. Paywalls are sticky, in that when - /// a user is assigned a paywall within a rule, they will continue to see that paywall unless you remove the paywall from the rule. - /// - /// - Parameters: - /// - event: The name of the event you wish to track. - /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. - /// - onPresent: A completion block that gets called immediately after the paywall is presented. Defaults to `nil`. Accepts a - /// ``PaywallInfo`` object containing information about the paywall. - /// - onDismiss: A completion block that gets called when the paywall is dismissed by the user, by way of purchasing, restoring or manually - /// dismissing. Defaults to `nil`. Accepts a `Bool` that is `true` if the user purchased a product and `false` if not, a `String?` equal - /// to the product id of the purchased product (if any) and a ``PaywallInfo`` object containing information about the paywall. - /// - onSkip: A completion block that gets called when the paywall's presentation is skipped. Defaults to `nil`. Accepts a - /// ``PaywallSkippedReasonObjc`` object and an `NSError` with more details. - @available(swift, obsoleted: 1.0) - @objc private func track( - event: String, - params: [String: Any]? = nil, - onSkip: ((PaywallSkippedReasonObjc, NSError) -> Void)? = nil, - onPresent: ((PaywallInfo) -> Void)? = nil, - onDismiss: ((PaywallResultObjc, String?, PaywallInfo) -> Void)? = nil - ) { - objcTrack( - event: event, - params: params, - onSkip: onSkip, - onPresent: onPresent, - onDismiss: onDismiss - ) - } - - // MARK: - Swift Track - - /// Tracks an event which, when added to a campaign on the Superwall dashboard, can show a paywall. - /// - /// This shows a paywall to the user when: An event you provide is added to a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); the user matches a rule in the campaign; and the user doesn't have an active subscription. - /// - /// Before using this method, you'll first need to create a campaign and add the event to the campaign on the [Superwall Dashboard](https://superwall.com/dashboard). - /// - /// The paywall shown to the user is determined by the rules defined in the campaign. When a user is assigned a paywall within a rule, they will continue to see that paywall unless you remove the paywall from the rule or reset assignments to the paywall. - /// - /// - Parameters: - /// - event: The name of the event you wish to track. - /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will be dropped. - /// - paywallOverrides: An optional ``PaywallOverrides`` object whose parameters override the paywall defaults. Use this to override products, presentation style, and whether it ignores the subscription status. Defaults to `nil`. - /// - paywallHandler: An optional callback that provides updates on the state of the paywall via a ``PaywallState`` object. - private func track( - event: String, - params: [String: Any]? = nil, - paywallOverrides: PaywallOverrides? = nil, - paywallHandler: ((PaywallState) -> Void)? = nil - ) { - publisher( - forEvent: event, - params: params, - paywallOverrides: paywallOverrides, - isFeatureGatable: false - ) - .subscribe(Subscribers.Sink( - receiveCompletion: { _ in }, - receiveValue: { state in - paywallHandler?(state) - } - )) - } + // MARK: - Register /// Registers an event to access a feature. When the event is added to a campaign on the Superwall dashboard, it can show a paywall. /// @@ -547,25 +211,4 @@ extension Superwall { } .eraseToAnyPublisher() } - - - /// Converts dismissal result from enums with associated values, to old objective-c compatible way - /// - /// - Parameters: - /// - result: The dismissal result - /// - completion: A completion block that gets called when the paywall is dismissed by the user, by way of purchasing, restoring or manually dismissing. Accepts a `Bool` that is `true` if the user purchased a product and `false` if not, a `String?` equal to the product id of the purchased product (if any) and a ``PaywallInfo`` object containing information about the paywall. - private func onDismissConverter( - paywallInfo: PaywallInfo, - state: PaywallResult, - completion: (PaywallResultObjc, String?, PaywallInfo) -> Void - ) { - switch state { - case .closed: - completion(.closed, nil, paywallInfo) - case .purchased(productId: let productId): - completion(.purchased, productId, paywallInfo) - case .restored: - completion(.restored, nil, paywallInfo) - } - } } diff --git a/Sources/SuperwallKit/Superwall.swift b/Sources/SuperwallKit/Superwall.swift index cfb4703c2..49ebc923c 100644 --- a/Sources/SuperwallKit/Superwall.swift +++ b/Sources/SuperwallKit/Superwall.swift @@ -461,7 +461,7 @@ extension Superwall: PaywallViewControllerDelegate { case .closed: let trackedEvent = InternalSuperwallEvent.PaywallDecline(paywallInfo: paywallViewController.paywallInfo) - let result = await getPresentationResult(forEvent: "paywall_decline") + let result = await getImplicitPresentationResult(forEvent: "paywall_decline") if case .paywall = result, paywallViewController.paywallInfo.presentedByEventWithName != SuperwallEventObjc.paywallDecline.description { From 85b1e74e4b14527de0f03cddcf4f281c70f98905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 4 May 2023 11:39:24 +0100 Subject: [PATCH 18/27] Documentation and swiftlint updates --- .../SwiftUI/Superwall-SwiftUI/HomeView.swift | 4 -- .../HomeViewController.swift | 2 - .../SSAHomeViewController.m | 15 +++--- .../Debug/DebugViewController.swift | 1 - .../Extensions/SuperwallExtension.md | 3 +- .../Models/Paywall/PaywallProducts.swift | 2 +- .../PublicGetPaywallViewController.swift | 6 +-- .../PublicGetPresentationResult.swift | 54 ++++++++----------- .../PaywallOverrides.swift | 4 +- .../PaywallSkippedReason.swift | 5 -- .../Presentation State/PaywallState.swift | 1 - .../Paywall/Presentation/PaywallInfo.swift | 4 +- .../Presentation/PublicPresentation.swift | 6 +-- .../PaywallViewController.swift | 2 +- 14 files changed, 44 insertions(+), 65 deletions(-) diff --git a/Examples/SwiftUI/Superwall-SwiftUI/HomeView.swift b/Examples/SwiftUI/Superwall-SwiftUI/HomeView.swift index ae52d1285..d813ef278 100644 --- a/Examples/SwiftUI/Superwall-SwiftUI/HomeView.swift +++ b/Examples/SwiftUI/Superwall-SwiftUI/HomeView.swift @@ -41,19 +41,15 @@ struct HomeView: View { VStack(spacing: 25) { BrandedButton(title: "Launch Feature") { 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: diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift index b2754b965..286037a6c 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/HomeViewController.swift @@ -71,11 +71,9 @@ final class HomeViewController: UIViewController { 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: diff --git a/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSAHomeViewController.m b/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSAHomeViewController.m index 2052234aa..153f59142 100644 --- a/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSAHomeViewController.m +++ b/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSAHomeViewController.m @@ -46,15 +46,18 @@ - (void)viewWillAppear:(BOOL)animated { - (IBAction)registerEvent:(id)sender { SWKPaywallPresentationHandler *handler = [[SWKPaywallPresentationHandler alloc] init]; - handler.onDismiss = ^(SWKPaywallInfo *paywallInfo) { + + [handler onDismiss:^(SWKPaywallInfo * _Nonnull paywallInfo) { NSLog(@"The paywall dismissed. PaywallInfo: %@", paywallInfo); - }; - handler.onPresent = ^(SWKPaywallInfo *paywallInfo) { + }]; + + [handler onPresent:^(SWKPaywallInfo * _Nonnull paywallInfo) { NSLog(@"The paywall presented. PaywallInfo: %@", paywallInfo); - }; - handler.onError = ^(NSError * _Nonnull error) { + }]; + + [handler onError:^(NSError * _Nonnull error) { NSLog(@"The paywall presentation failed with error %@", error); - }; + }]; [[Superwall sharedInstance] registerWithEvent:@"campaign_trigger" params:nil handler:handler feature:^{ UIAlertController* alert = [UIAlertController diff --git a/Sources/SuperwallKit/Debug/DebugViewController.swift b/Sources/SuperwallKit/Debug/DebugViewController.swift index 57305ca1c..92f996900 100644 --- a/Sources/SuperwallKit/Debug/DebugViewController.swift +++ b/Sources/SuperwallKit/Debug/DebugViewController.swift @@ -494,7 +494,6 @@ final class DebugViewController: UIViewController { self.bottomButton.setImage(playButton, for: .normal) self.activityIndicator.stopAnimating() } - } } diff --git a/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md b/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md index 14f885098..5ef49a43c 100644 --- a/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md +++ b/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md @@ -28,10 +28,11 @@ The ``Superwall`` class is used to access all the features of the SDK. Before us ### Presenting and Dismissing a Paywall -- ``register(event:params:handler:)`` - ``register(event:params:handler:feature:)`` +- ``register(event:params:handler:)`` - ``getPaywallViewController(forEvent:params:paywallOverrides:)`` - ``getPaywallViewController(forEvent:params:paywallOverrides:completion:)`` +- ``publisher(forEvent:params:paywallOverrides:isFeatureGatable:)`` - ``getPresentationResult(forEvent:)`` - ``getPresentationResult(forEvent:params:)-9ivi6`` - ``getPresentationResult(forEvent:params:)-60qtr`` diff --git a/Sources/SuperwallKit/Models/Paywall/PaywallProducts.swift b/Sources/SuperwallKit/Models/Paywall/PaywallProducts.swift index 69332b31d..59f2af42b 100644 --- a/Sources/SuperwallKit/Models/Paywall/PaywallProducts.swift +++ b/Sources/SuperwallKit/Models/Paywall/PaywallProducts.swift @@ -10,7 +10,7 @@ import StoreKit /// Defines primary, secondary and tertiary products to be used on the paywall. /// -/// Pass an instance of this to ``Superwall/track(event:params:paywallOverrides:paywallHandler:)`` to replace your remotely defined products. +/// Pass an instance of this to ``PaywallOverrides/products`` to replace your remotely defined products. @objc(SWKPaywallProducts) @objcMembers public final class PaywallProducts: NSObject, Sendable { diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift index 05b8ba13c..d1eb09dbf 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift @@ -40,7 +40,7 @@ extension Superwall { paywallOverrides: PaywallOverrides? = nil, completion: @escaping (Result) -> Void ) { - Task { @MainActor in + Task { @MainActor in do { let paywallViewController = try await getPaywallViewController( forEvent: event, @@ -81,13 +81,13 @@ extension Superwall { /// - Throws: An `Error` explaining why it couldn't get the view controller. @MainActor public func getPaywallViewController( - forEvent: String, + forEvent event: String, params: [String: Any]? = nil, paywallOverrides: PaywallOverrides? = nil ) async throws -> PaywallViewController { return try await Future { let trackableEvent = UserInitiatedEvent.Track( - rawName: forEvent, + rawName: event, canImplicitlyTriggerPaywall: false, customParameters: params ?? [:], isFeatureGatable: false diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift index 1fd35622e..3c8af1465 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Presentation Result/PublicGetPresentationResult.swift @@ -10,22 +10,19 @@ import Foundation typealias PresentationPipelineError = PaywallPresentationRequestStatusReason extension Superwall { - /// Preemptively get the result of tracking an event. + /// Preemptively gets the result of registering an event. /// - /// Use this function if you want to preemptively get the result of tracking - /// an event. - /// - /// This is useful for when you want to know whether a particular event will - /// present a paywall in the future. + /// This helps you determine whether a particular event will present a paywall + /// in the future. /// /// Note that this method does not present a paywall. To do that, use - /// ``track(event:params:paywallOverrides:paywallHandler:)``. + /// ``register(event:params:handler:feature:)``. /// /// - Parameters: - /// - event: The name of the event you want to track. + /// - event: The name of the event you want to register. /// - params: Optional parameters you'd like to pass with your event. /// - /// - Returns: A ``PresentationResult`` that indicates the result of tracking an event. + /// - Returns: A ``PresentationResult`` that indicates the result of registering an event. public func getPresentationResult( forEvent event: String, params: [String: Any]? = nil @@ -37,19 +34,16 @@ extension Superwall { ) } - /// Preemptively get the result of tracking an event. - /// - /// Use this function if you want to preemptively get the result of tracking - /// an event. + /// Preemptively gets the result of registering an event. /// - /// This is useful for when you want to know whether a particular event will - /// present a paywall in the future. + /// This helps you determine whether a particular event will present a paywall + /// in the future. /// /// Note that this method does not present a paywall. To do that, use - /// ``track(event:params:paywallOverrides:paywallHandler:)``. + /// ``register(event:params:handler:feature:)``. /// /// - Parameters: - /// - event: The name of the event you want to track. + /// - event: The name of the event you want to register. /// - params: Optional parameters you'd like to pass with your event. /// - completion: A completion block that accepts a ``PresentationResult`` indicating /// the result of tracking an event. @@ -64,8 +58,8 @@ extension Superwall { } } - /// Call when you need to get the presentation result from an implicit event. This prevents logs being - /// fired. + /// Called internally when you need to get the presentation result from an implicit event. + /// This prevents logs being fired. func getImplicitPresentationResult(forEvent event: String) async -> PresentationResult { return await internallyGetPresentationResult( forEvent: event, @@ -109,16 +103,16 @@ extension Superwall { return await getPresentationResult(for: presentationRequest) } - /// Objective-C-only function to preemptively get the result of tracking an event. + /// Objective-C-only function to preemptively gets the result of registering an event. /// - /// This is useful for when you want to know whether a particular event will - /// present a paywall in the future. + /// This helps you determine whether a particular event will present a paywall + /// in the future. /// /// Note that this method does not present a paywall. To do that, use - /// ``track(event:params:products:ignoreSubscriptionStatus:presentationStyleOverride:onSkip:onPresent:onDismiss:)``. + /// ``register(event:params:handler:feature:)``. /// /// - Parameters: - /// - event: The name of the event you want to track. + /// - event: The name of the event you want to register. /// - params: Optional parameters you'd like to pass with your event. /// /// - Returns: A ``PresentationResultObjc`` object that contains information about the result of tracking an event. @@ -131,17 +125,15 @@ extension Superwall { return PresentationResultObjc(trackResult: result) } - /// Objective-C-only function to preemptively get the result of tracking an event. + /// Objective-C-only function to preemptively gets the result of registering an event. /// - /// This is useful for when you want to know whether a particular event will - /// present a paywall in the future. + /// This helps you determine whether a particular event will present a paywall + /// in the future. /// /// Note that this method does not present a paywall. To do that, use - /// ``track(event:params:products:ignoreSubscriptionStatus:presentationStyleOverride:onSkip:onPresent:onDismiss:)``. - /// - /// - Parameters: - /// - event: The name of the event you want to track. + /// ``register(event:params:handler:feature:)``. /// + /// - Parameters event: The name of the event you want to register. /// - Returns: A ``PresentationResultObjc`` object that contains information about the result of tracking an event. @available(swift, obsoleted: 1.0) @objc public func getPresentationResult( diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PaywallOverrides.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PaywallOverrides.swift index 01f5cddd3..86d9fc473 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PaywallOverrides.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation Request/PaywallOverrides.swift @@ -9,7 +9,7 @@ import Foundation /// Override the default behavior and products of a paywall. /// -/// Provide an instance of this to ``Superwall/track(event:params:paywallOverrides:paywallHandler:)``. +/// Provide an instance of this to ``Superwall/getPaywallViewController(forEvent:params:paywallOverrides:)``. public final class PaywallOverrides: NSObject, Sendable { /// Defines the products to override on the paywall. /// @@ -24,7 +24,7 @@ public final class PaywallOverrides: NSObject, Sendable { /// Override the default behavior and products of a paywall. /// - /// Provide an instance of this to ``Superwall/track(event:params:paywallOverrides:paywallHandler:)``. + /// Provide an instance of this to ``Superwall/getPaywallViewController(forEvent:params:paywallOverrides:)``. /// /// - parameters: /// - products: A ``PaywallProducts`` object defining the products to override on the paywall. diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift index 1ff56f5c4..4c54b7fed 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift @@ -36,9 +36,6 @@ public enum PaywallSkippedReason: Sendable, Equatable { /// behavior in the paywall editor. case userIsSubscribed -// /// An error occurred. -// case error(Error) - public static func == (lhs: PaywallSkippedReason, rhs: PaywallSkippedReason) -> Bool { switch (lhs, rhs) { case (.noRuleMatch, .noRuleMatch), @@ -47,8 +44,6 @@ public enum PaywallSkippedReason: Sendable, Equatable { return true case let (.holdout(experiment1), .holdout(experiment2)): return experiment1 == experiment2 -// case let (.error(error1), .error(error2)): -// return error1.localizedDescription == error2.localizedDescription default: return false } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift index e7b09989d..972852e86 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallState.swift @@ -43,5 +43,4 @@ public enum PaywallState { /// The paywall was intentionally skipped. Contains a ``PaywallSkippedReason`` enum whose cases state why the paywall was skipped. case skipped(PaywallSkippedReason) - } diff --git a/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift b/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift index e6ed84c15..86c49c48b 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PaywallInfo.swift @@ -9,9 +9,7 @@ import Foundation import StoreKit -/// Contains information about a given paywall. -/// -/// This is returned in the `paywallState` after presenting a paywall with ``Superwall/track(event:params:paywallOverrides:paywallHandler:)``. +/// Contains information about a paywall. @objc(SWKPaywallInfo) @objcMembers public final class PaywallInfo: NSObject { diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index cd9799d75..37832ace2 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -4,7 +4,6 @@ // // Created by Jake Mor on 10/9/21. // -// swiftlint:disable line_length file_length function_body_length import Foundation import Combine @@ -159,12 +158,11 @@ extension Superwall { handler?.onErrorHandler?(error) // otherwise turning internet off would give unlimited access } } - } )) } - /// Returns a publisher that tracks an event which, when added to a campaign on the Superwall dashboard, can show a paywall. + /// Returns a publisher that registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. /// /// This shows a paywall to the user when: An event you provide is added to a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); the user matches a rule in the campaign; and the user doesn't have an active subscription. /// @@ -173,7 +171,7 @@ extension Superwall { /// The paywall shown to the user is determined by the rules defined in the campaign. When a user is assigned a paywall within a rule, they will continue to see that paywall unless you remove the paywall from the rule or reset assignments to the paywall. /// /// - Parameters: - /// - event: The name of the event you wish to track. + /// - event: The name of the event you wish to register. /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will be dropped. /// - paywallOverrides: An optional ``PaywallOverrides`` object whose parameters override the paywall defaults. Use this to override products, presentation style, and whether it ignores the subscription status. Defaults to `nil`. /// diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index b3d0bb804..7564a82c7 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -91,7 +91,7 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading // MARK: - Private Properties /// Internal passthrough subject that emits ``PaywallState`` objects. These state objects feed back to - /// the caller of ``Superwall/track(event:params:paywallOverrides:paywallHandler:)`` + /// the caller of ``Superwall/register(event:params:handler:feature:)`` /// /// This publisher is set on presentation of the paywall. private var paywallStateSubject: PassthroughSubject! From f8aa22914a665ae487f9dbfc1136e027cca959b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 4 May 2023 12:45:06 +0100 Subject: [PATCH 19/27] Update tests --- .../TrackableSuperwallEvent.swift | 6 +- .../Internal Tracking/TrackTests.swift | 397 +++++++++++------- .../Config/ConfigManagerTests.swift | 3 +- .../Network/NetworkTests.swift | 3 +- .../CheckForPaywallResultTests.swift | 8 +- .../CheckPaywallIsPresentableTests.swift | 4 +- ...etPaywallViewControllerNoChecksTests.swift | 8 +- ...eckDebuggerPresentationOperatorTests.swift | 17 +- ...CheckPaywallPresentableOperatorTests.swift | 20 +- .../CheckUserSubscriptionOperatorTests.swift | 17 +- ...ft => ConfirmHoldoutAssignmentTests.swift} | 15 +- ...onfirmPaywallAssignmentOperatorTests.swift | 9 +- .../EvaluateRulesOperatorTests.swift | 16 +- ....swift => GetPaywallVcOperatorTests.swift} | 23 +- .../HandleTriggerResultOperatorTests.swift | 11 +- .../PresentPaywallOperatorTests.swift | 9 +- .../WaitToPresentOperatorTests.swift | 11 +- .../Core Data/CoreDataManagerTests.swift | 8 +- 18 files changed, 320 insertions(+), 265 deletions(-) rename Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/{ConfirmHoldoutAssignment.swift => ConfirmHoldoutAssignmentTests.swift} (92%) rename Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/{GetPaywallVcOperator.swift => GetPaywallVcOperatorTests.swift} (91%) diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift index 32997615f..9fd040857 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift @@ -254,10 +254,10 @@ enum InternalSuperwallEvent { var customParameters: [String: Any] = [:] func getSuperwallParameters() async -> [String: Any] { [ - "eventName": eventData?.name ?? "", - "pipelineType": type.rawValue, + "source_event_name": eventData?.name ?? "", + "pipeline_type": type.rawValue, "status": status.rawValue, - "statusReason": statusReason?.description ?? "" + "status_reason": statusReason?.description ?? "" ] } } diff --git a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift index 05004586c..d9962c8f1 100644 --- a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift +++ b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift @@ -162,15 +162,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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) } @@ -230,7 +230,7 @@ final class TrackingTests: XCTestCase { XCTAssertEqual(result.parameters.eventParams["$result"] as! String, "present") XCTAssertEqual(result.parameters.eventParams["$trigger_name"] as! String, triggerName) XCTAssertEqual(result.parameters.eventParams["$variant_id"] as! String, experiment.variant.id) - XCTAssertEqual(result.parameters.eventParams["$paywall_identifier"] as! String, experiment.variant.paywallId) + XCTAssertEqual(result.parameters.eventParams["$paywall_identifier"] as! String, experiment.variant.paywallId!) XCTAssertEqual(result.parameters.eventParams["$experiment_id"] as! String, experiment.id) } @@ -257,61 +257,148 @@ final class TrackingTests: XCTestCase { XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "trigger_fire") } - func test_unableToPresent_userIsSubscribed() async { - let result = await Superwall.shared.track(InternalSuperwallEvent.UnableToPresent(state: .userIsSubscribed)) + func test_presentationRequest_userIsSubscribed() async { + let eventData: EventData = .stub() + let event = InternalSuperwallEvent.PresentationRequest( + eventData: eventData, + type: .getPaywallViewController, + status: .noPresentation, + statusReason: .userIsSubscribed + ) + let result = await Superwall.shared.track(event) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) - XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationFail_userIsSubscribed") + XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationRequest") + XCTAssertEqual(result.parameters.eventParams["$source_event_name"] as! String, eventData.name) + XCTAssertEqual(result.parameters.eventParams["$status"] as! String, "no_presentation") + XCTAssertEqual(result.parameters.eventParams["$pipeline_type"] as! String, "getPaywallViewController") + XCTAssertEqual(result.parameters.eventParams["$status_reason"] as! String, "user_is_subscribed") } - func test_unableToPresent_eventNotFound() async { - let result = await Superwall.shared.track(InternalSuperwallEvent.UnableToPresent(state: .eventNotFound)) + func test_presentationRequest_eventNotFound() async { + let eventData: EventData = .stub() + let event = InternalSuperwallEvent.PresentationRequest( + eventData: eventData, + type: .getPaywallViewController, + status: .noPresentation, + statusReason: .eventNotFound + ) + let result = await Superwall.shared.track(event) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) - XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationFail_eventNotFound") + XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationRequest") + XCTAssertEqual(result.parameters.eventParams["$source_event_name"] as! String, eventData.name) + XCTAssertEqual(result.parameters.eventParams["$status"] as! String, "no_presentation") + XCTAssertEqual(result.parameters.eventParams["$pipeline_type"] as! String, "getPaywallViewController") + XCTAssertEqual(result.parameters.eventParams["$status_reason"] as! String, "event_not_found") } - func test_unableToPresent_noRuleMatch() async { - let result = await Superwall.shared.track(InternalSuperwallEvent.UnableToPresent(state: .noRuleMatch)) + func test_presentationRequest_noRuleMatch() async { + let eventData: EventData = .stub() + let event = InternalSuperwallEvent.PresentationRequest( + eventData: eventData, + type: .getPaywallViewController, + status: .noPresentation, + statusReason: .noRuleMatch + ) + let result = await Superwall.shared.track(event) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) - XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationFail_noRuleMatch") + XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationRequest") + XCTAssertEqual(result.parameters.eventParams["$source_event_name"] as! String, eventData.name) + XCTAssertEqual(result.parameters.eventParams["$status"] as! String, "no_presentation") + XCTAssertEqual(result.parameters.eventParams["$pipeline_type"] as! String, "getPaywallViewController") + XCTAssertEqual(result.parameters.eventParams["$status_reason"] as! String, "no_rule_match") } - func test_unableToPresent_alreadyPresented() async { - let result = await Superwall.shared.track(InternalSuperwallEvent.UnableToPresent(state: .alreadyPresented)) + func test_presentationRequest_alreadyPresented() async { + let eventData: EventData = .stub() + let event = InternalSuperwallEvent.PresentationRequest( + eventData: eventData, + type: .getPaywallViewController, + status: .noPresentation, + statusReason: .paywallAlreadyPresented + ) + let result = await Superwall.shared.track(event) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) - XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationFail_alreadyPresented") + XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationRequest") + XCTAssertEqual(result.parameters.eventParams["$source_event_name"] as! String, eventData.name) + XCTAssertEqual(result.parameters.eventParams["$status"] as! String, "no_presentation") + XCTAssertEqual(result.parameters.eventParams["$pipeline_type"] as! String, "getPaywallViewController") + XCTAssertEqual(result.parameters.eventParams["$status_reason"] as! String, "paywall_already_presented") } - func test_unableToPresent_debuggerLaunched() async { - let result = await Superwall.shared.track(InternalSuperwallEvent.UnableToPresent(state: .debuggerLaunched)) + func test_presentationRequest_debuggerLaunched() async { + let eventData: EventData = .stub() + let event = InternalSuperwallEvent.PresentationRequest( + eventData: eventData, + type: .getPaywallViewController, + status: .noPresentation, + statusReason: .debuggerPresented + ) + let result = await Superwall.shared.track(event) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) - XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationFail_debuggerLaunched") + XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationRequest") + XCTAssertEqual(result.parameters.eventParams["$source_event_name"] as! String, eventData.name) + XCTAssertEqual(result.parameters.eventParams["$status"] as! String, "no_presentation") + XCTAssertEqual(result.parameters.eventParams["$pipeline_type"] as! String, "getPaywallViewController") + XCTAssertEqual(result.parameters.eventParams["$status_reason"] as! String, "debugger_presented") } - func test_unableToPresent_noPresenter() async { - let result = await Superwall.shared.track(InternalSuperwallEvent.UnableToPresent(state: .noPresenter)) + func test_presentationRequest_noPresenter() async { + let eventData: EventData = .stub() + let event = InternalSuperwallEvent.PresentationRequest( + eventData: eventData, + type: .getPaywallViewController, + status: .noPresentation, + statusReason: .noPresenter + ) + let result = await Superwall.shared.track(event) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) - XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationFail_noPresenter") + XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationRequest") + XCTAssertEqual(result.parameters.eventParams["$source_event_name"] as! String, eventData.name) + XCTAssertEqual(result.parameters.eventParams["$status"] as! String, "no_presentation") + XCTAssertEqual(result.parameters.eventParams["$pipeline_type"] as! String, "getPaywallViewController") + XCTAssertEqual(result.parameters.eventParams["$status_reason"] as! String, "no_presenter") } - func test_unableToPresent_noPaywallViewController() async { - let result = await Superwall.shared.track(InternalSuperwallEvent.UnableToPresent(state: .noPaywallViewController)) + func test_presentationRequest_noPaywallViewController() async { + let eventData: EventData = .stub() + let event = InternalSuperwallEvent.PresentationRequest( + eventData: eventData, + type: .getPaywallViewController, + status: .noPresentation, + statusReason: .noPaywallViewController + ) + let result = await Superwall.shared.track(event) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) - XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationFail_noPaywallViewController") + XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationRequest") + XCTAssertEqual(result.parameters.eventParams["$source_event_name"] as! String, eventData.name) + XCTAssertEqual(result.parameters.eventParams["$status"] as! String, "no_presentation") + XCTAssertEqual(result.parameters.eventParams["$pipeline_type"] as! String, "getPaywallViewController") + XCTAssertEqual(result.parameters.eventParams["$status_reason"] as! String, "no_paywall_view_controller") } - func test_unableToPresent_holdout() async { - let experiment: Experiment = .stub() - let result = await Superwall.shared.track(InternalSuperwallEvent.UnableToPresent(state: .holdout(experiment))) + func test_presentationRequest_holdout() async { + let eventData: EventData = .stub() + let event = InternalSuperwallEvent.PresentationRequest( + eventData: eventData, + type: .getPaywallViewController, + status: .noPresentation, + statusReason: .holdout(.stub()) + ) + let result = await Superwall.shared.track(event) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) - XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationFail_holdout") + XCTAssertEqual(result.parameters.eventParams["$event_name"] as! String, "paywallPresentationRequest") + XCTAssertEqual(result.parameters.eventParams["$source_event_name"] as! String, eventData.name) + XCTAssertEqual(result.parameters.eventParams["$status"] as! String, "no_presentation") + XCTAssertEqual(result.parameters.eventParams["$pipeline_type"] as! String, "getPaywallViewController") + XCTAssertEqual(result.parameters.eventParams["$status_reason"] as! String, "holdout") } func test_paywallOpen() async { @@ -330,15 +417,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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_open") @@ -360,15 +447,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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") @@ -395,15 +482,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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["$product_id"] as! String, productId) @@ -464,15 +551,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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["$product_id"] as! String, productId) @@ -533,15 +620,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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["$product_id"] as! String, productId) @@ -602,15 +689,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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["$product_id"] as! String, productId) @@ -672,15 +759,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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["$product_id"] as! String, productId) @@ -723,8 +810,6 @@ final class TrackingTests: XCTestCase { let product = StoreProduct(sk1Product: MockSkProduct(productIdentifier: productId)) let dependencyContainer = DependencyContainer() let skTransaction = MockSKPaymentTransaction(state: .purchased) - let transaction = await dependencyContainer.makeStoreTransaction(from: skTransaction) - let error = TransactionError.failure("failed mate", product) let result = await Superwall.shared.track(InternalSuperwallEvent.SubscriptionStart(paywallInfo: paywallInfo, product: product)) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) @@ -739,15 +824,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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["$product_id"] as! String, productId) @@ -788,8 +873,6 @@ final class TrackingTests: XCTestCase { let product = StoreProduct(sk1Product: MockSkProduct(productIdentifier: productId)) let dependencyContainer = DependencyContainer() let skTransaction = MockSKPaymentTransaction(state: .purchased) - let transaction = await dependencyContainer.makeStoreTransaction(from: skTransaction) - let error = TransactionError.failure("failed mate", product) let result = await Superwall.shared.track(InternalSuperwallEvent.FreeTrialStart(paywallInfo: paywallInfo, product: product)) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) @@ -804,15 +887,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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["$product_id"] as! String, productId) @@ -853,8 +936,6 @@ final class TrackingTests: XCTestCase { let product = StoreProduct(sk1Product: MockSkProduct(productIdentifier: productId)) let dependencyContainer = DependencyContainer() let skTransaction = MockSKPaymentTransaction(state: .purchased) - let transaction = await dependencyContainer.makeStoreTransaction(from: skTransaction) - let error = TransactionError.failure("failed mate", product) let result = await Superwall.shared.track(InternalSuperwallEvent.NonRecurringProductPurchase(paywallInfo: paywallInfo, product: product)) XCTAssertNotNil(result.parameters.eventParams["$app_session_id"]) XCTAssertTrue(result.parameters.eventParams["$is_standard_event"] as! Bool) @@ -869,15 +950,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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["$product_id"] as! String, productId) @@ -928,15 +1009,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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) XCTAssertNotNil(result.parameters.eventParams["$primary_product_id"]) @@ -961,15 +1042,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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) XCTAssertNotNil(result.parameters.eventParams["$primary_product_id"]) @@ -994,15 +1075,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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) XCTAssertNotNil(result.parameters.eventParams["$primary_product_id"]) @@ -1027,15 +1108,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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) XCTAssertNotNil(result.parameters.eventParams["$primary_product_id"]) @@ -1061,15 +1142,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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) XCTAssertNotNil(result.parameters.eventParams["$primary_product_id"]) @@ -1095,15 +1176,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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) XCTAssertNotNil(result.parameters.eventParams["$primary_product_id"]) @@ -1129,15 +1210,15 @@ final class TrackingTests: XCTestCase { 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_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_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_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) XCTAssertNotNil(result.parameters.eventParams["$primary_product_id"]) diff --git a/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift b/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift index cfaf75c8c..a6d4852e8 100644 --- a/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift +++ b/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift @@ -64,8 +64,7 @@ final class ConfigManagerTests: XCTestCase { expectation.fulfill() } - await waitForExpectations(timeout: 1) - + await fulfillment(of: [expectation], timeout: 1) XCTAssertTrue(storage.getConfirmedAssignments().isEmpty) XCTAssertTrue(configManager.unconfirmedAssignments.isEmpty) diff --git a/Tests/SuperwallKitTests/Network/NetworkTests.swift b/Tests/SuperwallKitTests/Network/NetworkTests.swift index 38fdfeb5b..a0ce6c377 100644 --- a/Tests/SuperwallKitTests/Network/NetworkTests.swift +++ b/Tests/SuperwallKitTests/Network/NetworkTests.swift @@ -49,7 +49,8 @@ final class NetworkTests: XCTestCase { expectation.fulfill() } - wait(for: [expectation], timeout: 0.4) + await fulfillment(of: [expectation], timeout: 0.4) + XCTAssertFalse(urlSession.didRequest) } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift index cba626788..b5aa6128e 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift @@ -79,7 +79,7 @@ final class CheckForPaywallResultTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } func test_checkForPaywallResult_holdout() async { @@ -118,7 +118,7 @@ final class CheckForPaywallResultTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } func test_checkForPaywallResult_error() async { @@ -158,7 +158,7 @@ final class CheckForPaywallResultTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } func test_checkForPaywallResult_paywall() async { @@ -192,6 +192,6 @@ final class CheckForPaywallResultTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift index 132a1798e..5bc8c2c72 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift @@ -52,7 +52,7 @@ final class CheckPaywallIsPresentableTests: XCTestCase { try? await Task.sleep(nanoseconds: 100_000_000) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } func test_checkPaywallIsPresentable_userNotSubscribed() async { @@ -91,6 +91,6 @@ final class CheckPaywallIsPresentableTests: XCTestCase { try? await Task.sleep(nanoseconds: 100_000_000) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/GetPaywallViewControllerNoChecksTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/GetPaywallViewControllerNoChecksTests.swift index 1f5ed75d3..3ed5c72d4 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/GetPaywallViewControllerNoChecksTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/GetPaywallViewControllerNoChecksTests.swift @@ -19,7 +19,7 @@ final class GetPaywallVcNoChecksOperatorTests: XCTestCase { factory: dependencyContainer, paywallRequestManager: dependencyContainer.paywallRequestManager ) - paywallManager.getPaywallError = PresentationPipelineError.cancelled + paywallManager.getPaywallError = PresentationPipelineError.noPaywallViewController let publisher = CurrentValueSubject(SubscriptionStatus.inactive) .eraseToAnyPublisher() @@ -56,9 +56,7 @@ final class GetPaywallVcNoChecksOperatorTests: XCTestCase { ) .store(in: &cancellables) - try? await Task.sleep(nanoseconds: 1_000_000) - - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } @MainActor @@ -107,6 +105,6 @@ final class GetPaywallVcNoChecksOperatorTests: XCTestCase { try? await Task.sleep(nanoseconds: 1_000_000) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift index 20dbfe194..52658750e 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift @@ -40,7 +40,7 @@ final class CheckDebuggerPresentationOperatorTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [continuePipelineExpectation, stateExpectation], timeout: 0.1) + await fulfillment(of: [continuePipelineExpectation, stateExpectation], timeout: 0.1) } func test_checkDebuggerPresentation_debuggerLaunched_presentingOnDebugger() async { @@ -74,7 +74,7 @@ final class CheckDebuggerPresentationOperatorTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [continuePipelineExpectation, stateExpectation], timeout: 0.1) + await fulfillment(of: [continuePipelineExpectation, stateExpectation], timeout: 0.1) } func test_checkDebuggerPresentation_debuggerLaunched_notPresentingOnDebugger() async { @@ -88,14 +88,9 @@ final class CheckDebuggerPresentationOperatorTests: XCTestCase { let stateExpectation = expectation(description: "Output a state") statePublisher.sink { state in switch state { - case .skipped(let reason): - switch reason { - case .error(let error): - if (error as NSError).code == 101 { - stateExpectation.fulfill() - } - default: - break + case .presentationError(let error): + if (error as NSError).code == 101 { + stateExpectation.fulfill() } default: break @@ -125,7 +120,7 @@ final class CheckDebuggerPresentationOperatorTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [continuePipelineExpectation, stateExpectation], timeout: 0.1) + await fulfillment(of: [continuePipelineExpectation, stateExpectation], timeout: 0.1) } /* diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift index ab9df92bc..39e91656b 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift @@ -72,7 +72,7 @@ final class CheckPaywallPresentableOperatorTests: XCTestCase { try? await Task.sleep(nanoseconds: 500_000_000) - wait(for: [expectation, stateExpectation], timeout: 2) + await fulfillment(of: [expectation, stateExpectation], timeout: 2) } @MainActor @@ -90,13 +90,8 @@ final class CheckPaywallPresentableOperatorTests: XCTestCase { } } receiveValue: { state in switch state { - case .skipped(let reason): - switch reason { - case .error: - stateExpectation.fulfill() - default: - break - } + case .presentationError: + stateExpectation.fulfill() default: break } @@ -112,7 +107,8 @@ final class CheckPaywallPresentableOperatorTests: XCTestCase { .explicitTrigger(.stub()), isDebuggerLaunched: false, subscriptionStatus: inactiveSubscriptionPublisher, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) .setting(\.presenter, to: nil) @@ -145,9 +141,7 @@ final class CheckPaywallPresentableOperatorTests: XCTestCase { ) .store(in: &cancellables) - try? await Task.sleep(nanoseconds: 500_000_000) - - wait(for: [expectation, stateExpectation], timeout: 2) + await fulfillment(of: [expectation, stateExpectation], timeout: 2) } @MainActor @@ -198,6 +192,6 @@ final class CheckPaywallPresentableOperatorTests: XCTestCase { try? await Task.sleep(nanoseconds: 500_000_000) - wait(for: [expectation, stateExpectation], timeout: 2) + await fulfillment(of: [expectation, stateExpectation], timeout: 2) } } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift index d3bd61b63..2c0ff3291 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift @@ -21,7 +21,8 @@ final class CheckUserSubscriptionOperatorTests: XCTestCase { .explicitTrigger(.stub()), isDebuggerLaunched: false, subscriptionStatus: publisher, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) let input = AssignmentPipelineOutput( @@ -70,7 +71,7 @@ final class CheckUserSubscriptionOperatorTests: XCTestCase { try? await Task.sleep(nanoseconds: 10_000_000) - wait(for: [pipelineExpectation, stateExpectation], timeout: 0.1) + await fulfillment(of: [pipelineExpectation, stateExpectation], timeout: 0.1) } func test_checkUserSubscription_paywall() async { @@ -82,7 +83,8 @@ final class CheckUserSubscriptionOperatorTests: XCTestCase { .explicitTrigger(.stub()), isDebuggerLaunched: false, subscriptionStatus: publisher, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) let input = AssignmentPipelineOutput( @@ -116,7 +118,7 @@ final class CheckUserSubscriptionOperatorTests: XCTestCase { try? await Task.sleep(nanoseconds: 10_000_000) - wait(for: [pipelineExpectation, stateExpectation], timeout: 0.1) + await fulfillment(of: [pipelineExpectation, stateExpectation], timeout: 0.1) } func test_checkUserSubscription_notPaywall_userNotSubscribed() async { @@ -128,7 +130,8 @@ final class CheckUserSubscriptionOperatorTests: XCTestCase { .explicitTrigger(.stub()), isDebuggerLaunched: false, subscriptionStatus: publisher, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) let input = AssignmentPipelineOutput( @@ -161,7 +164,7 @@ final class CheckUserSubscriptionOperatorTests: XCTestCase { .store(in: &cancellables) try? await Task.sleep(nanoseconds: 10_000_000) - - wait(for: [pipelineExpectation, stateExpectation], timeout: 0.1) + + await fulfillment(of: [pipelineExpectation, stateExpectation], timeout: 0.1) } } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignmentTests.swift similarity index 92% rename from Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignmentTests.swift index 373a66e6a..cb817d3ae 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignmentTests.swift @@ -34,7 +34,8 @@ final class ConfirmHoldoutAssignmentOperatorTests: XCTestCase { .explicitTrigger(.stub()), isDebuggerLaunched: false, subscriptionStatus: inactiveSubscriptionPublisher, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) let input = AssignmentPipelineOutput( request: request, @@ -56,7 +57,7 @@ final class ConfirmHoldoutAssignmentOperatorTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [pipelineExpectation], timeout: 0.1) + await fulfillment(of: [pipelineExpectation], timeout: 0.1) XCTAssertFalse(configManager.confirmedAssignment) } @@ -81,7 +82,8 @@ final class ConfirmHoldoutAssignmentOperatorTests: XCTestCase { .explicitTrigger(.stub()), isDebuggerLaunched: false, subscriptionStatus: inactiveSubscriptionPublisher, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) let input = AssignmentPipelineOutput( @@ -105,7 +107,7 @@ final class ConfirmHoldoutAssignmentOperatorTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [pipelineExpectation], timeout: 0.1) + await fulfillment(of: [pipelineExpectation], timeout: 0.1) XCTAssertFalse(configManager.confirmedAssignment) } @@ -129,7 +131,8 @@ final class ConfirmHoldoutAssignmentOperatorTests: XCTestCase { .explicitTrigger(.stub()), isDebuggerLaunched: false, subscriptionStatus: inactiveSubscriptionPublisher, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) let input = AssignmentPipelineOutput( request: request, @@ -152,7 +155,7 @@ final class ConfirmHoldoutAssignmentOperatorTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [pipelineExpectation], timeout: 0.1) + await fulfillment(of: [pipelineExpectation], timeout: 0.1) XCTAssertTrue(configManager.confirmedAssignment) } } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperatorTests.swift index f91a02dc7..1eed604ff 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperatorTests.swift @@ -51,7 +51,7 @@ final class ConfirmPaywallAssignmentOperatorTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } @MainActor @@ -94,7 +94,7 @@ final class ConfirmPaywallAssignmentOperatorTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } @MainActor @@ -114,7 +114,8 @@ final class ConfirmPaywallAssignmentOperatorTests: XCTestCase { let request = dependencyContainer.makePresentationRequest( .explicitTrigger(.stub()), isDebuggerLaunched: false, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) let input = PresentablePipelineOutput( @@ -140,6 +141,6 @@ final class ConfirmPaywallAssignmentOperatorTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/EvaluateRulesOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/EvaluateRulesOperatorTests.swift index 883089c59..6315a455f 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/EvaluateRulesOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/EvaluateRulesOperatorTests.swift @@ -22,7 +22,8 @@ final class EvaluateRulesOperatorTests: XCTestCase { .fromIdentifier(identifier, freeTrialOverride: false), isDebuggerLaunched: true, subscriptionStatus: inactiveSubscriptionPublisher, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) let debugInfo: [String: Any] = [:] @@ -30,7 +31,7 @@ final class EvaluateRulesOperatorTests: XCTestCase { CurrentValueSubject((request, debugInfo)) .setFailureType(to: Error.self) .eraseToAnyPublisher() - .evaluateRules(isPreemptive: false) + .evaluateRules() .eraseToAnyPublisher() .sink( receiveCompletion: { _ in }, @@ -54,7 +55,7 @@ final class EvaluateRulesOperatorTests: XCTestCase { try? await Task.sleep(nanoseconds: 100_000_000) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } func test_evaluateRules_isNotDebugger() async { @@ -65,7 +66,8 @@ final class EvaluateRulesOperatorTests: XCTestCase { .explicitTrigger(.stub()), isDebuggerLaunched: false, subscriptionStatus: inactiveSubscriptionPublisher, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) let debugInfo: [String: Any] = [:] @@ -73,7 +75,7 @@ final class EvaluateRulesOperatorTests: XCTestCase { CurrentValueSubject((request, debugInfo)) .setFailureType(to: Error.self) .eraseToAnyPublisher() - .evaluateRules(isPreemptive: false) + .evaluateRules() .eraseToAnyPublisher() .sink( receiveCompletion: { _ in }, @@ -91,7 +93,7 @@ final class EvaluateRulesOperatorTests: XCTestCase { .store(in: &cancellables) try? await Task.sleep(nanoseconds: 100_000_000) - - wait(for: [expectation], timeout: 0.1) + + await fulfillment(of: [expectation], timeout: 0.1) } } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperatorTests.swift similarity index 91% rename from Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperatorTests.swift index 97df614d8..da401ab33 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperatorTests.swift @@ -45,7 +45,7 @@ final class GetPaywallVcOperatorTests: XCTestCase { factory: dependencyContainer, paywallRequestManager: dependencyContainer.paywallRequestManager ) - paywallManager.getPaywallError = PresentationPipelineError.cancelled + paywallManager.getPaywallError = PresentationPipelineError.noPaywallViewController let publisher = CurrentValueSubject(SubscriptionStatus.active) .eraseToAnyPublisher() @@ -84,7 +84,7 @@ final class GetPaywallVcOperatorTests: XCTestCase { try? await Task.sleep(nanoseconds: 1_000_000) - wait(for: [expectation, stateExpectation], timeout: 0.1) + await fulfillment(of: [expectation, stateExpectation], timeout: 0.1) } @MainActor @@ -102,13 +102,8 @@ final class GetPaywallVcOperatorTests: XCTestCase { } } receiveValue: { state in switch state { - case .skipped(let reason): - switch reason { - case .error: - stateExpectation.fulfill() - default: - break - } + case .presentationError: + stateExpectation.fulfill() default: break } @@ -120,7 +115,7 @@ final class GetPaywallVcOperatorTests: XCTestCase { factory: dependencyContainer, paywallRequestManager: dependencyContainer.paywallRequestManager ) - paywallManager.getPaywallError = PresentationPipelineError.cancelled + paywallManager.getPaywallError = PresentationPipelineError.userIsSubscribed let publisher = CurrentValueSubject(SubscriptionStatus.inactive) .eraseToAnyPublisher() @@ -157,9 +152,7 @@ final class GetPaywallVcOperatorTests: XCTestCase { ) .store(in: &cancellables) - try? await Task.sleep(nanoseconds: 1_000_000) - - wait(for: [expectation, stateExpectation], timeout: 0.1) + await fulfillment(of: [expectation, stateExpectation], timeout: 0.1) } @MainActor @@ -210,8 +203,6 @@ final class GetPaywallVcOperatorTests: XCTestCase { ) .store(in: &cancellables) - try? await Task.sleep(nanoseconds: 1_000_000) - - wait(for: [expectation, stateExpectation], timeout: 0.1) + await fulfillment(of: [expectation, stateExpectation], timeout: 0.1) } } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift index 5be56b38b..f6e305bdf 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift @@ -243,14 +243,9 @@ final class HandleTriggerResultOperatorTests: XCTestCase { } } receiveValue: { state in switch state { - case .skipped(let reason): - switch reason { - case .error(let error): - XCTAssertEqual(error as NSError, outputError) - stateExpectation.fulfill() - default: - break - } + case .presentationError(let error): + XCTAssertEqual(error as NSError, outputError) + stateExpectation.fulfill() default: break } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift index 75e25939f..d4e5a7f88 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift @@ -94,13 +94,8 @@ final class PresentPaywallOperatorTests: XCTestCase { } } receiveValue: { state in switch state { - case .skipped(let reason): - switch reason { - case .error: - stateExpectation.fulfill() - default: - break - } + case .presentationError: + stateExpectation.fulfill() default: break } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperatorTests.swift index 985730108..f47578989 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/WaitToPresentOperatorTests.swift @@ -42,7 +42,7 @@ final class WaitToPresentTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } func test_waitToPresent_noIdentity_activeStatus() async { @@ -67,7 +67,7 @@ final class WaitToPresentTests: XCTestCase { ) .store(in: &cancellables) - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } func test_waitToPresent_hasIdentity_activeStatus_noConfig() async { @@ -95,7 +95,7 @@ final class WaitToPresentTests: XCTestCase { identityManager.didSetIdentity() - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } func test_waitToPresent_hasIdentity_activeStatus_hasConfig() async { @@ -109,7 +109,8 @@ final class WaitToPresentTests: XCTestCase { .explicitTrigger(.stub()), paywallOverrides: nil, isDebuggerLaunched: false, - isPaywallPresented: false + isPaywallPresented: false, + type: .getPaywallViewController ) .setting(\.dependencyContainer.identityManager, to: identityManager) .setting(\.flags.subscriptionStatus, to: unknownSubscriptionPublisher) @@ -129,6 +130,6 @@ final class WaitToPresentTests: XCTestCase { identityManager.didSetIdentity() - wait(for: [expectation], timeout: 0.1) + await fulfillment(of: [expectation], timeout: 0.1) } } diff --git a/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerTests.swift b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerTests.swift index 0717b22f5..f5a20bff6 100644 --- a/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerTests.swift +++ b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerTests.swift @@ -92,9 +92,7 @@ class CoreDataManagerTests: XCTestCase { expectation1.fulfill() } - await waitForExpectations(timeout: 2.0) { error in - XCTAssertNil(error, "Save did not occur") - } + await fulfillment(of: [expectation1], timeout: 2) // Save Trigger Rule Occurrence @@ -115,9 +113,7 @@ class CoreDataManagerTests: XCTestCase { expectation2.fulfill() } - await waitForExpectations(timeout: 20.0) { error in - XCTAssertNil(error, "Save did not occur") - } + await fulfillment(of: [expectation2], timeout: 20) // Delete All Entities coreDataManager.deleteAllEntities() From 224fc24e093d573d821704813785d99cc7712094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 4 May 2023 13:06:02 +0100 Subject: [PATCH 20/27] Adds Objective-c helper functions for register --- .../Presentation/PublicPresentation.swift | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index 37832ace2..c692dcba5 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -77,7 +77,7 @@ extension Superwall { /// /// - Parameters: /// - event: The name of the event you wish to register. - /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will be dropped. + /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will be dropped. Defaults to `nil`. /// - handler: An optional handler whose variables provide status updates for a paywall. Defaults to `nil`. /// - feature: A completion block containing a feature that you wish to paywall. Access to this block is remotely configurable via the [Superwall Dashboard](https://superwall.com/dashboard). If the paywall is set to _Non Gated_, this will be called when the paywall is dismissed or if the user is already paying. If the paywall is _Gated_, this will be called only if the user is already paying or if they begin paying. If no paywall is configured, this gets called immediately. This will not be called in the event of an error, which you can detect via the `handler`. public func register( @@ -99,7 +99,7 @@ extension Superwall { /// /// - Parameters: /// - event: The name of the event you wish to register. - /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will be dropped. + /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will be dropped. Defaults to `nil`. /// - handler: An optional handler whose variables provide status updates for a paywall. Defaults to `nil`. public func register( event: String, @@ -161,6 +161,40 @@ extension Superwall { } )) } + + /// Registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. + /// + /// This shows a paywall to the user when: An event you provide is added to a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); the user matches a rule in the campaign; and the user doesn't have an active subscription. + /// + /// Before using this method, you'll first need to create a campaign and add the event to the campaign on the [Superwall Dashboard](https://superwall.com/dashboard). + /// + /// The paywall shown to the user is determined by the rules defined in the campaign. When a user is assigned a paywall within a rule, they will continue to see that paywall unless you remove the paywall from the rule or reset assignments to the paywall. + /// + /// - Parameters: + /// - event: The name of the event you wish to register. + @available(swift, obsoleted: 1.0) + @objc private func register(event: String) { + internallyRegister(event: event) + } + + /// Registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. + /// + /// This shows a paywall to the user when: An event you provide is added to a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); the user matches a rule in the campaign; and the user doesn't have an active subscription. + /// + /// Before using this method, you'll first need to create a campaign and add the event to the campaign on the [Superwall Dashboard](https://superwall.com/dashboard). + /// + /// The paywall shown to the user is determined by the rules defined in the campaign. When a user is assigned a paywall within a rule, they will continue to see that paywall unless you remove the paywall from the rule or reset assignments to the paywall. + /// + /// - Parameters: + /// - event: The name of the event you wish to register. + /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will be dropped. Defaults to `nil`. + @available(swift, obsoleted: 1.0) + @objc private func register( + event: String, + params: [String: Any]? + ) { + 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. /// From b798a6f7f6d2be41627917037c8ac275873fa701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 4 May 2023 13:08:58 +0100 Subject: [PATCH 21/27] Makes new objc funcs public --- .../Paywall/Presentation/PublicPresentation.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index c692dcba5..397844b86 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -173,7 +173,7 @@ extension Superwall { /// - Parameters: /// - event: The name of the event you wish to register. @available(swift, obsoleted: 1.0) - @objc private func register(event: String) { + @objc public func register(event: String) { internallyRegister(event: event) } @@ -189,7 +189,7 @@ extension Superwall { /// - event: The name of the event you wish to register. /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will be dropped. Defaults to `nil`. @available(swift, obsoleted: 1.0) - @objc private func register( + @objc public func register( event: String, params: [String: Any]? ) { From 908f3fea03deaa795a727fb67cedae1a05b2ed1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 4 May 2023 13:41:19 +0100 Subject: [PATCH 22/27] Adds onSkipHandlerObjc to fix lack on onSkip in objc Removes DispatchQueue.main blocks because they shouldn't be needed. --- .../SSAHomeViewController.m | 17 +++++++ .../Extensions/SuperwallExtension.md | 2 + .../PaywallSkippedReason.swift | 3 -- .../PaywallPresentationHandler.swift | 28 +++++++---- .../Presentation/PublicPresentation.swift | 46 +++++++++++-------- 5 files changed, 66 insertions(+), 30 deletions(-) diff --git a/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSAHomeViewController.m b/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSAHomeViewController.m index 153f59142..ab1226ed0 100644 --- a/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSAHomeViewController.m +++ b/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSAHomeViewController.m @@ -55,6 +55,23 @@ - (IBAction)registerEvent:(id)sender { NSLog(@"The paywall presented. PaywallInfo: %@", paywallInfo); }]; + [handler onSkip:^(enum SWKPaywallSkippedReason reason) { + switch (reason) { + case SWKPaywallSkippedReasonUserIsSubscribed: + NSLog(@"Paywall not shown because user is subscribed."); + break; + case SWKPaywallSkippedReasonHoldout: + NSLog(@"Paywall not shown because user is in a holdout group."); + break; + case SWKPaywallSkippedReasonNoRuleMatch: + NSLog(@"Paywall not shown because user doesn't match any rules."); + break; + case SWKPaywallSkippedReasonEventNotFound: + NSLog(@"Paywall not shown because this event isn't part of a campaign."); + break; + } + }]; + [handler onError:^(NSError * _Nonnull error) { NSLog(@"The paywall presentation failed with error %@", error); }]; diff --git a/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md b/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md index 5ef49a43c..853764350 100644 --- a/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md +++ b/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md @@ -30,6 +30,8 @@ The ``Superwall`` class is used to access all the features of the SDK. Before us - ``register(event:params:handler:feature:)`` - ``register(event:params:handler:)`` +- ``register(event:)`` +- ``register(event:params:)`` - ``getPaywallViewController(forEvent:params:paywallOverrides:)`` - ``getPaywallViewController(forEvent:params:paywallOverrides:completion:)`` - ``publisher(forEvent:params:paywallOverrides:isFeatureGatable:)`` diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift index 4c54b7fed..b27d61d2e 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift @@ -74,7 +74,4 @@ public enum PaywallSkippedReasonObjc: Int, Sendable, Equatable { /// By default, paywalls do not show to users who are already subscribed. You can override this /// behavior in the paywall editor. case userIsSubscribed - - /// An error occurred. - case error } diff --git a/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift b/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift index 4c9818676..6c6ce8883 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PaywallPresentationHandler.swift @@ -13,22 +13,25 @@ import Foundation @objcMembers public class PaywallPresentationHandler: NSObject { /// A block called when the paywall did present. - var onPresentHandler: ((_ paywallInfo: PaywallInfo) -> Void)? + var onPresentHandler: ((PaywallInfo) -> Void)? /// A block called when the paywall did dismiss. - var onDismissHandler: ((_ paywallInfo: PaywallInfo) -> Void)? + var onDismissHandler: ((PaywallInfo) -> Void)? /// A block called when an error occurred while trying to present a paywall. - var onErrorHandler: ((_ error: Error) -> Void)? + var onErrorHandler: ((Error) -> Void)? /// A block called when an error occurred while trying to present a paywall. - var onSkipHandler: ((_ reason: PaywallSkippedReason) -> Void)? + var onSkipHandler: ((PaywallSkippedReason) -> Void)? + + /// An objective-c only block called when an error occurred while trying to present a paywall. + var onSkipHandlerObjc: ((PaywallSkippedReasonObjc) -> Void)? /// Sets the handler that will be called when the paywall did presented. /// /// - Parameter handler: A block that accepts a ``PaywallInfo`` object associated with /// the presented paywall. - public func onPresent(_ handler: @escaping (_ paywallInfo: PaywallInfo) -> Void) { + public func onPresent(_ handler: @escaping (PaywallInfo) -> Void) { self.onPresentHandler = handler } @@ -36,7 +39,7 @@ public class PaywallPresentationHandler: NSObject { /// /// - Parameter handler: A block that accepts a ``PaywallInfo`` object associated with /// the dismissed paywall. - public func onDismiss(_ handler: @escaping (_ paywallInfo: PaywallInfo) -> Void) { + public func onDismiss(_ handler: @escaping (PaywallInfo) -> Void) { self.onDismissHandler = handler } @@ -44,7 +47,7 @@ public class PaywallPresentationHandler: NSObject { /// /// - Parameter handler: A block that accepts an `Error` indicating why the paywall /// could not present. - public func onError(_ handler: @escaping (_ error: Error) -> Void) { + public func onError(_ handler: @escaping (Error) -> Void) { self.onErrorHandler = handler } @@ -52,7 +55,16 @@ public class PaywallPresentationHandler: NSObject { /// /// - Parameter handler: A block that accepts a `Error` indicating why the paywall /// could not present. - public func onSkip(_ handler: @escaping (_ reason: PaywallSkippedReason) -> Void) { + public func onSkip(_ handler: @escaping (PaywallSkippedReason) -> Void) { self.onSkipHandler = handler } + + /// Sets the handler that will be called when a paywall is skipped, but no error has occurred. + /// + /// - Parameter handler: A block that accepts a `Error` indicating why the paywall + /// could not present. + @available(swift, obsoleted: 1.0) + public func onSkip(_ handler: @escaping (PaywallSkippedReasonObjc) -> Void) { + self.onSkipHandlerObjc = handler + } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index 397844b86..e82eed695 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -123,46 +123,41 @@ extension Superwall { ) .subscribe(Subscribers.Sink( receiveCompletion: { _ in }, - receiveValue: { state in + receiveValue: { [weak self] state in + guard let self = self else { + return + } switch state { case .presented(let paywallInfo): - DispatchQueue.main.async { handler?.onPresentHandler?(paywallInfo) - } case let .dismissed(paywallInfo, state): - DispatchQueue.main.async { handler?.onDismissHandler?(paywallInfo) - } switch state { case .purchased, .restored: - DispatchQueue.main.async { - completion?() - } + completion?() case .closed: let closeReason = paywallInfo.closeReason let featureGating = paywallInfo.featureGatingBehavior if closeReason != .forNextPaywall && featureGating == .nonGated { - DispatchQueue.main.async { - completion?() - } + completion?() } } case .skipped(let reason): - DispatchQueue.main.async { - handler?.onSkipHandler?(reason) - completion?() + if let handler = handler?.onSkipHandler { + handler(reason) + } else { + let objcReason = self.onSkipConverter(reason: reason) + handler?.onSkipHandlerObjc?(objcReason) } case .presentationError(let error): - DispatchQueue.main.async { - handler?.onErrorHandler?(error) // otherwise turning internet off would give unlimited access - } + handler?.onErrorHandler?(error) // otherwise turning internet off would give unlimited access } } )) } - /// Registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. + /// Objective-C-only convenience method. Registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. /// /// This shows a paywall to the user when: An event you provide is added to a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); the user matches a rule in the campaign; and the user doesn't have an active subscription. /// @@ -177,7 +172,7 @@ extension Superwall { internallyRegister(event: event) } - /// Registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. + /// Objective-C-only convenience method. Registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. /// /// This shows a paywall to the user when: An event you provide is added to a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); the user matches a rule in the campaign; and the user doesn't have an active subscription. /// @@ -243,4 +238,17 @@ extension Superwall { } .eraseToAnyPublisher() } + + private func onSkipConverter(reason: PaywallSkippedReason) -> PaywallSkippedReasonObjc { + switch reason { + case .holdout: + return .holdout + case .noRuleMatch: + return .noRuleMatch + case .eventNotFound: + return .eventNotFound + case .userIsSubscribed: + return .userIsSubscribed + } + } } From e472895cc1402ea3757e2d8c39fd25ceef4f38b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 4 May 2023 13:41:59 +0100 Subject: [PATCH 23/27] swiftlint fix --- .../Presentation/PublicPresentation.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index e82eed695..086c7e796 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -129,20 +129,20 @@ extension Superwall { } switch state { case .presented(let paywallInfo): - handler?.onPresentHandler?(paywallInfo) + handler?.onPresentHandler?(paywallInfo) case let .dismissed(paywallInfo, state): - handler?.onDismissHandler?(paywallInfo) - switch state { - case .purchased, - .restored: + handler?.onDismissHandler?(paywallInfo) + switch state { + case .purchased, + .restored: + completion?() + case .closed: + let closeReason = paywallInfo.closeReason + let featureGating = paywallInfo.featureGatingBehavior + if closeReason != .forNextPaywall && featureGating == .nonGated { completion?() - case .closed: - let closeReason = paywallInfo.closeReason - let featureGating = paywallInfo.featureGatingBehavior - if closeReason != .forNextPaywall && featureGating == .nonGated { - completion?() - } } + } case .skipped(let reason): if let handler = handler?.onSkipHandler { handler(reason) @@ -156,7 +156,7 @@ extension Superwall { } )) } - + /// Objective-C-only convenience method. Registers an event which, when added to a campaign on the Superwall dashboard, can show a paywall. /// /// This shows a paywall to the user when: An event you provide is added to a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); the user matches a rule in the campaign; and the user doesn't have an active subscription. From 211c65e7fe111eae0791f48c23525b36dc23802e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 4 May 2023 15:20:30 +0100 Subject: [PATCH 24/27] Update workflow so tests can run in xcode 14 --- .github/workflows/tag-version.yml | 4 ++-- .github/workflows/tests.yml | 5 ++++- .github/workflows/xcode-12-test.yml | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tag-version.yml b/.github/workflows/tag-version.yml index 6172dede5..6219a9eff 100644 --- a/.github/workflows/tag-version.yml +++ b/.github/workflows/tag-version.yml @@ -28,7 +28,7 @@ jobs: cocoapods: needs: tag - runs-on: macos-12 + runs-on: macos-latest steps: - uses: actions/checkout@v1 - name: Publish to CocoaPod register @@ -39,7 +39,7 @@ jobs: xcode12: needs: tag - runs-on: macos-12 + runs-on: macos-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f4576b960..ed0cb2286 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,9 +15,12 @@ concurrency: jobs: run-tests: - runs-on: macos-12 + runs-on: macos-13 steps: + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '14.3' - name: Git Checkout uses: actions/checkout@v3 - name: xcodegen diff --git a/.github/workflows/xcode-12-test.yml b/.github/workflows/xcode-12-test.yml index 2605b5746..7bbee25c9 100644 --- a/.github/workflows/xcode-12-test.yml +++ b/.github/workflows/xcode-12-test.yml @@ -26,7 +26,7 @@ jobs: cocoapods: needs: tag - runs-on: macos-11 + runs-on: macos-latest steps: - uses: actions/checkout@v1 - name: Publish to CocoaPod register From bcb46f0521f86d5f574c59018462e05c0bed28b8 Mon Sep 17 00:00:00 2001 From: jakemor Date: Thu, 4 May 2023 11:20:30 -0400 Subject: [PATCH 25/27] moves make window key window to right before presentation --- .../Operators/CheckPaywallPresentableOperator.swift | 2 +- .../Paywall/View Controller/PaywallViewController.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift index 3c43ba399..1ad73dc01 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift @@ -107,7 +107,7 @@ extension Superwall { } presentingWindow?.rootViewController = UIViewController() - presentingWindow?.makeKeyAndVisible() +// presentingWindow?.makeKeyAndVisible() presentationItems.window = presentingWindow } diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 7564a82c7..0b2fb7df6 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -478,6 +478,7 @@ public class PaywallViewController: UIViewController, SWWebViewDelegate, Loading || isBeingPresented { return completion(false) } + Superwall.shared.presentationItems.window?.makeKeyAndVisible() addShimmerView(onPresent: true) prepareForPresentation() From 43ff1cdfb0058dba1f6fa7b05cbd66cca60a55dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 4 May 2023 18:20:53 +0100 Subject: [PATCH 26/27] Throws a PaywallSkippedReason when getPaywallViewController called --- .../InternalGetPaywallViewController.swift | 6 +- .../CheckSubscriptionStatusOperator.swift | 0 ...ExtractPaywallViewControllerOperator.swift | 0 .../Operators/MapErrorsOperator.swift | 48 +++++++++++++ .../PublicGetPaywallViewController.swift | 71 +++++++++++++++++-- .../PaywallSkippedReason.swift | 4 +- .../Presentation/PresentationItems.swift | 23 +++--- Sources/SuperwallKit/Superwall.swift | 2 +- 8 files changed, 135 insertions(+), 19 deletions(-) rename Sources/SuperwallKit/Paywall/Presentation/{Get Paywall => Get PaywallViewController}/InternalGetPaywallViewController.swift (91%) rename Sources/SuperwallKit/Paywall/Presentation/{Get Paywall => Get PaywallViewController}/Operators/CheckSubscriptionStatusOperator.swift (100%) rename Sources/SuperwallKit/Paywall/Presentation/{Get Paywall => Get PaywallViewController}/Operators/ExtractPaywallViewControllerOperator.swift (100%) create mode 100644 Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/Operators/MapErrorsOperator.swift rename Sources/SuperwallKit/Paywall/Presentation/{Get Paywall => Get PaywallViewController}/PublicGetPaywallViewController.swift (58%) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywallViewController.swift b/Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/InternalGetPaywallViewController.swift similarity index 91% rename from Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywallViewController.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/InternalGetPaywallViewController.swift index 59c6f2d98..25a9abbda 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/InternalGetPaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/InternalGetPaywallViewController.swift @@ -16,8 +16,9 @@ extension Superwall { /// /// - Returns: A ``PaywallViewController`` to present. @discardableResult - func internallyGetPaywallViewController( - _ request: PresentationRequest + func getPaywallViewController( + _ request: PresentationRequest, + isObjc: Bool ) -> AnyPublisher { let paywallStatePublisher: PassthroughSubject = .init() let presentationSubject = PresentationSubject(request) @@ -35,5 +36,6 @@ extension Superwall { .storePresentationObjects(presentationSubject, paywallStatePublisher) .logErrors(from: request) .extractPaywallViewController(paywallStatePublisher) + .mapErrors(isObjc: isObjc) } } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/CheckSubscriptionStatusOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/Operators/CheckSubscriptionStatusOperator.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/CheckSubscriptionStatusOperator.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/Operators/CheckSubscriptionStatusOperator.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/ExtractPaywallViewControllerOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/Operators/ExtractPaywallViewControllerOperator.swift similarity index 100% rename from Sources/SuperwallKit/Paywall/Presentation/Get Paywall/Operators/ExtractPaywallViewControllerOperator.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/Operators/ExtractPaywallViewControllerOperator.swift diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/Operators/MapErrorsOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/Operators/MapErrorsOperator.swift new file mode 100644 index 000000000..ba5d9bdec --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/Operators/MapErrorsOperator.swift @@ -0,0 +1,48 @@ +// +// File.swift +// +// +// Created by Yusuf Tör on 04/05/2023. +// + +import Foundation +import Combine + +extension AnyPublisher where Output == PaywallViewController, Failure == Error { + func mapErrors(isObjc: Bool) -> AnyPublisher { + mapError { error -> Error in + if let error = error as? PresentationPipelineError { + switch error { + case .holdout(let experiment): + if isObjc { + return PaywallSkippedReasonObjc.holdout + } else { + return PaywallSkippedReason.holdout(experiment) + } + case .noRuleMatch: + if isObjc { + return PaywallSkippedReasonObjc.noRuleMatch + } else { + return PaywallSkippedReason.noRuleMatch + } + case .eventNotFound: + if isObjc { + return PaywallSkippedReasonObjc.eventNotFound + } else { + return PaywallSkippedReason.eventNotFound + } + case .userIsSubscribed: + if isObjc { + return PaywallSkippedReasonObjc.userIsSubscribed + } else { + return PaywallSkippedReason.userIsSubscribed + } + default: + return error + } + } + return error + } + .eraseToAnyPublisher() + } +} diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift b/Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/PublicGetPaywallViewController.swift similarity index 58% rename from Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift rename to Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/PublicGetPaywallViewController.swift index d1eb09dbf..f9f99b085 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/PublicGetPaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get PaywallViewController/PublicGetPaywallViewController.swift @@ -34,7 +34,7 @@ extension Superwall { /// - paywallOverrides: An optional ``PaywallOverrides`` object whose parameters override the paywall defaults. Use this to override products, presentation style, and whether it ignores the subscription status. Defaults to `nil`. /// - completion: A completion block that contains a `Result` type, containing either a success case with a /// ``PaywallViewController`` object, or a failure case with an `Error`. - public func getPaywallViewController( + @nonobjc public func getPaywallViewController( forEvent event: String, params: [String: Any]? = nil, paywallOverrides: PaywallOverrides? = nil, @@ -78,12 +78,75 @@ extension Superwall { /// - paywallOverrides: An optional ``PaywallOverrides`` object whose parameters override the paywall defaults. Use this to override products, presentation style, and whether it ignores the subscription status. Defaults to `nil`. /// /// - Returns A ``PaywallViewController`` object. - /// - Throws: An `Error` explaining why it couldn't get the view controller. + /// - Throws: An `Error` explaining why it couldn't get the view controller. If the ``PaywallViewController`` couldn't be retrieved + /// because its presentation should be skipped, catch an error of type``PaywallSkippedReason`` and switch over its cases to find out + /// more info. All other errors will be returned in the general catch block. @MainActor - public func getPaywallViewController( + @nonobjc public func getPaywallViewController( forEvent event: String, params: [String: Any]? = nil, paywallOverrides: PaywallOverrides? = nil + ) async throws -> PaywallViewController { + return try await internallyGetPaywallViewController( + forEvent: event, + params: params, + paywallOverrides: paywallOverrides, + isObjc: false + ) + } + + /// Objective-C-only method that gets the ``PaywallViewController`` object, which you can present + /// however you want. + /// + /// To use this you **must** follow the following steps: + /// + /// 1. Call this function to retrieve the ``PaywallViewController``. + /// 2. Call ``PaywallViewController/presentationWillBegin()`` when + /// you're about to present the view controller. + /// 3. Present the view controller. + /// 4. Call ``PaywallViewController/presentationDidFinish()`` after presentation + /// completes. + /// + /// - Note: The remotely configured presentation style will be ignored, it is up to you + /// to set it programmatically. + /// + /// - Parameters: + /// - event: The name of the event, as you have defined on the Superwall dashboard. + /// - params: Optional parameters you'd like to pass with your event. These can be referenced within the rules + /// of your campaign. Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any + /// JSON encodable value, URLs or Dates. Arrays and dictionaries as values are not supported at this time, and will + /// be dropped. + /// - paywallOverrides: An optional ``PaywallOverrides`` object whose parameters override the paywall defaults. Use this to override products, presentation style, and whether it ignores the subscription status. Defaults to `nil`. + /// - completion: A completion block accepting an optional ``PaywallViewController`` and an optional `Error`. If the + /// ``PaywallViewController`` couldn't be retrieved because its presentation should be skipped, the error will be of type + /// ``PaywallSkippedReasonObjc``, whose cases you can switch over for more info. Otherwise, it'll be a generic `Error`. + @available(swift, obsoleted: 1.0) + public func getPaywallViewController( + forEvent event: String, + params: [String: Any]? = nil, + paywallOverrides: PaywallOverrides? = nil, + completion: @escaping (PaywallViewController?, Error?) -> Void + ) { + Task { @MainActor in + do { + let paywallViewController = try await internallyGetPaywallViewController( + forEvent: event, + params: params, + paywallOverrides: paywallOverrides, + isObjc: true + ) + completion(paywallViewController, nil) + } catch { + completion(paywallViewController, error) + } + } + } + + private func internallyGetPaywallViewController( + forEvent event: String, + params: [String: Any]?, + paywallOverrides: PaywallOverrides?, + isObjc: Bool ) async throws -> PaywallViewController { return try await Future { let trackableEvent = UserInitiatedEvent.Track( @@ -102,7 +165,7 @@ extension Superwall { isPaywallPresented: false, type: .getPaywallViewController ) - return self.internallyGetPaywallViewController(presentationRequest) + return self.getPaywallViewController(presentationRequest, isObjc: isObjc) } .eraseToAnyPublisher() .throwableAsync() diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift index b27d61d2e..8d4a5e89c 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Presentation State/PaywallSkippedReason.swift @@ -8,7 +8,7 @@ import Foundation /// The reason the paywall presentation was skipped. -public enum PaywallSkippedReason: Sendable, Equatable { +public enum PaywallSkippedReason: Error, Sendable, Equatable { /// The user was assigned to a holdout. /// /// A holdout is a control group which you can analyse against @@ -52,7 +52,7 @@ public enum PaywallSkippedReason: Sendable, Equatable { /// Objective-C-only enum. Specifies the reason the paywall presentation was skipped. @objc(SWKPaywallSkippedReason) -public enum PaywallSkippedReasonObjc: Int, Sendable, Equatable { +public enum PaywallSkippedReasonObjc: Int, Error, Sendable, Equatable { /// The user was assigned to a holdout group. case holdout diff --git a/Sources/SuperwallKit/Paywall/Presentation/PresentationItems.swift b/Sources/SuperwallKit/Paywall/Presentation/PresentationItems.swift index 79bb6b047..65f0a3141 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PresentationItems.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PresentationItems.swift @@ -12,13 +12,16 @@ final class PresentationItems: @unchecked Sendable { /// The publisher and request involved in the last successful paywall presentation request. var last: LastPresentationItems? { get { - queue.sync { [unowned self] in + queue.sync { [weak self] in + guard let self = self else { + return nil + } return self._last } } set { - queue.async { [unowned self] in - self._last = newValue + queue.async { [weak self] in + self?._last = newValue } } } @@ -27,13 +30,13 @@ final class PresentationItems: @unchecked Sendable { /// The ``PaywallInfo`` object stored from the last paywall view controller that was dismissed. var paywallInfo: PaywallInfo? { get { - queue.sync { [unowned self] in - return self._paywallInfo + queue.sync { [weak self] in + return self?._paywallInfo } } set { - queue.async { [unowned self] in - self._paywallInfo = newValue + queue.async { [weak self] in + self?._paywallInfo = newValue } } } @@ -46,9 +49,9 @@ final class PresentationItems: @unchecked Sendable { private let queue = DispatchQueue(label: "com.superwall.presentationitems") func reset() { - queue.async { [unowned self] in - self._last = nil - self._paywallInfo = nil + queue.async { [weak self] in + self?._last = nil + self?._paywallInfo = nil } Task { @MainActor [weak self] in diff --git a/Sources/SuperwallKit/Superwall.swift b/Sources/SuperwallKit/Superwall.swift index 49ebc923c..11d520a90 100644 --- a/Sources/SuperwallKit/Superwall.swift +++ b/Sources/SuperwallKit/Superwall.swift @@ -161,7 +161,7 @@ public final class Superwall: NSObject, ObservableObject { } /// Items involved in the presentation of paywalls. - var presentationItems = PresentationItems() + let presentationItems = PresentationItems() /// Determines whether a paywall is being presented. var isPaywallPresented: Bool { From bfba47b00ab601b77481f82339cb4e389b1d68f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= Date: Thu, 4 May 2023 18:24:26 +0100 Subject: [PATCH 27/27] Remove rogue comment --- .../Operators/CheckPaywallPresentableOperator.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift index 1ad73dc01..a57c27116 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperator.swift @@ -107,8 +107,6 @@ extension Superwall { } presentingWindow?.rootViewController = UIViewController() -// presentingWindow?.makeKeyAndVisible() - presentationItems.window = presentingWindow }