fix: filter old subscription transactions during upgrade#22
Conversation
WalkthroughAdds per-subscription-group latest transaction tracking and a gating function to skip superseded subscription transactions. Updates OpenIapModule to log transactions, skip revoked transactions, consult the per-group gate, preserve processed checks, store pending transactions, and log emitted purchase updates. (≤50 words) Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant StoreKit
participant OpenIapModule
participant IapState
User->>StoreKit: purchase / upgrade
StoreKit-->>OpenIapModule: transaction update
OpenIapModule->>OpenIapModule: log transaction details
alt revoked (revocationDate set)
OpenIapModule-->>StoreKit: skip (revoked)
else not revoked
alt subscription transaction (has subscriptionGroupID)
OpenIapModule->>IapState: shouldProcessSubscriptionTransaction(tx)
IapState-->>OpenIapModule: true / false
alt false (superseded)
OpenIapModule-->>StoreKit: skip (superseded by newer in group)
else true
OpenIapModule->>OpenIapModule: check already-processed
alt already processed
OpenIapModule-->>StoreKit: skip (already processed)
else mark/store pending and emit
OpenIapModule-->>User: emitPurchaseUpdate (logged)
end
end
else non-subscription
OpenIapModule->>OpenIapModule: check already-processed → mark/store → emit
OpenIapModule-->>User: emitPurchaseUpdate (logged)
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
🧰 Additional context used📓 Path-based instructions (2)Sources/**/*.swift📄 CodeRabbit inference engine (CLAUDE.md)
Files:
Sources/Helpers/**/*.swift📄 CodeRabbit inference engine (CLAUDE.md)
Files:
🪛 markdownlint-cli2 (0.18.1)ISSUE_ANALYSIS.md12-12: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 23-23: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 54-54: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 🔇 Additional comments (4)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
Sources/Helpers/IapState.swift(3 hunks)Sources/OpenIapModule.swift(2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
Sources/**/*.swift
📄 CodeRabbit inference engine (CLAUDE.md)
Sources/**/*.swift: iOS-specific functions MUST have IOS suffix
Android-specific functions MUST have Android suffix
Cross-platform functions must have NO platform suffix
Acronyms in Swift should be ALL CAPS only when used as a suffix; otherwise use Pascal case (first letter caps, rest lowercase)
Specific casing: iOS -> Ios in beginning/middle, IOS as suffix
Specific casing: IAP -> Iap in beginning/middle, IAP as suffix
Specific casing: API -> Api in beginning/middle, API as suffix
Specific casing: URL -> Url in beginning/middle, URL as suffix
OpenIapError static code constants use PascalCase names; raw string values remain E_ codes; avoid introducing new E_-prefixed identifiers in Swift
Files:
Sources/Helpers/IapState.swiftSources/OpenIapModule.swift
Sources/Helpers/**/*.swift
📄 CodeRabbit inference engine (CLAUDE.md)
Helpers should use descriptive names ending with purpose (e.g., Manager, Cache, Status)
Files:
Sources/Helpers/IapState.swift
Sources/{OpenIapProtocol.swift,OpenIapModule.swift}
📄 CodeRabbit inference engine (CLAUDE.md)
Sources/{OpenIapProtocol.swift,OpenIapModule.swift}: Public API names MUST match openiap.dev and React Native OpenIAP (Apple module)
Use standard Apple module API names exactly: initConnection(), endConnection(), fetchProducts(), getAvailablePurchases(), requestPurchase(), finishTransaction()
Files:
Sources/OpenIapModule.swift
🧠 Learnings (1)
📚 Learning: 2025-10-09T19:13:15.972Z
Learnt from: CR
PR: hyodotdev/openiap-apple#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-09T19:13:15.972Z
Learning: Applies to Sources/{OpenIapProtocol.swift,OpenIapModule.swift} : Use standard Apple module API names exactly: initConnection(), endConnection(), fetchProducts(), getAvailablePurchases(), requestPurchase(), finishTransaction()
Applied to files:
Sources/OpenIapModule.swift
🧬 Code graph analysis (2)
Sources/Helpers/IapState.swift (2)
Sources/OpenIapStore.swift (1)
shouldReplaceSubscription(442-465)Sources/Helpers/StoreKitTypesBridge.swift (1)
subscriptionAutoRenewState(147-168)
Sources/OpenIapModule.swift (1)
Sources/Helpers/IapState.swift (1)
shouldProcessSubscriptionTransaction(40-58)
🔇 Additional comments (5)
Sources/OpenIapModule.swift (3)
841-855: LGTM! Transaction logging and revocation check are well-implemented.The detailed logging provides excellent visibility into transaction processing, and the explicit revocation check correctly skips invalid transactions before any further processing. The placement of the revocation check before the subscription filtering ensures revoked transactions don't affect the latest-transaction tracking.
916-916: Good observability improvement.The debug log provides helpful information about purchase update emissions, making it easier to diagnose listener issues.
857-862: IapState actor isolation confirmed:IapStateis declared as anactorand allstateinteractions areawaited, guaranteeing serialized processing and proper skipping of older subscription transactions.Sources/Helpers/IapState.swift (2)
11-12: LGTM! Per-group tracking added correctly.The
latestTransactionDateByGroupdictionary effectively tracks the newest transaction per subscription group, enabling the filtering of superseded transactions during upgrades.
24-24: Correct reset behavior.Clearing
latestTransactionDateByGroupon reset ensures that the tracking state is properly reinitialized when the connection is reset.
There was a problem hiding this comment.
The tie-breaking logic concern has been addressed in commit 7f271a8. Resolving this thread.
Problem
Fixes hyochan/react-native-iap#3054
When upgrading from a monthly subscription to a yearly subscription (within the same subscription group),
onPurchaseSuccesswas emitting old purchase objects first, and the correct upgraded transaction arrived only after 3-4 minutes in iOS Sandbox.Root Cause
Analysis of actual purchase data from the issue reporter revealed that
isUpgradedis not reliably set totrueduring subscription upgrades in Sandbox. Both the old and new transactions showisUpgradedIOS: falsewithreasonIOS: "renewal".This happens because StoreKit treats subscription upgrades that occur at renewal boundaries as "renewal" events, not "upgrade" events.
Example data from issue reporter:
Monthly (arrives first):
{ "transactionDate": 1760118720000, "isUpgradedIOS": false, // ❌ Not set despite being superseded "reasonIOS": "renewal" }Yearly (arrives 180 seconds later):
{ "transactionDate": 1760118900000, "isUpgradedIOS": false, "reasonIOS": "renewal" }Solution
Instead of relying on the unreliable
isUpgradedflag, we now track the latest transaction date per subscription group:How It Works
purchaseDatefor eachsubscriptionGroupIDisUpgradedflag valueChanges
IapState.swift:
latestTransactionDateByGroupdictionary to track latest transaction per subscription groupshouldProcessSubscriptionTransaction()to determine if a transaction should be processedOpenIapModule.swift:
isUpgradedcheckTesting
Behavior After Fix
Subscription Upgrade (Monthly → Yearly):
Subscription Renewal:
Refunded Purchase:
revocationDate != nil)Non-subscription IAP:
Summary by CodeRabbit