Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions Sources/SuperwallKit/Debug/DebugManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ final class DebugManager {
struct DeepLinkOutcome {
let debugKey: String
let paywallId: String?
let overrides: DebugPaywallOverrides
}

init(
Expand All @@ -33,7 +34,10 @@ final class DebugManager {
}
storage.debugKey = outcome.debugKey
Task {
await self.launchDebugger(withPaywallId: outcome.paywallId)
await self.launchDebugger(
withPaywallId: outcome.paywallId,
overrides: outcome.overrides
)
}
return true
}
Expand All @@ -58,7 +62,11 @@ final class DebugManager {
fromUrl: url,
withName: .paywallId
)
return .init(debugKey: debugKey, paywallId: paywallId)
return .init(
debugKey: debugKey,
paywallId: paywallId,
overrides: DebugPaywallOverrides(url: url)
)
}

/// Launches the debugger for you to preview paywalls.
Expand All @@ -68,38 +76,50 @@ final class DebugManager {
///
/// Remember to add your URL scheme in settings for QR code scanning to work.
@MainActor
func launchDebugger(withPaywallId paywallDatabaseId: String? = nil) async {
func launchDebugger(
withPaywallId paywallDatabaseId: String? = nil,
overrides: DebugPaywallOverrides = DebugPaywallOverrides()
) async {
if Superwall.shared.isPaywallPresented {
await Superwall.shared.dismiss()
await launchDebugger(withPaywallId: paywallDatabaseId)
await launchDebugger(withPaywallId: paywallDatabaseId, overrides: overrides)
} else {
if viewController == nil {
let milliseconds = 200
let nanoseconds = UInt64(milliseconds * 1_000_000)
try? await Task.sleep(nanoseconds: nanoseconds)
await presentDebugger(withPaywallId: paywallDatabaseId)
await presentDebugger(withPaywallId: paywallDatabaseId, overrides: overrides)
} else {
await closeDebugger(animated: true)
await launchDebugger(withPaywallId: paywallDatabaseId)
await launchDebugger(withPaywallId: paywallDatabaseId, overrides: overrides)
}
}
}

@MainActor
func presentDebugger(withPaywallId paywallDatabaseId: String? = nil) async {
func presentDebugger(
withPaywallId paywallDatabaseId: String? = nil,
overrides: DebugPaywallOverrides = DebugPaywallOverrides()
) async {
isDebuggerLaunched = true
if let viewController = viewController {
if viewController.isBeingPresented {
return
}
viewController.paywallDatabaseId = paywallDatabaseId
await viewController.loadPreview()
viewController.overrides = overrides
Comment thread
pullfrog[bot] marked this conversation as resolved.
// On reuse, `viewDidLoad` won't run again, so apply locale/appearance here.
viewController.applyOverrides()
await viewController.startPreviewLoad().value
await UIViewController.topMostViewController?.present(
viewController,
animated: true
)
} else {
let viewController = factory.makeDebugViewController(withDatabaseId: paywallDatabaseId)
let viewController = factory.makeDebugViewController(
withDatabaseId: paywallDatabaseId,
overrides: overrides
)
UIViewController.topMostViewController?.present(
viewController,
animated: true,
Expand Down
81 changes: 81 additions & 0 deletions Sources/SuperwallKit/Debug/DebugPaywallOverrides.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// DebugPaywallOverrides.swift
// SuperwallKit
//
// Created by Konrad Roj on 04/08/2026.
//

import Foundation

struct DebugPaywallOverrides: Equatable {
enum Appearance: String {
case light
case dark
case system

var interfaceStyle: InterfaceStyle? {
switch self {
case .light:
return .light
case .dark:
return .dark
case .system:
return nil
}
}
}

var freeTrialOverride: Bool?
var appearance: Appearance?
var localeIdentifier: String?
var shouldPresent: Bool

var isEmpty: Bool {
freeTrialOverride == nil
&& appearance == nil
&& localeIdentifier == nil
&& !shouldPresent
}

init(
freeTrialOverride: Bool? = nil,
appearance: Appearance? = nil,
localeIdentifier: String? = nil,
shouldPresent: Bool = false
) {
self.freeTrialOverride = freeTrialOverride
self.appearance = appearance
self.localeIdentifier = localeIdentifier
self.shouldPresent = shouldPresent
}

init(url: URL) {
switch SWDebugManagerLogic.getQueryItemValue(fromUrl: url, withName: .trialState)?.lowercased() {
case "eligible":
freeTrialOverride = true
case "ineligible":
freeTrialOverride = false
default:
freeTrialOverride = nil
}

if let value = SWDebugManagerLogic.getQueryItemValue(fromUrl: url, withName: .appearance)?.lowercased() {
appearance = Appearance(rawValue: value)
} else {
appearance = nil
}

if let value = SWDebugManagerLogic.getQueryItemValue(fromUrl: url, withName: .locale),
!value.isEmpty {
localeIdentifier = value
} else {
localeIdentifier = nil
}

if let value = SWDebugManagerLogic.getQueryItemValue(fromUrl: url, withName: .present)?.lowercased() {
shouldPresent = ["true", "1", "yes"].contains(value)
} else {
shouldPresent = false
}
}
}
62 changes: 58 additions & 4 deletions Sources/SuperwallKit/Debug/DebugViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,12 @@ final class DebugViewController: UIViewController {
/// has a single paywall, in which case the picker declines to open.
var previewPaywalls: [PaywallSummary] = []
var previewViewContent: UIView?
var overrides = DebugPaywallOverrides()
private var cancellable: AnyCancellable?
private var initialLocaleIdentifier: String?
private var initialInterfaceStyleOverride: InterfaceStyle?
private var didAppear = false
private var previewTask: Task<Void, Never>?

private unowned let storeKitManager: StoreKitManager
private unowned let network: Network
Expand Down Expand Up @@ -158,11 +162,49 @@ final class DebugViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
initialLocaleIdentifier = Superwall.shared.options.localeIdentifier
initialInterfaceStyleOverride = Superwall.shared.dependencyContainer.deviceHelper.interfaceStyleOverride
applyOverrides()
addSubviews()
Task { await loadPreview() }
startPreviewLoad()
Task { await loadPreviewPaywalls() }
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
didAppear = true
presentAutomaticallyIfNeeded()
}

/// Every preview load runs through here so that `viewDidDisappear` can cancel
/// whichever one is in flight, not just the initial `viewDidLoad` load.
@discardableResult
func startPreviewLoad() -> Task<Void, Never> {
previewTask?.cancel()
let task = Task { await loadPreview() }
previewTask = task
return task
}
Comment thread
pullfrog[bot] marked this conversation as resolved.

func applyOverrides() {
if let localeIdentifier = overrides.localeIdentifier {
Superwall.shared.options.localeIdentifier = localeIdentifier
}
if let appearance = overrides.appearance {
Superwall.shared.setInterfaceStyle(to: appearance.interfaceStyle)
}
}

private func presentAutomaticallyIfNeeded() {
guard didAppear,
viewIfLoaded?.window != nil,
overrides.shouldPresent,
paywall != nil else {
return
}
overrides.shouldPresent = false
loadAndShowPaywall(introOfferAvailable: overrides.freeTrialOverride ?? (paywall?.isFreeTrialAvailable ?? false))
}

private func addSubviews() {
view.addSubview(previewContainerView)
view.addSubview(activityIndicator)
Expand Down Expand Up @@ -241,7 +283,7 @@ final class DebugViewController: UIViewController {
let request = factory.makePaywallRequest(
placementData: nil,
responseIdentifiers: .init(paywallId: paywallId),
overrides: nil,
overrides: overrides.freeTrialOverride.map { PaywallRequest.Overrides(isFreeTrial: $0) },
isDebuggerLaunched: true,
presentationSourceType: nil
)
Expand All @@ -250,10 +292,17 @@ final class DebugViewController: UIViewController {
let productVariables = await storeKitManager.getProductVariables(for: paywall)
paywall.productVariables = productVariables

// Debugger dismissed mid-load (previewTask cancelled): skip rendering/presenting.
guard !Task.isCancelled else {
return
}

self.paywall = paywall
self.previewPickerButton.setTitle("\(paywall.name)", for: .normal)
self.activityIndicator.stopAnimating()
self.addPaywallPreview()

presentAutomaticallyIfNeeded()
} catch {
Logger.debug(
logLevel: .error,
Expand Down Expand Up @@ -357,7 +406,7 @@ final class DebugViewController: UIViewController {
action: { [weak self] in
self?.paywallDatabaseId = paywall.id
self?.paywallIdentifier = paywall.identifier
Task { await self?.loadPreview() }
self?.startPreviewLoad()
},
style: .default
)
Expand Down Expand Up @@ -401,7 +450,7 @@ final class DebugViewController: UIViewController {
func showLocalizationPicker() async {
let viewController = SWLocalizationViewController { [weak self] identifier in
Superwall.shared.options.localeIdentifier = identifier
Task { await self?.loadPreview() }
self?.startPreviewLoad()
}

let navController = UINavigationController(rootViewController: viewController)
Expand Down Expand Up @@ -551,9 +600,14 @@ final class DebugViewController: UIViewController {

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
didAppear = false
previewTask?.cancel()
Comment thread
pullfrog[bot] marked this conversation as resolved.
paywallManager.resetCache()
debugManager.isDebuggerLaunched = false
Superwall.shared.options.localeIdentifier = initialLocaleIdentifier
if overrides.appearance != nil {
Superwall.shared.setInterfaceStyle(to: initialInterfaceStyleOverride)
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions Sources/SuperwallKit/Debug/SWDebugManagerLogic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ enum SWDebugManagerLogic {
case token
case paywallId = "paywall_id"
case superwallDebug = "superwall_debug"
case trialState = "trial_state"
case appearance
case locale
case present
}

static func getQueryItemValue(
Expand Down
6 changes: 5 additions & 1 deletion Sources/SuperwallKit/Dependencies/DependencyContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,10 @@ extension DependencyContainer: ViewControllerFactory {
}

@MainActor
func makeDebugViewController(withDatabaseId id: String?) -> DebugViewController {
func makeDebugViewController(
withDatabaseId id: String?,
overrides: DebugPaywallOverrides
) -> DebugViewController {
let viewController = DebugViewController(
storeKitManager: storeKitManager,
network: network,
Expand All @@ -344,6 +347,7 @@ extension DependencyContainer: ViewControllerFactory {
factory: self
)
viewController.paywallDatabaseId = id
viewController.overrides = overrides
viewController.modalPresentationStyle = .overFullScreen
return viewController
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/SuperwallKit/Dependencies/FactoryProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ protocol ViewControllerFactory: AnyObject {
) -> PaywallViewController

@MainActor
func makeDebugViewController(withDatabaseId id: String?) -> DebugViewController
func makeDebugViewController(
withDatabaseId id: String?,
overrides: DebugPaywallOverrides
) -> DebugViewController
}

protocol CacheFactory: AnyObject {
Expand Down
Loading