Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
34 changes: 26 additions & 8 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,48 @@ 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
viewController.overrides = overrides
Comment thread
pullfrog[bot] marked this conversation as resolved.
await viewController.loadPreview()
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
}
}
}
46 changes: 44 additions & 2 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,38 @@ 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() }
previewTask = Task { await loadPreview() }
Task { await loadPreviewPaywalls() }
}

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

private 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,
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 +272,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 +281,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 @@ -551,9 +589,13 @@ final class DebugViewController: UIViewController {

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
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
60 changes: 60 additions & 0 deletions Tests/SuperwallKitTests/Debug/DebugManagerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// DebugManagerTests.swift
// SuperwallKit
//
// Created by Konrad Roj on 04/08/2026.
//
// swiftlint:disable all

import Foundation
import Testing
@testable import SuperwallKit

struct DebugManagerTests {
@Test func outcomeForDeepLink_notADebugLink() {
let url = URL(string: "myapp://?paywall_id=123")!

let outcome = DebugManager.outcomeForDeepLink(url: url)

#expect(outcome == nil)
}

@Test func outcomeForDeepLink_missingToken() {
let url = URL(string: "myapp://?superwall_debug=true&paywall_id=123")!

let outcome = DebugManager.outcomeForDeepLink(url: url)

#expect(outcome == nil)
}

@Test func outcomeForDeepLink_requiresDebugFlag() {
let url = URL(string: "myapp://?superwall_debug=false&token=abc")!

let outcome = DebugManager.outcomeForDeepLink(url: url)

#expect(outcome == nil)
}

@Test func outcomeForDeepLink_minimalValidLink() {
let url = URL(string: "myapp://?superwall_debug=true&token=abc")!

let outcome = DebugManager.outcomeForDeepLink(url: url)

#expect(outcome?.debugKey == "abc")
#expect(outcome?.paywallId == nil)
#expect(outcome?.overrides.isEmpty == true)
}

@Test func outcomeForDeepLink_carriesOverrides() {
let url = URL(string: "myapp://?superwall_debug=true&token=abc&paywall_id=123&trial_state=ineligible&appearance=dark&locale=de&present=true")!

let outcome = DebugManager.outcomeForDeepLink(url: url)

#expect(outcome?.debugKey == "abc")
#expect(outcome?.paywallId == "123")
#expect(outcome?.overrides.freeTrialOverride == false)
#expect(outcome?.overrides.appearance == .dark)
#expect(outcome?.overrides.localeIdentifier == "de")
#expect(outcome?.overrides.shouldPresent == true)
}
}
Loading