Skip to content

4.14.0#439

Merged
yusuftor merged 75 commits intomasterfrom
develop
Feb 24, 2026
Merged

4.14.0#439
yusuftor merged 75 commits intomasterfrom
develop

Conversation

@yusuftor
Copy link
Collaborator

@yusuftor yusuftor commented Feb 20, 2026

Changes in this pull request

Enhancements

  • Adds support for "Test Mode", which allows you to simulate in-app purchases without involving StoreKit. Test Mode can be enabled through the Superwall dashboard by marking specific users as test store users, or activates automatically when a bundle ID mismatch is detected. When active, a configuration modal lets you select starting entitlements and override free trial availability. Purchases are simulated with a UI that lets users complete, abandon, or fail transactions, with all purchase events firing normally for end-to-end paywall testing.
  • Adds prioritized campaign preloading. When a campaign is marked as prioritized in the dashboard, its paywalls are preloaded before all others.
  • Adds Stripe checkout message handling for stripe_checkout_start, stripe_checkout_submit, stripe_checkout_complete, stripe_checkout_fail, and stripe_checkout_abandon.
  • Adds SDK-side analytics tracking for Stripe checkout lifecycle events (start, submit, complete, fail) with store and product_identifier payload fields.

Fixes

  • Fixes issue with compiling on Xcode 26.4 beta.
  • Fixes dashboard display of multiple active entitlements.

Checklist

  • All unit tests pass.
  • All UI tests pass.
  • Demo project builds and runs on iOS.
  • Demo project builds and runs on Mac Catalyst.
  • Demo project builds and runs on visionOS.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run swiftlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

Greptile Summary

This release adds two major features: Test Mode for simulating in-app purchases without StoreKit, and Stripe checkout lifecycle tracking with automatic recovery polling.

Major changes:

  • Test Mode activates via dashboard user marking, bundle ID mismatch detection, or explicit SDK configuration, allowing end-to-end paywall testing with simulated transactions
  • Stripe checkout polling uses proper actor synchronization with hasActiveStripePoll flag to prevent race conditions
  • Added prioritized campaign preloading for campaigns marked as priority in dashboard
  • Comprehensive test coverage for new functionality (898 lines in WebEntitlementRedeemerTests.swift, 323 lines in TestModeManagerTests.swift)

Key implementation notes:

  • Stripe checkout recovery polls on foreground, paywall open, and checkout complete events with configurable timeout (60s) and interval (1.5s)
  • Test mode uses unowned references appropriately to avoid retain cycles while maintaining safe access patterns
  • Version numbers correctly updated across Constants.swift, SuperwallKit.podspec, and CHANGELOG.md

Confidence Score: 5/5

  • Safe to merge - well-tested major feature release with proper synchronization
  • Extensive test coverage (1200+ lines of tests), proper actor-based synchronization for async operations, comprehensive version management, and adherence to project conventions
  • No files require special attention

Important Files Changed

Filename Overview
Sources/SuperwallKit/TestMode/TestModeManager.swift New test mode manager for simulating purchases without StoreKit - clean implementation
Sources/SuperwallKit/Web/WebEntitlementRedeemer.swift Added Stripe checkout polling with proper synchronization using hasActiveStripePoll flag
Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift Added Stripe checkout recovery on paywall open with proper loading state management
Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift Added handlers for stripe_checkout_start/submit/complete/fail/abandon messages
Sources/SuperwallKit/StoreKit/Products/StoreProduct/TestStoreProduct.swift New test store product implementation backed by SuperwallProduct API data
Sources/SuperwallKit/Config/ConfigManager.swift Added test mode evaluation logic and prioritized campaign preloading

Sequence Diagram

