feat: add renewalInfo in ActiveSubscription#25
Conversation
WalkthroughSwitches subscription logic and UI to use ActiveSubscription with optional iOS renewal info; adds StoreKit renewal-info plumbing and logging; relaxes access for renewal-info bridge; detects and surfaces pending upgrades in upgrade flows and diagnostics. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant App as Example App UI
participant OpenIAP as OpenIapModule
participant Bridge as StoreKitTypesBridge
participant SK as StoreKit
User->>App: Open Subscription Screen
App->>OpenIAP: getActiveSubscriptions()
OpenIAP->>SK: fetch transactions
loop per transaction
OpenIAP->>Bridge: subscriptionRenewalInfoIOS(transaction)
Bridge->>SK: request statuses (requires subscriptionGroupID)
SK-->>Bridge: group statuses (auto‑renew, pending upgrade, ...)
Bridge-->>OpenIAP: RenewalInfoIOS?
OpenIAP->>OpenIAP: Build ActiveSubscription(with renewalInfoIOS)
end
OpenIAP-->>App: [ActiveSubscription...]
App->>App: Determine current tier / pending upgrade
App-->>User: Render UI (pending/upgrade/switch/manage)
sequenceDiagram
autonumber
actor User
participant Card as SubscriptionCard UI
participant OpenIAP as OpenIapModule
User->>Card: Tap Upgrade
Card->>OpenIAP: initiate upgrade flow
OpenIAP-->>Card: async result (success / pending / failed)
alt Pending upgrade detected
Card-->>User: Show "activates on next renewal" + pending row
else Success now
Card-->>User: Show updated plan
else Failure
Card-->>User: Show error
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 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🧬 Code graph analysis (2)Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift (2)
Example/OpenIapExample/Screens/uis/SubscriptionCard.swift (1)
🔇 Additional comments (9)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
Sources/Helpers/StoreKitTypesBridge.swift (1)
188-270: Reduce code duplication between verified and unverified cases.The logic for building
RenewalInfoIOSfrom renewal info is duplicated between the.verifiedcase (lines 189-229) and.unverifiedcase (lines 230-270). This makes maintenance harder and increases the risk of inconsistencies.Extract the renewal info mapping into a helper function:
private static func mapRenewalInfo( _ info: Product.SubscriptionInfo.RenewalInfo, transactionProductID: String ) -> RenewalInfoIOS { let pendingProductId = (info.autoRenewPreference != transactionProductID) ? 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, jsonRepresentation: nil, pendingUpgradeProductId: pendingProductId, priceIncreaseStatus: priceIncrease, renewalDate: info.renewalDate?.milliseconds, renewalOfferId: offerInfo?.id, renewalOfferType: offerInfo?.type, willAutoRenew: info.willAutoRenew ) }Then simplify both cases:
switch status.renewalInfo { case .verified(let info): return mapRenewalInfo(info, transactionProductID: transaction.productID) case .unverified(let info, _): return mapRenewalInfo(info, transactionProductID: transaction.productID) }Example/OpenIapExample/Screens/uis/SubscriptionCard.swift (1)
195-264: Consider extracting upgrade UI into separate views.The nested conditionals and button logic in the upgrade flow (lines 195-264) make this code difficult to follow. The complexity could be reduced by extracting sub-views.
Consider creating helper views:
@ViewBuilder private func upgradeActionView(upgradeInfo: UpgradeInfo) -> some View { if upgradeInfo.isPending { PendingUpgradeView(currentTier: upgradeInfo.currentTier ?? "") } else { ActiveUpgradeView( upgradeInfo: upgradeInfo, product: product, isLoading: isLoading, onSubscribe: onSubscribe ) } }This would make the main body more readable and each sub-view could be tested independently.
Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift (1)
317-323: Simplistic tier preference logic may not scale.The tier selection logic (lines 321-322) assumes yearly is always preferred over monthly. This won't work correctly if:
- User has multiple active subscriptions in different groups
- There are more than two tiers
- Tier priority isn't simply yearly > monthly
Consider using transaction date or a more explicit tier ranking:
private func getCurrentSubscription() -> ActiveSubscription? { let activeSubs = iapStore.activeSubscriptions.filter { $0.isActive } // Return the most recent subscription return activeSubs.max { $0.transactionDate < $1.transactionDate } }Or maintain an explicit tier map:
private let tierRanking: [String: Int] = [ "dev.hyo.martie.premium": 1, "dev.hyo.martie.premium_year": 2 ] private func getCurrentSubscription() -> ActiveSubscription? { let activeSubs = iapStore.activeSubscriptions.filter { $0.isActive } return activeSubs.max { (tierRanking[$0.productId] ?? 0) < (tierRanking[$1.productId] ?? 0) } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift(6 hunks)Example/OpenIapExample/Screens/uis/SubscriptionCard.swift(1 hunks)Sources/Helpers/StoreKitTypesBridge.swift(1 hunks)Sources/Models/Types.swift(1 hunks)Sources/OpenIapModule.swift(2 hunks)Sources/OpenIapStore.swift(1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
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/Models/Types.swiftSources/OpenIapStore.swiftSources/Helpers/StoreKitTypesBridge.swiftSources/OpenIapModule.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/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
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
🧬 Code graph analysis (3)
Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift (3)
Sources/OpenIapModule.swift (1)
getActiveSubscriptions(447-491)Sources/OpenIapStore.swift (1)
getActiveSubscriptions(290-304)Sources/Helpers/StoreKitTypesBridge.swift (1)
product(6-8)
Example/OpenIapExample/Screens/uis/SubscriptionCard.swift (1)
Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift (1)
product(434-436)
Sources/OpenIapModule.swift (1)
Sources/Helpers/StoreKitTypesBridge.swift (1)
subscriptionRenewalInfoIOS(173-279)
🪛 GitHub Actions: Tests
Sources/Models/Types.swift
[error] 1-1: CI step './scripts/generate-types.sh' failed with exit code 1. The diff shows removal of renewalInfoIOS from ActiveSubscription; potential breaking change in Types.swift that caused the script to fail.
🔇 Additional comments (9)
Sources/OpenIapStore.swift (1)
292-303: LGTM! Diagnostic logging is helpful.The logging for active subscriptions and renewal info provides useful visibility into subscription states, especially for pending upgrades. The implementation is read-only with no side effects.
Sources/Models/Types.swift (2)
178-180: Verify the field placement and documentation.The
renewalInfoIOSfield is positioned afterpurchaseTokenAndroid. Ensure this ordering matches the GraphQL schema definition.The documentation comment is clear and describes the field's purpose well. Verify that the GraphQL schema includes similar documentation.
Based on coding guidelines
1-4: Update GraphQL schema and regenerate types
Ensure your GraphQL schema definesrenewalInfoIOS: RenewalInfoIOSunderActiveSubscription, runnpm run generate, and commit both the schema and regenerated types file.Sources/OpenIapModule.swift (2)
468-470: LGTM! Renewal info retrieval is correctly integrated.The call to
StoreKitTypesBridge.subscriptionRenewalInfoIOS(for: transaction)is properly placed within the loop that builds active subscriptions. The async call is awaited correctly.
480-480: LGTM! RenewalInfo is passed to ActiveSubscription.The
renewalInfoparameter is correctly passed to theActiveSubscriptioninitializer, completing the integration of renewal information into the subscription data model.Sources/Helpers/StoreKitTypesBridge.swift (2)
173-173: Access level change is necessary.Changing from
privatetointernalallowsOpenIapModuleto call this method when buildingActiveSubscriptionobjects. This is the correct scope for the integration.Based on coding guidelines (iOS-specific functions MUST have IOS suffix - the function name
subscriptionRenewalInfoIOSfollows this rule correctly)
177-179: Verify subscriptionGroupID semantics across supported iOS versions
The guard returns nil fortransaction.subscriptionGroupID == nil. Per Apple (iOS 15+), all auto-renewable subscriptions belong to a group—confirm this holds for your min-iOS target or add availability checks to avoid unintentionally dropping valid renewals.Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift (2)
450-456: LGTM! Cancellation detection uses renewal info correctly.The logic correctly identifies a cancelled subscription as one that is active but has
renewalInfoIOS.willAutoRenew == false. This is the proper way to detect cancellations in StoreKit 2.
261-262: Verify UI coverage after replacing getAvailablePurchases() with getActiveSubscriptions()
Removing thegetAvailablePurchases()call may leaveiapStore.iosAvailablePurchasesunset. Confirm thatactiveSubscriptionspopulates all data used in:
- SubscriptionFlowScreen.swift (line 439)
- AvailablePurchasesScreen.swift
Summary by CodeRabbit
New Features
Improvements
Bug Fixes