Trigger /redeem after a successful native purchase - #490
Conversation
A successful native (App Store) purchase only refreshed entitlements locally via `loadPurchasedProducts` and never called `/redeem`, so a new subscription for an already-identified user wasn't linked to them server-side until another trigger fired. Call `webEntitlementRedeemer.redeem(.existingCodes)` after `loadPurchasedProducts` in `TransactionManager.loadPurchasedProductsIfNeeded` so the freshly-loaded receipts + appTransactionId are pushed to the backend on every native purchase. This is the single funnel for both SK1 and SK2 purchases and, being gated by `shouldSkipReceiptLoading`, skips custom/web products and test mode. Restores go through `didRestore` and are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrsXRkhSGciN62RK7mVpBV
Extend SW-5516 (already redeeming after a native purchase) to also fire webEntitlementRedeemer.redeem(.existingCodes) on the successful restore path. Added in TransactionManager.didRestore, which is only reached after a restore succeeds and after receipts/entitlements have been reloaded, so a freshly-restored subscription is linked to the identified user server-side. Skipped in test mode, mirroring the purchase path's shouldSkipReceiptLoading gate. Updated the purchase-path comment and CHANGELOG accordingly.
|
PR author is not in the allowed authors list. |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — this PR fires /redeem on two success-only paths so a new native subscription for an already-identified user is linked server-side immediately after purchase or restore (Linear SW-5516).
- Redeem after native purchase —
TransactionManager.loadPurchasedProductsIfNeeded(for:)callswebEntitlementRedeemer.redeem(.existingCodes)right afterreceiptManager.loadPurchasedProducts, behind the existingshouldSkipReceiptLoadingguard (skips custom/web products and test mode). - Redeem after restore —
TransactionManager.didRestore(restoreSource:)calls the same redeem after the restore tracking, behind a new inlineisTestModeearly return. CHANGELOG.md— one enhancement bullet under4.16.2.
The redeem call is a benign resend of existing codes plus the freshly-loaded device receipts and appTransactionId, and is already invoked from ConfigManager, IdentityManager, and ReceiptManager. The test-mode restore branch (TransactionManager.swift:333) does reach didRestore, but the new isTestMode guard correctly skips redeem there; .webRestore returns before didRestore. Purchase and restore are distinct funnels, so there's no double-fire.
ℹ️ No tests added for the new redeem triggers
Neither redeem trigger is covered by a test. The direct Superwall.shared.dependencyContainer.webEntitlementRedeemer singleton access in TransactionManager makes these paths awkward to unit-test in isolation, and this mirrors the existing untested pattern in ReceiptManager.registerAppTransactionIdIfNeeded, so this is a consistency observation rather than a gap introduced by this PR. Mergeable as-is; worth a note if TransactionManager redeem behavior later needs regression coverage.
Claude Opus | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — since the prior pullfrog review the two /redeem triggers were switched from blocking await to fire-and-forget Task { }, moving the redeem round-trip off the purchase/restore completion critical path.
- Redeem calls made async — in
TransactionManager, bothwebEntitlementRedeemer.redeem(.existingCodes)calls (purchase path inloadPurchasedProductsIfNeeded(for:)and restore path indidRestore(restoreSource:)) are now wrapped in a detachedTask { }instead of being awaited inline. CHANGELOG.md— the enhancement bullet was reworded ("Links subscriptions to the user server-side after successful purchase or restore.").
The fire-and-forget change is sound: it matches the established convention at the other redeem(.existingCodes) call sites (ReceiptManager.registerAppTransactionIdIfNeeded and IdentityManager), and nothing downstream in handle(result:state:) or didRestore depends on the redeem result, so detaching it only removes network latency from paywall dismissal and success tracking. No behavior regression.
Claude Opus | 𝕏

Requested by David Granzin · Slack thread
Changes in this pull request
Before: A successful native (App Store) purchase only refreshed entitlements locally via
receiptManager.loadPurchasedProducts. It never called/redeem, so when an already-identified user started a new native subscription, that subscription was not linked to them server-side until some unrelated trigger fired (deep-link code, identity set, config reset, attribution change, or the one-time appTransactionId registration). Successful restores had the same gap.After: Per Linear SW-5516 ("always call redeem after a successful purchase & on restore"),
/redeemnow fires on both paths: after a successful native purchase and after a successful restore. Each call carries the fresh device receipts +appTransactionId, so new subscriptions are linked to the user on the backend immediately after the purchase or restore completes.How
Two success-only hooks, both re-sending the current codes plus the freshly-loaded native receipts and
appTransactionIdviaSuperwall.shared.dependencyContainer.webEntitlementRedeemer.redeem(.existingCodes)(seeWebEntitlementRedeemer.createRedeemRequest):Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift,loadPurchasedProductsIfNeeded(for:)callsredeem(.existingCodes)right afterreceiptManager.loadPurchasedProducts(config: nil). This is the single funnel that both SK1 and SK2 purchases settle through (purchase→handle(result:state:)→didPurchase()), so no per-purchaser edits are needed.didRestorepath callsredeem(.existingCodes)after a successful restore, so restored subscriptions are linked server-side just like fresh purchases.Both calls sit behind the existing test-mode /
shouldSkipReceiptLoadingguards, so they are skipped for custom/web (Stripe) products and in test mode, and both fire only on success. It matches the existing access pattern used byReceiptManagerfor the one-time appTransactionId registration.Checklist
CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.swiftlintin the main directory and fixed any issues.Generated by Claude Code