Skip to content

0.3.0 Add support for post checkout behaviors - #9

Open
ianrumac wants to merge 7 commits into
mainfrom
ir/fix/merged-app-types
Open

ianrumac wants to merge 7 commits into
mainfrom
ir/fix/merged-app-types

Conversation

@ianrumac

@ianrumac ianrumac commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Consume the unified webapp/web2app post-purchase behaviors

Pairs with paywall-next ir/feat/web2app-delivery-runtime (#3913), which now sends
redemption_codes, a populated redirect_url, and post_purchase_behavior on
post_checkout_complete. Before this, the SDK completed the purchase and silently
dropped all three — the paywall closed and nothing else happened.

Wire alignment (the reason nothing worked)

PostCheckoutCompleteMessage declared transactionData / redirectUrl in camelCase
while the wire (and our own tests) used snake_case, so those lookups never matched
anything. Renamed to transaction_data / redirect_url with snake_case inner keys,
and readTransactionField now reads the right path. Also deleted the "these are
ALWAYS undefined" contract comment on the handler — no longer true.

Behavior handling

Behavior Carried by SDK action
GRANT_ACCESS entitlements_token unchanged
REDIRECT redirect_url emit paywallWillOpenURL, then navigate
REDEEM redemption_codes hand to merchant; redeem() takes them as-is
CUSTOM redemption_codes hand to merchant, no navigation

REDIRECT navigates the current tab rather than opening a new one. The
post_checkout_complete handler runs without transient user activation, so
window.open is routinely popup-blocked and the redirect vanishes silently.
Navigation is deferred one tick so the resolved purchase (onDismiss,
paywall_close, event dispatches) flushes before unload. paywallWillOpenURL
still fires first so consumers can route it themselves, and
options.paywalls.postPurchaseRedirect: "newTab" opts back into the old behavior
for hosts that can't lose page state.

New public surface

  • PaywallResult (purchased) carries redemptionCodes and postPurchaseBehavior,
    so they arrive via handler.onDismiss and register()'s return value.
  • New local-only redemptionCodesReceived event (codes, productId,
    checkoutContextId, paywallInfo, behavior), bridged to
    SuperwallDelegate.onRedemptionCodesReceived. Local-only because the backend
    already records the redemption server-side.
  • New PostPurchaseBehavior type ("GRANT_ACCESS" | "REDIRECT" | "REDEEM" | "CUSTOM");
    unknown future values are dropped rather than passed through.
  • sw.redeem(code) — redeems a redemption_… code for the current user. The
    redeem logic previously lived inline in the automatic controller's deps, so it was
    unreachable for anyone supplying a custom PurchaseController; it's now a shared
    redeemCode() used by both that path and the public method. Seeds customerInfo,
    flips subscriptionStatus on success, fires onWillRedeemLink / onDidRedeemLink,
    and never throws — failures resolve as error / expired / invalid. Drive-by
    fix: an INVALID code now resolves as a clean { type: "invalid", code } instead of
    stuffing an error field into the invalid variant.

paywallId parity with the native SDKs

The init payload sent the identifier slug as both paywallId and
paywallIdentifier. Now parses paywall_responses[].id (exposed as
PaywallInfo.databaseId) and sends it as paywallId, keeping the slug as
paywallIdentifier — matching iOS/Android. Configs predating the field fall back to
the slug so it's never empty. Applies to both collector.paywallSlice and
checkoutContext.paywall.

Compatibility

Every new wire field is optional, so an older paywall sends none and behaves exactly
as today. The exposure runs the other way: an older SDK against a paywall newly
authored REDIRECT/REDEEM/CUSTOM
completes the purchase and drops the URL or code.
Gate those three behaviors in the dashboard picker on an SDK version floor, or don't
offer them for embedded until this ships.

Testing

New/extended coverage for the populated post_checkout_complete path (codes on the
result, the event with purchase context, behavior passthrough on all three surfaces),
the navigate-by-default redirect and the newTab opt-in, sw.redeem() success and
INVALID paths, the LOCAL_ONLY set, and the paywallId/paywallIdentifier split
including slug fallback. tsc --noEmit clean in paywalls-js and paywalls-react;
package suite 278 pass with 86 pre-existing sandbox-network failures that fail
identically on a clean checkout.

@ianrumac ianrumac changed the title Add support for post checkout behaviors 0.3.0 Add support for post checkout behaviors Sep 10, 2026

This branch has not been deployed

No deployments
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.

1 participant