Skip to content

Trigger /redeem after a successful native purchase - #490

Merged
yusuftor merged 4 commits into
developfrom
redeem-on-native-purchase
Jul 22, 2026
Merged

Trigger /redeem after a successful native purchase#490
yusuftor merged 4 commits into
developfrom
redeem-on-native-purchase

Conversation

@claude

@claude claude Bot commented Jul 18, 2026

Copy link
Copy Markdown

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"), /redeem now 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 appTransactionId via Superwall.shared.dependencyContainer.webEntitlementRedeemer.redeem(.existingCodes) (see WebEntitlementRedeemer.createRedeemRequest):

  • Purchase: In Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift, loadPurchasedProductsIfNeeded(for:) calls redeem(.existingCodes) right after receiptManager.loadPurchasedProducts(config: nil). This is the single funnel that both SK1 and SK2 purchases settle through (purchasehandle(result:state:)didPurchase()), so no per-purchaser edits are needed.
  • Restore: The didRestore path calls redeem(.existingCodes) after a successful restore, so restored subscriptions are linked server-side just like fresh purchases.

Both calls sit behind the existing test-mode / shouldSkipReceiptLoading guards, 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 by ReceiptManager for the one-time appTransactionId registration.

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

Generated by Claude Code

claude added 2 commits July 18, 2026 22:57
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.
@anglinb
anglinb requested a review from yusuftor July 20, 2026 17:00
@yusuftor
yusuftor marked this pull request as ready for review July 22, 2026 13:55
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

PR author is not in the allowed authors list.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ 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 purchaseTransactionManager.loadPurchasedProductsIfNeeded(for:) calls webEntitlementRedeemer.redeem(.existingCodes) right after receiptManager.loadPurchasedProducts, behind the existing shouldSkipReceiptLoading guard (skips custom/web products and test mode).
  • Redeem after restoreTransactionManager.didRestore(restoreSource:) calls the same redeem after the restore tracking, behind a new inline isTestMode early return.
  • CHANGELOG.md — one enhancement bullet under 4.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.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ 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, both webEntitlementRedeemer.redeem(.existingCodes) calls (purchase path in loadPurchasedProductsIfNeeded(for:) and restore path in didRestore(restoreSource:)) are now wrapped in a detached Task { } 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.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@yusuftor
yusuftor merged commit 6119661 into develop Jul 22, 2026
3 checks passed
@yusuftor
yusuftor deleted the redeem-on-native-purchase branch July 22, 2026 15:30
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.

2 participants