diff --git a/Sources/SuperwallKit/Debug/DebugManager.swift b/Sources/SuperwallKit/Debug/DebugManager.swift index bb2ebe140f..f793c4d1df 100644 --- a/Sources/SuperwallKit/Debug/DebugManager.swift +++ b/Sources/SuperwallKit/Debug/DebugManager.swift @@ -17,6 +17,7 @@ final class DebugManager { struct DeepLinkOutcome { let debugKey: String let paywallId: String? + let overrides: DebugPaywallOverrides } init( @@ -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 } @@ -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. @@ -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 + // 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, diff --git a/Sources/SuperwallKit/Debug/DebugPaywallOverrides.swift b/Sources/SuperwallKit/Debug/DebugPaywallOverrides.swift new file mode 100644 index 0000000000..48cdd985ad --- /dev/null +++ b/Sources/SuperwallKit/Debug/DebugPaywallOverrides.swift @@ -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 + } + } +} diff --git a/Sources/SuperwallKit/Debug/DebugViewController.swift b/Sources/SuperwallKit/Debug/DebugViewController.swift index e02936f7a0..4ab2bfe408 100644 --- a/Sources/SuperwallKit/Debug/DebugViewController.swift +++ b/Sources/SuperwallKit/Debug/DebugViewController.swift @@ -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? private unowned let storeKitManager: StoreKitManager private unowned let network: Network @@ -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 { + previewTask?.cancel() + let task = Task { await loadPreview() } + previewTask = task + return task + } + + 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) @@ -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 ) @@ -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, @@ -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 ) @@ -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) @@ -551,9 +600,14 @@ final class DebugViewController: UIViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) + didAppear = false + previewTask?.cancel() paywallManager.resetCache() debugManager.isDebuggerLaunched = false Superwall.shared.options.localeIdentifier = initialLocaleIdentifier + if overrides.appearance != nil { + Superwall.shared.setInterfaceStyle(to: initialInterfaceStyleOverride) + } } } diff --git a/Sources/SuperwallKit/Debug/SWDebugManagerLogic.swift b/Sources/SuperwallKit/Debug/SWDebugManagerLogic.swift index dfaac873f2..8cfbe8d182 100644 --- a/Sources/SuperwallKit/Debug/SWDebugManagerLogic.swift +++ b/Sources/SuperwallKit/Debug/SWDebugManagerLogic.swift @@ -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( diff --git a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift index ba96227197..a43d470b93 100644 --- a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift +++ b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift @@ -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, @@ -344,6 +347,7 @@ extension DependencyContainer: ViewControllerFactory { factory: self ) viewController.paywallDatabaseId = id + viewController.overrides = overrides viewController.modalPresentationStyle = .overFullScreen return viewController } diff --git a/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift b/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift index e7748fcb6f..dc277f638e 100644 --- a/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift +++ b/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift @@ -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 { diff --git a/SuperwallKit.xcodeproj/project.pbxproj b/SuperwallKit.xcodeproj/project.pbxproj index d395f3ccee..a52ece4ebf 100644 --- a/SuperwallKit.xcodeproj/project.pbxproj +++ b/SuperwallKit.xcodeproj/project.pbxproj @@ -256,6 +256,7 @@ 76CC2455A437DC1DB0F3424E /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC3DA8E774FBFE31F811FAF /* ConfigManager.swift */; }; 774699D4CCD6952F124FE333 /* PermissionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD75014714392592D708077C /* PermissionType.swift */; }; 776B122691BB67A703BB0DDD /* Survey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D44CEC91693B4B900472C1C /* Survey.swift */; }; + 77B4F3A29F407B531C43E115 /* DebugPaywallOverridesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E6EF12D52F982114994627 /* DebugPaywallOverridesTests.swift */; }; 77ED3D92C176CC6D93D625B2 /* ExpressionLogicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8C73737C103C168647DCFB /* ExpressionLogicTests.swift */; }; 77EDD2927FF8DCF95579BE3E /* IdentityLogicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40AE19B5A9B237A2552D5F36 /* IdentityLogicTests.swift */; }; 77FF632568317C7745451D67 /* ConfirmPaywallAssignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F7B4D9230BF922AE19A830 /* ConfirmPaywallAssignment.swift */; }; @@ -478,6 +479,7 @@ D66461863D54A56BE9C29310 /* Publisher+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5A8FFFC23826AD113D525A /* Publisher+Async.swift */; }; D6CA719D79BAA6369D1C01C3 /* AudienceLogic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CF4BE5D2A6CEA9F8993C81 /* AudienceLogic.swift */; }; D77DB3187C62B91E3D55DF80 /* ArchiveRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4300EBF7463A42D2FB89371 /* ArchiveRequest.swift */; }; + D7CB4F11ADBDC9765290798E /* DebugPaywallOverrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A49F6B1241A7BFD64964946 /* DebugPaywallOverrides.swift */; }; D7F5A91A1E37E6BFB84E5609 /* StoreProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10310294FD27EB6A0621341 /* StoreProduct.swift */; }; D89E9C69317044050B97B573 /* IdentityManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2714D9FD9F55611B4C5E4E7D /* IdentityManagerMock.swift */; }; D89F615A7826947C9246F6B1 /* WebArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = E72593E1D4123B176EC83499 /* WebArchive.swift */; }; @@ -526,6 +528,7 @@ E9F892ABB9BDA85F4794E3CF /* SubscriptionStatusResolutionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C15CF29C17FE1EE3BFDEDC /* SubscriptionStatusResolutionTests.swift */; }; EA50607230AA07B509E90E10 /* TestStoreUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7162E1E791297A3BF80B65A4 /* TestStoreUser.swift */; }; EA66951B1DF341C4F0448C9F /* PlacementsQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682AB10207309C439F64BC69 /* PlacementsQueueTests.swift */; }; + EAA7E12CECD1429B1BCE7EB5 /* DebugManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80A78796667486692804BB6 /* DebugManagerTests.swift */; }; EB1964816A8297CE133F96BF /* PurchaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E522F5BCABB3A95B97549E /* PurchaseError.swift */; }; EB6540A8E1ECC3548C5E6368 /* PaywallMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC653A44D9B40812BDDD94E7 /* PaywallMessage.swift */; }; ECA7E9C9898CAB24B56E7054 /* SK2PriceFormatRoundingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC580BF1CC720ECBC4E68A28 /* SK2PriceFormatRoundingTests.swift */; }; @@ -685,6 +688,7 @@ 26B5B7A4C7137EB233CC1262 /* InternalPurchaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalPurchaseController.swift; sourceTree = ""; }; 2714D9FD9F55611B4C5E4E7D /* IdentityManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityManagerMock.swift; sourceTree = ""; }; 27E41300E7467F017BCD5E5C /* Capabilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capabilities.swift; sourceTree = ""; }; + 27E6EF12D52F982114994627 /* DebugPaywallOverridesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPaywallOverridesTests.swift; sourceTree = ""; }; 2845A6B61C43D18F869D750E /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; 2888B05A273E9EB6E9382332 /* UIColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Hex.swift"; sourceTree = ""; }; 28A1C8B0D79B8AC70ECF0CBB /* ConfigState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigState.swift; sourceTree = ""; }; @@ -823,6 +827,7 @@ 68363EE16B2DE5C1C5361657 /* Enrichment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Enrichment.swift; sourceTree = ""; }; 6944763A0D07AFA102B023C5 /* PaywallManagerLogicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallManagerLogicTests.swift; sourceTree = ""; }; 69A4D77D819DDB696834E1B7 /* UIViewController+AsyncPresent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+AsyncPresent.swift"; sourceTree = ""; }; + 6A49F6B1241A7BFD64964946 /* DebugPaywallOverrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPaywallOverrides.swift; sourceTree = ""; }; 6A56D712042043783D7CA142 /* ProductPurchaserSK1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductPurchaserSK1.swift; sourceTree = ""; }; 6B103FA8F9AE387E7DB4B471 /* LocationPermissionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionDelegate.swift; sourceTree = ""; }; 6B7CFAF4B3E32AE628A249C8 /* AttributionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionTests.swift; sourceTree = ""; }; @@ -1181,6 +1186,7 @@ F67A5C0CA15AF645709A2545 /* PurchaseSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseSource.swift; sourceTree = ""; }; F6EED7C7E264C38A1A7C3EFB /* StorePayment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePayment.swift; sourceTree = ""; }; F798D9662212AD1CC75666F3 /* PaywallMessageHandlerDelegateMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallMessageHandlerDelegateMock.swift; sourceTree = ""; }; + F80A78796667486692804BB6 /* DebugManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugManagerTests.swift; sourceTree = ""; }; F85ED994DEC92BB90ACC6AC2 /* TrackingResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingResult.swift; sourceTree = ""; }; F9098101E599AEB01521FE89 /* DebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewController.swift; sourceTree = ""; }; F96ABDCA3686C7F1CAF41B03 /* en_GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en_GB; path = en_GB.lproj/Localizable.strings; sourceTree = ""; }; @@ -1714,6 +1720,8 @@ 38C02C19ED9C9958A7A61FB1 /* Debug */ = { isa = PBXGroup; children = ( + F80A78796667486692804BB6 /* DebugManagerTests.swift */, + 27E6EF12D52F982114994627 /* DebugPaywallOverridesTests.swift */, C733A9BE56EA9E10D75B073B /* SWDebugManagerLogicTests.swift */, ); path = Debug; @@ -2427,6 +2435,7 @@ isa = PBXGroup; children = ( 5383FA48A6E9EF8F30683C9B /* DebugManager.swift */, + 6A49F6B1241A7BFD64964946 /* DebugPaywallOverrides.swift */, F9098101E599AEB01521FE89 /* DebugViewController.swift */, 299F91895EE88281B5ED8320 /* SWBounceButton.swift */, 3CDFBF0FA8B313E0D84A51DB /* SWConsoleViewController.swift */, @@ -3268,6 +3277,8 @@ 37FDB46DD55E649FA10D753C /* CustomerInfoDecodingTests.swift in Sources */, 654803E77F7CDBF6282D0110 /* Date+IsWithinAnHourBeforeTests.swift in Sources */, D91750797BB4947F6975B2B9 /* Date+IsoStringTests.swift in Sources */, + EAA7E12CECD1429B1BCE7EB5 /* DebugManagerTests.swift in Sources */, + 77B4F3A29F407B531C43E115 /* DebugPaywallOverridesTests.swift in Sources */, 01BE837B492223B76A95CB5D /* DeepLinkRouterTests.swift in Sources */, 0CA13E721ADB243882536D4A /* DeviceHelperMock.swift in Sources */, 9DBDDD10A1EFC7CD3575D9E5 /* DeviceHelperTests.swift in Sources */, @@ -3450,6 +3461,7 @@ B4C9FECB4894151E42A34F85 /* Date+TimeIntervalMilliseconds.swift in Sources */, F326AADBFE0083F8F18E81CE /* Date+WithinAnHourBefore.swift in Sources */, 432FB2AE172725B4ED208416 /* DebugManager.swift in Sources */, + D7CB4F11ADBDC9765290798E /* DebugPaywallOverrides.swift in Sources */, 11FF1373BAAE0B019CE6AE21 /* DebugViewController.swift in Sources */, B5AE7BCDCB6D49FC8493D55B /* DecodingError+Extensions.swift in Sources */, CB2F2B4DA3709F171E54CBB8 /* DeepLinkRouter.swift in Sources */, diff --git a/Tests/SuperwallKitTests/Debug/DebugManagerTests.swift b/Tests/SuperwallKitTests/Debug/DebugManagerTests.swift new file mode 100644 index 0000000000..7b48463be5 --- /dev/null +++ b/Tests/SuperwallKitTests/Debug/DebugManagerTests.swift @@ -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) + } +} diff --git a/Tests/SuperwallKitTests/Debug/DebugPaywallOverridesTests.swift b/Tests/SuperwallKitTests/Debug/DebugPaywallOverridesTests.swift new file mode 100644 index 0000000000..0df829b40d --- /dev/null +++ b/Tests/SuperwallKitTests/Debug/DebugPaywallOverridesTests.swift @@ -0,0 +1,155 @@ +// +// DebugPaywallOverridesTests.swift +// SuperwallKit +// +// Created by Konrad Roj on 04/08/2026. +// +// swiftlint:disable all + +import Foundation +import Testing +@testable import SuperwallKit + +struct DebugPaywallOverridesTests { + @Test func parse_noOverrides_isEmpty() { + let url = URL(string: "myapp://?superwall_debug=true&token=abc&paywall_id=123")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.isEmpty) + #expect(overrides.freeTrialOverride == nil) + #expect(overrides.appearance == nil) + #expect(overrides.localeIdentifier == nil) + #expect(overrides.shouldPresent == false) + } + + @Test func parse_trialState_eligible() { + let url = URL(string: "myapp://?trial_state=eligible")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.freeTrialOverride == true) + } + + @Test func parse_trialState_ineligible() { + let url = URL(string: "myapp://?trial_state=ineligible")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.freeTrialOverride == false) + } + + @Test func parse_trialState_caseInsensitive() { + let url = URL(string: "myapp://?trial_state=ELIGIBLE")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.freeTrialOverride == true) + } + + @Test func parse_trialState_unknownIsIgnored() { + let url = URL(string: "myapp://?trial_state=maybe")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.freeTrialOverride == nil) + } + + @Test func parse_appearance_light() { + let url = URL(string: "myapp://?appearance=light")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.appearance == .light) + #expect(overrides.appearance?.interfaceStyle == .light) + } + + @Test func parse_appearance_dark() { + let url = URL(string: "myapp://?appearance=dark")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.appearance == .dark) + #expect(overrides.appearance?.interfaceStyle == .dark) + } + + @Test func parse_appearance_systemClearsInterfaceStyle() { + let url = URL(string: "myapp://?appearance=system")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.appearance == .system) + #expect(overrides.appearance?.interfaceStyle == nil) + #expect(overrides.isEmpty == false) + } + + @Test func parse_appearance_unknownIsIgnored() { + let url = URL(string: "myapp://?appearance=sepia")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.appearance == nil) + } + + @Test func parse_locale() { + let url = URL(string: "myapp://?locale=de")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.localeIdentifier == "de") + } + + @Test func parse_locale_emptyIsIgnored() { + let url = URL(string: "myapp://?locale=")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.localeIdentifier == nil) + } + + @Test func parse_present_true() { + let url = URL(string: "myapp://?present=true")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.shouldPresent == true) + } + + @Test func parse_present_false() { + let url = URL(string: "myapp://?present=false")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.shouldPresent == false) + } + + @Test(arguments: ["true", "TRUE", "1", "yes", "YES"]) + func parse_present_truthyValues(value: String) { + let url = URL(string: "myapp://?present=\(value)")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.shouldPresent == true) + } + + @Test(arguments: ["false", "0", "no", "nope", ""]) + func parse_present_falsyValues(value: String) { + let url = URL(string: "myapp://?present=\(value)")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.shouldPresent == false) + } + + @Test func parse_combined() { + let url = URL(string: "myapp://?superwall_debug=true&token=abc&paywall_id=123&trial_state=ineligible&appearance=dark&locale=de&present=true")! + + let overrides = DebugPaywallOverrides(url: url) + + #expect(overrides.freeTrialOverride == false) + #expect(overrides.appearance == .dark) + #expect(overrides.localeIdentifier == "de") + #expect(overrides.shouldPresent == true) + #expect(overrides.isEmpty == false) + } +} diff --git a/Tests/SuperwallKitTests/Debug/SWDebugManagerLogicTests.swift b/Tests/SuperwallKitTests/Debug/SWDebugManagerLogicTests.swift index 9185ebed2e..59ba1d8e5d 100644 --- a/Tests/SuperwallKitTests/Debug/SWDebugManagerLogicTests.swift +++ b/Tests/SuperwallKitTests/Debug/SWDebugManagerLogicTests.swift @@ -87,4 +87,48 @@ struct SWDebugManagerLogicTests { // Then #expect(value == nil) } + + @Test func getQueryItemValue_trialState() { + // Given + let url = URL(string: "myapp://?superwall_debug=true&trial_state=eligible")! + + // When + let value = SWDebugManagerLogic.getQueryItemValue(fromUrl: url, withName: .trialState) + + // Then + #expect(value == "eligible") + } + + @Test func getQueryItemValue_appearance() { + // Given + let url = URL(string: "myapp://?superwall_debug=true&appearance=dark")! + + // When + let value = SWDebugManagerLogic.getQueryItemValue(fromUrl: url, withName: .appearance) + + // Then + #expect(value == "dark") + } + + @Test func getQueryItemValue_locale() { + // Given + let url = URL(string: "myapp://?superwall_debug=true&locale=de")! + + // When + let value = SWDebugManagerLogic.getQueryItemValue(fromUrl: url, withName: .locale) + + // Then + #expect(value == "de") + } + + @Test func getQueryItemValue_present() { + // Given + let url = URL(string: "myapp://?superwall_debug=true&present=true")! + + // When + let value = SWDebugManagerLogic.getQueryItemValue(fromUrl: url, withName: .present) + + // Then + #expect(value == "true") + } } diff --git a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift index e4cd87166c..10837cb241 100644 --- a/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift +++ b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift @@ -40,7 +40,10 @@ final class CheckDebuggerPresentationTests { @Test func checkDebuggerPresentation_debuggerLaunched_presentingOnDebugger() async { let dependencyContainer = DependencyContainer() - let debugViewController = await dependencyContainer.makeDebugViewController(withDatabaseId: "abc") + let debugViewController = await dependencyContainer.makeDebugViewController( + withDatabaseId: "abc", + overrides: DebugPaywallOverrides() + ) let request = PresentationRequest.stub() .setting(\.flags.isDebuggerLaunched, to: true) .setting(\.presenter, to: debugViewController)