sequenceDiagram
    participant User
    participant Paywall
    participant MessageHandler
    participant WebEntitlementRedeemer
    participant StripeAPI
    
    User->>Paywall: Click Stripe checkout
    Paywall->>MessageHandler: stripe_checkout_start
    MessageHandler->>MessageHandler: Track start event
    
    User->>Paywall: Submit payment
    Paywall->>MessageHandler: stripe_checkout_submit
    MessageHandler->>WebEntitlementRedeemer: registerStripeCheckoutSubmit(contextId, productId)
    WebEntitlementRedeemer->>WebEntitlementRedeemer: Save pending state
    MessageHandler->>MessageHandler: Track submit event
    
    alt Checkout succeeds
        Paywall->>MessageHandler: stripe_checkout_complete
        MessageHandler->>WebEntitlementRedeemer: handleStripeCheckoutComplete(contextId, productId)
        MessageHandler->>MessageHandler: Track complete event
        WebEntitlementRedeemer->>WebEntitlementRedeemer: Set hasActiveStripePoll = true
        loop Poll until success/timeout
            WebEntitlementRedeemer->>StripeAPI: Poll redemption result
            alt Result ready
                StripeAPI-->>WebEntitlementRedeemer: Redemption code
                WebEntitlementRedeemer->>WebEntitlementRedeemer: Handle redemption success
                WebEntitlementRedeemer->>WebEntitlementRedeemer: Clear pending state
            else Still pending
                StripeAPI-->>WebEntitlementRedeemer: Status: pending
                WebEntitlementRedeemer->>WebEntitlementRedeemer: Sleep 1.5s, continue
            end
        end
        WebEntitlementRedeemer->>WebEntitlementRedeemer: Set hasActiveStripePoll = false
        WebEntitlementRedeemer->>Paywall: Update loading state
    else Checkout abandoned
        Paywall->>MessageHandler: stripe_checkout_abandon
        MessageHandler->>WebEntitlementRedeemer: handleStripeCheckoutAbandon(productId)
        WebEntitlementRedeemer->>WebEntitlementRedeemer: Track abandon event
        WebEntitlementRedeemer->>WebEntitlementRedeemer: Clear awaitingCheckoutComplete
    else Checkout fails
        Paywall->>MessageHandler: stripe_checkout_fail
        MessageHandler->>MessageHandler: Track fail event
    end
    
    alt App backgrounded then foregrounded
        User->>Paywall: Return to app
        WebEntitlementRedeemer->>WebEntitlementRedeemer: pollPendingStripeCheckoutOnForeground
        WebEntitlementRedeemer->>WebEntitlementRedeemer: Decrement foreground attempts
        WebEntitlementRedeemer->>StripeAPI: Poll for pending checkout
    end
    
    alt Paywall reopened with pending checkout
        User->>Paywall: Open paywall
        Paywall->>WebEntitlementRedeemer: pollOrWaitForActiveStripePoll()
        alt Poll already in-flight
            WebEntitlementRedeemer->>WebEntitlementRedeemer: Wait for existing poll (100ms checks)
        else No active poll
            WebEntitlementRedeemer->>StripeAPI: Start new poll
        end
    end
Loading

Last reviewed commit: 7ab22d3

Context used:

  • Context from dashboard - CLAUDE.md (source)

yusuftor and others added 30 commits June 4, 2025 16:45
And renamed `getAllActiveTreatmentPaywallIds` to `getActiveTreatmentPaywallIds`, and removed the old `getActiveTreatmentPaywallIds`. This means that when someone calls `preloadPaywalls(forPlacements:)`
Add CodingKeys for snake_case decoding since Web2App decoder
doesn't auto-convert from snake case.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Maps V2Product API response data into a StoreProductType for use
as a test store product backed by the Superwall API.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add TestModeManager for detecting test mode users via config
- Add TestStoreUser model parsed from config testStoreUsers
- Add TestModeColdLaunchAlert for showing test mode status on launch
- Add TestModePurchaseDrawer for simulating purchases (purchase/abandon/fail)
- Add TestModeTransactionHandler to intercept purchase and restore flows
- Override StoreKitManager.getProducts to use cached test products
- Override TransactionManager.purchase to show test mode drawer
- Override TransactionManager.tryToRestore to show entitlement picker
- Override DeviceHelper.isSandbox to return true in test mode
- Rename V2Product to SuperwallProduct across codebase
- Fetch products from /v1/products endpoint on config load when test mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…tsById

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add bundleId to config endpoint application query
- Include bundle_id_config in config JSON response
- Decode bundleIdConfig in Swift Config model
- Check bundle ID mismatch in evaluateTestMode — activates test mode
  when app bundle ID doesn't match config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
yusuftor and others added 23 commits February 18, 2026 14:29
… detection

- Rename DebugModeBehavior -> TestModeBehavior, debugModeBehavior -> testModeBehavior
- Rename SWKDebugModeBehavior -> SWKTestModeBehavior (ObjC)
- Rename debugOption -> testModeOption
- Rename isUITestEnvironment -> isTestEnvironment
- Skip test mode in test environments unless SUPERWALL_UNIT_TESTS launch arg is set
- Update tests to work correctly with the new environment detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use where clause instead of if inside for loop
- Remove superfluous swiftlint disable commands
- Remove extra blank line before closing brace

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add testModeBehavior option to SuperwallOptions
feat: Test Mode — test store products, purchase/restore overrides, dark mode UI
Replaces the preloading schedule model with a simple prioritizedCampaignId
on the config. When set, the SDK preloads the prioritized campaign's
paywalls first (with a 5s delay), then preloads the rest.

- Remove PaywallPreloading, PreloadingStep, PreloadablePlacement models
- Add prioritizedCampaignId to Config
- Update ConfigManager.preloadAllPaywalls() to use prioritized campaign
- Update tests for new approach

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Now if we're in an extension, we won't start test mode
When switching API keys, the cached config from the old key persists and
can cause incorrect test mode activation due to bundleIdConfig mismatch.
Store the last-used API key and clear config, enrichment, and test mode
caches when it changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

69 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@yusuftor yusuftor merged commit aa839af into master Feb 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants