feat: add renewalInfoIOS for subscription status tracking#24
Conversation
WalkthroughAdds a RenewalInfoIOS model and attaches it to PurchaseIOS; extracts subscription renewal info from StoreKit via a new helper and uses it to set willAutoRenew/isAutoRenewing and include renewalInfoIOS in PurchaseIOS. Also adjusts deduplication/logging, external purchase notice error mapping, tests, and bumps a gql dependency. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor App
participant Bridge as StoreKitTypesBridge
participant StoreKit as StoreKit
participant Models as PurchaseIOS/RenewalInfoIOS
participant Store as OpenIapStore
App->>Bridge: Build PurchaseIOS from transaction
Bridge->>StoreKit: Query subscription status / renewal info
alt Detailed renewal info available (iOS18+/macOS15+)
StoreKit-->>Bridge: Renewal data
else Limited or unavailable
StoreKit-->>Bridge: No/limited data
end
Bridge->>Models: Construct RenewalInfoIOS? (including willAutoRenew)
Bridge-->>Store: PurchaseIOS(renewalInfoIOS: ...)
Store->>Store: Deduplicate purchases, skip inactive subs, log renewalInfo
Store-->>App: Available purchases set (with renewal info)
sequenceDiagram
autonumber
actor Client
participant Module as OpenIapModule
participant System as ExternalPurchaseNoticeAPI
Client->>Module: presentExternalPurchaseNotice(...)
alt Sheet available
Module->>System: Present notice
System-->>Module: Result (acknowledged / dismissed / other)
alt acknowledged/dismissed
Module-->>Client: ExternalPurchaseNoticeResultIOS(status, errorMessage: nil)
else unexpected
Module->>Module: Throw PurchaseError(unknown result)
Module-->>Client: ExternalPurchaseNoticeResultIOS(dismissed, errorMessage: mapped)
end
else Not available
Module->>Module: Throw PurchaseError(featureNotSupported)
Module-->>Client: ExternalPurchaseNoticeResultIOS(dismissed, errorMessage: mapped)
end
note right of Module: All errors are mapped to PurchaseError and surfaced via result.errorMessage
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/OpenIapModule.swift (1)
653-681: Inconsistent error channel for notice sheetOn unavailability, you throw; other errors return
.dismissedwith an error string. This inconsistency complicates callers.Return a result for unavailability too:
- guard await ExternalPurchase.canPresent else { - throw makePurchaseError( - code: .featureNotSupported, - message: "External purchase notice sheet is not available" - ) - } + guard await ExternalPurchase.canPresent else { + return ExternalPurchaseNoticeResultIOS( + error: "External purchase notice sheet is not available", + result: .dismissed + ) + }And for older OS:
- } else { - throw makePurchaseError( - code: .featureNotSupported, - message: "External purchase notice sheet requires iOS 18.2 or later" - ) - } + } else { + return ExternalPurchaseNoticeResultIOS( + error: "External purchase notice sheet requires iOS 18.2 or later", + result: .dismissed + ) + }
🧹 Nitpick comments (4)
Sources/OpenIapStore.swift (2)
173-201: Gate verbose renewalInfo logsThese per-field logs are helpful but chatty. Consider wrapping with a debug flag or lowered log level to avoid production noise.
421-434: Use grace period and renewalInfo when deciding “active”Inactive filtering ignores gracePeriodExpirationDate and willAutoRenew. This can drop subscriptions in billing‑retry grace periods.
Apply this refinement:
- let isActive: Bool - if let expiry = iosPurchase.expirationDateIOS { - let expiryDate = Date(timeIntervalSince1970: expiry / 1000) - isActive = expiryDate > Date() - } else { - isActive = iosPurchase.isAutoRenewing - || iosPurchase.purchaseState == .purchased - || iosPurchase.purchaseState == .restored - } + var isActive: Bool + if let expiryMs = iosPurchase.expirationDateIOS { + let expiryDate = Date(timeIntervalSince1970: expiryMs / 1000) + isActive = expiryDate > Date() + // Treat billing grace period as active + if isActive == false, let graceMs = iosPurchase.renewalInfoIOS?.gracePeriodExpirationDate { + let graceDate = Date(timeIntervalSince1970: graceMs / 1000) + isActive = graceDate > Date() + } + } else { + // No expiry known; prefer RenewalInfo when available + isActive = (iosPurchase.renewalInfoIOS?.willAutoRenew == true) + || iosPurchase.isAutoRenewing + || iosPurchase.purchaseState == .purchased + || iosPurchase.purchaseState == .restored + }Sources/Helpers/StoreKitTypesBridge.swift (2)
139-170: Dead code: not used anywhere
determineAutoRenewStatusand its helper are no longer called. Remove or wire them in to avoid confusion.
172-249: Consider caching SubscriptionInfo.status calls per group
status(for:)is invoked per transaction during mapping, which can be costly for many entitlements. Cache results bysubscriptionGroupIDwithin a call scope or batch compute to reduce StoreKit calls.Would you like a small in‑memory cache keyed by groupId for this mapper?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
Sources/Helpers/StoreKitTypesBridge.swift(3 hunks)Sources/Models/Types.swift(2 hunks)Sources/OpenIapModule.swift(3 hunks)Sources/OpenIapStore.swift(3 hunks)Tests/OpenIapTests.swift(3 hunks)openiap-versions.json(1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
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/OpenIapStore.swiftSources/Models/Types.swiftSources/OpenIapModule.swiftSources/Helpers/StoreKitTypesBridge.swift
Sources/Models/Types.swift
📄 CodeRabbit inference engine (CLAUDE.md)
Types.swift in Sources/Models is auto-generated; DO NOT edit directly
Files:
Sources/Models/Types.swift
Sources/Models/**/*.swift
📄 CodeRabbit inference engine (CLAUDE.md)
Models must match OpenIAP specification exactly (official types only)
Files:
Sources/Models/Types.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
openiap-versions.json
📄 CodeRabbit inference engine (CLAUDE.md)
openiap-versions.json: Version management is in openiap-versions.json; update versions there (not elsewhere)
Commit changes to openiap-versions.json when updating versions
Files:
openiap-versions.json
Sources/Helpers/**/*.swift
📄 CodeRabbit inference engine (CLAUDE.md)
Helpers should use descriptive names ending with purpose (e.g., Manager, Cache, Status)
Files:
Sources/Helpers/StoreKitTypesBridge.swift
Sources/Helpers/**/*Store*.swift
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid using 'Store' in names for caching/helper classes; use Cache or Manager instead
Files:
Sources/Helpers/StoreKitTypesBridge.swift
🧠 Learnings (3)
📚 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/OpenIapStore.swiftSources/OpenIapModule.swift
📚 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 openiap-versions.json : Version management is in openiap-versions.json; update versions there (not elsewhere)
Applied to files:
openiap-versions.json
📚 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 openiap-versions.json : Commit changes to openiap-versions.json when updating versions
Applied to files:
openiap-versions.json
🧬 Code graph analysis (3)
Tests/OpenIapTests.swift (1)
Sources/Models/OpenIapSerialization.swift (2)
purchase(200-207)encode(22-31)
Sources/OpenIapStore.swift (2)
Example/OpenIapExample/Models/IapCompat.swift (2)
asIOS(44-49)asIOS(54-59)Sources/Helpers/StoreKitTypesBridge.swift (1)
purchase(64-66)
Sources/OpenIapModule.swift (1)
Sources/Models/OpenIapError.swift (1)
purchaseError(79-81)
🪛 SwiftLint (0.57.0)
Sources/Helpers/StoreKitTypesBridge.swift
[Warning] 208-208: TODOs should be resolved (Add when API confirmed)
(todo)
[Warning] 235-235: TODOs should be resolved (Add when API confirmed)
(todo)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: test
🔇 Additional comments (9)
openiap-versions.json (1)
3-3: Version bump location is correctgql upgraded to 1.2.0 in the right place. No other changes needed here.
As per coding guidelines
Sources/OpenIapStore.swift (1)
169-172: Good: summarize after deduplicationAssigning deduplicated purchases before logging prevents noisy per-item logs. Looks solid.
Sources/Helpers/StoreKitTypesBridge.swift (1)
208-209: Resolve or suppress TODOs flagged by SwiftLintReplace TODOs with tracked issues or
// swiftlint:disable:this todoif intentional.As per static analysis hints
Also applies to: 235-235
Tests/OpenIapTests.swift (3)
47-102: RenewalInfo tests look solidCovers presence and key fields, good fixtures.
104-141: Serialization round‑trip verifiedDictionary and JSON round‑trip checks are appropriate. Nice.
218-253: Helper for sample with renewal info is cleanKeeps tests DRY and readable.
Sources/Models/Types.swift (2)
406-406: Model surface extension aligns with schemaAdding
renewalInfoIOSto PurchaseIOS matches the new capability.As per coding guidelines (auto‑generated file)
460-489: RenewalInfoIOS fields look correct and future‑proofDoc comments help. Ensure this stays code‑generated to match the OpenIAP spec.
Sources/OpenIapModule.swift (1)
288-289: LGTM: clearer restore logIncluding onlyActive flag improves traceability.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
Sources/Helpers/StoreKitTypesBridge.swift (2)
188-243: Extract duplicate logic into a helper function.The verified (lines 188-215) and unverified (lines 216-242) cases contain nearly identical logic (~27 lines). The only difference is the info source.
Consider extracting the common logic:
private static func makeRenewalInfo( from info: StoreKit.Product.SubscriptionInfo.RenewalInfo, currentProductID: String ) -> RenewalInfoIOS { let pendingProductId = (info.autoRenewPreference != currentProductID) ? info.autoRenewPreference : nil let offerInfo: (id: String?, type: String?)? if #available(iOS 18.0, macOS 15.0, *) { offerInfo = (id: info.offer?.id, type: info.offer?.type.rawValue.description) } else { #if compiler(>=5.9) offerInfo = (id: info.offerID, type: info.offerType?.rawValue.description) #else offerInfo = nil #endif } return RenewalInfoIOS( autoRenewPreference: info.autoRenewPreference, expirationReason: info.expirationReason?.rawValue.description, gracePeriodExpirationDate: info.gracePeriodExpirationDate?.milliseconds, isInBillingRetry: nil, jsonRepresentation: nil, pendingUpgradeProductId: pendingProductId, priceIncreaseStatus: nil, renewalDate: info.renewalDate?.milliseconds, renewalOfferId: offerInfo?.id, renewalOfferType: offerInfo?.type, willAutoRenew: info.willAutoRenew ) }Then simplify the switch cases:
switch status.renewalInfo { case .verified(let info): - let pendingProductId = (info.autoRenewPreference != transaction.productID) ? info.autoRenewPreference : nil - let offerInfo: (id: String?, type: String?)? - if #available(iOS 18.0, macOS 15.0, *) { - offerInfo = (id: info.offer?.id, type: info.offer?.type.rawValue.description) - } else { - #if compiler(>=5.9) - offerInfo = (id: info.offerID, type: info.offerType?.rawValue.description) - #else - offerInfo = nil - #endif - } - let renewalInfo = RenewalInfoIOS(...) - return renewalInfo + return makeRenewalInfo(from: info, currentProductID: transaction.productID) case .unverified(let info, _): - let pendingProductId = (info.autoRenewPreference != transaction.productID) ? info.autoRenewPreference : nil - let offerInfo: (id: String?, type: String?)? - if #available(iOS 18.0, macOS 15.0, *) { - offerInfo = (id: info.offer?.id, type: info.offer?.type.rawValue.description) - } else { - #if compiler(>=5.9) - offerInfo = (id: info.offerID, type: info.offerType?.rawValue.description) - #else - offerInfo = nil - #endif - } - let renewalInfo = RenewalInfoIOS(...) - return renewalInfo + return makeRenewalInfo(from: info, currentProductID: transaction.productID) }
173-173: Consider adding IOS suffix for consistency.The function returns
RenewalInfoIOS?(an iOS-specific type). Per the coding guidelines, iOS-specific functions should have an IOS suffix.As per coding guidelines:
-private static func subscriptionRenewalInfo(for transaction: StoreKit.Transaction) async -> RenewalInfoIOS? { +private static func subscriptionRenewalInfoIOS(for transaction: StoreKit.Transaction) async -> RenewalInfoIOS? {And update the call site:
-let renewalInfoIOS = await subscriptionRenewalInfo(for: transaction) +let renewalInfoIOS = await subscriptionRenewalInfoIOS(for: transaction)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
Sources/Helpers/StoreKitTypesBridge.swift(3 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/StoreKitTypesBridge.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/StoreKitTypesBridge.swift
Sources/Helpers/**/*Store*.swift
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid using 'Store' in names for caching/helper classes; use Cache or Manager instead
Files:
Sources/Helpers/StoreKitTypesBridge.swift
🪛 SwiftLint (0.57.0)
Sources/Helpers/StoreKitTypesBridge.swift
[Warning] 209-209: TODOs should be resolved (Add when API confirmed)
(todo)
[Warning] 236-236: TODOs should be resolved (Add when API confirmed)
(todo)
🔇 Additional comments (2)
Sources/Helpers/StoreKitTypesBridge.swift (2)
73-75: LGTM! Conservative fallback addressed.The fallback to
falsewhen renewal info is unavailable correctly prevents misreporting cancelled/paused subscriptions as auto-renewing. This addresses the concern raised in the previous review.
192-201: Guard deprecated offer properties by OS availability, not compiler version Confirm whenofferID/offerTypewere first introduced, then replace the#if compiler(>=5.9)fallback with anif #available(<introduced version>, *)check, and apply the same update at lines 219–228.
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 (1)
Sources/Helpers/StoreKitTypesBridge.swift(3 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/StoreKitTypesBridge.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/StoreKitTypesBridge.swift
Sources/Helpers/**/*Store*.swift
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid using 'Store' in names for caching/helper classes; use Cache or Manager instead
Files:
Sources/Helpers/StoreKitTypesBridge.swift
🔇 Additional comments (2)
Sources/Helpers/StoreKitTypesBridge.swift (2)
73-76: Conservative auto-renew fallback: LGTMUsing renewalInfoIOS?.willAutoRenew ?? false is correct and avoids false positives.
122-122: Attaching renewalInfoIOS: LGTMOptional field preserves backward compatibility.
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 (1)
Sources/Helpers/StoreKitTypesBridge.swift(3 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/StoreKitTypesBridge.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/StoreKitTypesBridge.swift
Sources/Helpers/**/*Store*.swift
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid using 'Store' in names for caching/helper classes; use Cache or Manager instead
Files:
Sources/Helpers/StoreKitTypesBridge.swift
🔇 Additional comments (2)
Sources/Helpers/StoreKitTypesBridge.swift (2)
73-76: Conservative fallback for auto‑renewing looks goodDefaulting to false when renewal info is unavailable is the safe choice. Thanks for addressing prior feedback.
122-122: Passing renewalInfoIOS into PurchaseIOS is correctPlumbs renewal info through without breaking existing payloads (optional field).
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
Sources/Helpers/StoreKitTypesBridge.swift (1)
173-279: Consider extracting common mapping logic to reduce duplication.The
verifiedandunverifiedcases (lines 189-229 vs 230-270) share nearly identical mapping logic. While this duplication isn't critical, extracting a helper function would improve maintainability.Optional refactor to reduce duplication:
private static func subscriptionRenewalInfoIOS(for transaction: StoreKit.Transaction) async -> RenewalInfoIOS? { guard transaction.productType == .autoRenewable else { return nil } guard let groupId = transaction.subscriptionGroupID else { return nil } do { let statuses = try await StoreKit.Product.SubscriptionInfo.status(for: groupId) for status in statuses { guard case .verified(let statusTransaction) = status.transaction else { continue } guard statusTransaction.productID == transaction.productID else { continue } let info: Product.SubscriptionInfo.RenewalInfo switch status.renewalInfo { case .verified(let verifiedInfo): info = verifiedInfo case .unverified(let unverifiedInfo, _): info = unverifiedInfo } return mapRenewalInfo(info, transaction: transaction) } } catch { OpenIapLog.debug("⚠️ Failed to fetch renewalInfo: \(error.localizedDescription)") return nil } return nil } private static func mapRenewalInfo(_ info: Product.SubscriptionInfo.RenewalInfo, transaction: StoreKit.Transaction) -> RenewalInfoIOS { let pendingProductId = (info.autoRenewPreference != transaction.productID) ? info.autoRenewPreference : nil let offerInfo: (id: String?, type: String?)? #if swift(>=6.1) if #available(iOS 18.0, macOS 15.0, *) { let offerTypeString = info.offer.map { String(describing: $0.type) } offerInfo = (id: info.offer?.id, type: offerTypeString) } else { #endif #if compiler(>=5.9) let offerTypeString = info.offerType.map { String(describing: $0) } offerInfo = (id: info.offerID, type: offerTypeString) #else offerInfo = nil #endif #if swift(>=6.1) } #endif let priceIncrease: String? = { if #available(iOS 15.0, macOS 12.0, *) { return String(describing: info.priceIncreaseStatus) } return nil }() return RenewalInfoIOS( autoRenewPreference: info.autoRenewPreference, expirationReason: info.expirationReason?.rawValue.description, gracePeriodExpirationDate: info.gracePeriodExpirationDate?.milliseconds, isInBillingRetry: nil, // Not available in RenewalInfo, available in Status jsonRepresentation: nil, pendingUpgradeProductId: pendingProductId, priceIncreaseStatus: priceIncrease, renewalDate: info.renewalDate?.milliseconds, renewalOfferId: offerInfo?.id, renewalOfferType: offerInfo?.type, willAutoRenew: info.willAutoRenew ) }Additionally, consider populating
isInBillingRetryfrom thestatusobject if the API is available:// Inside the loop where you have access to status isInBillingRetry: { if #available(iOS 16.0, macOS 14.0, *) { return status.isInBillingRetry } return nil }()
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
Sources/Helpers/StoreKitTypesBridge.swift(3 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/StoreKitTypesBridge.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/StoreKitTypesBridge.swift
Sources/Helpers/**/*Store*.swift
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid using 'Store' in names for caching/helper classes; use Cache or Manager instead
Files:
Sources/Helpers/StoreKitTypesBridge.swift
🔇 Additional comments (1)
Sources/Helpers/StoreKitTypesBridge.swift (1)
73-75: LGTM! Conservative fallback approach for auto-renewal status.The implementation correctly derives
autoRenewingfromrenewalInfoIOS?.willAutoRenewwith a safefalsefallback when renewal information is unavailable. This prevents falsely reporting subscriptions as auto-renewing and aligns with the fixes from previous reviews.
Applied hyodotdev/openiap-apple#24 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * iOS subscription status cards: Upgrade Detected and Cancellation Detected with per-subscription details (current vs. upgrading product, dates, auto-renew status) and per-item renewal-info viewing. * Dedicated renewal-info view button with alert-based details. * **Improvements** * More reliable status refresh by waiting until subscriptions load. * **Style** * New visual styles for upgrade/cancellation cards and renewal-info controls. * **New Support** * Added an additional subscription product (e.g., yearly). <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Summary
Adds
renewalInfoIOSfield toPurchaseIOSto support iOS subscription upgrade/downgrade detection and auto-renewal status tracking using Apple's StoreKit 2 RenewalInfo API.Key Features
pendingUpgradeProductIdwillAutoRenewrenewalDateautoRenewPreferenceChanges
subscriptionRenewalInfo()inStoreKitTypesBridge.swiftopeniap-gql@1.2.0with RenewalInfoIOS typesOpenIapTests.swiftUsage Example
Breaking Changes
None - backwards compatible optional field.
Related
Provides upgrade/downgrade detection capability
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Tests
Chores