Fix type mappings and enhance platform field segregation#530
Conversation
WalkthroughIntroduces OpenIAP-aligned cross-platform types and renames public models (IAPItem→IapItem, BaseProduct→ProductCommon), adds Android-specific product/purchase fields and replacementMode enums, adjusts iOS payload keys, updates docs/examples/tests, bumps version to 6.3.3, and disables markdownlint rule MD025. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor App
participant Dart as FlutterInappPurchase (Dart)
participant Channel as MethodChannel
participant Android as Android Plugin
participant iOS as iOS Plugin
rect rgb(240,248,255)
note over App,Dart: Product retrieval (OpenIAP types)
App->>Dart: requestProducts(params)
Dart->>Channel: invokeMethod(getAvailableItemsByType, params)
Channel->>Android: getProductsByType
Channel->>iOS: getAvailableItems
Android-->>Channel: JSON list (includes nameAndroid, offer details)
iOS-->>Channel: JSON list (includes environmentIOS, subscriptionGroupIdIOS)
Channel-->>Dart: JSON list
Dart-->>App: List<ProductCommon> / List<IapItem>
end
rect rgb(245,255,240)
note over App,Android: Subscription update using replacementMode
App->>Dart: requestSubscription(..., replacementModeAndroid, purchaseTokenAndroid?)
Dart->>Channel: invokeMethod(requestSubscription, args)
Channel->>Android: launchBillingFlow(args)
alt replacement requires token
Android-->>Channel: assert purchaseTokenAndroid present
end
Android-->>Channel: billing result
Channel-->>Dart: result
Dart-->>App: Future<void> result
end
rect rgb(255,248,240)
note over iOS,Dart: Purchase event normalization
iOS-->>Channel: emit purchaseUpdated (ids as String)
Channel-->>Dart: event
Dart-->>App: Purchase stream (PurchaseCommon)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
- Add subscriptionOfferDetailsAndroid field to native Android response - Fix offerId handling with proper null value support - Add platform checks for Android/iOS specific fields in Purchase conversion - Ensure dataAndroid field is properly mapped from native data
- Fix Product fromJson parsing for nameAndroid and oneTimePurchaseOfferDetailsAndroid - Fix subscription offer details structure to handle nested pricingPhases - Fix PricingPhase parsing for Android field names (formattedPrice, priceCurrencyCode, priceAmountMicros) - Add platform checks for Android/iOS suffix fields in toJson methods - Add comprehensive TODO comments for v6.4.0 deprecation cleanup - Add TypeScript-compatible field conversion extensions
- Update Android module with improved type handling - Enhance IAP type definitions for better Android/iOS compatibility - Improve error messages and handling consistency
- Update tests to match new Android field parsing behavior - Fix test expectations for displayPrice vs deprecated price field - Add release blog post for 6.3.3
- Remove lib/extensions/type_conversion.dart as it's not needed - Keep type conversion logic directly in the main type classes
- Apply dart format to all source files for consistency - Ensure CI format checks will pass - All tests verified to pass after formatting
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #530 +/- ##
==========================================
+ Coverage 27.05% 36.77% +9.72%
==========================================
Files 6 6
Lines 1582 2284 +702
==========================================
+ Hits 428 840 +412
- Misses 1154 1444 +290 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- Add events_test.dart: 100% coverage for events classes (7/7 lines) - Add errors_test.dart: improved coverage from 36% to 81% (47/58 lines) - Add enums_test.dart: comprehensive enum testing for better coverage - All new tests pass and maintain existing functionality - Significantly improved overall test coverage metrics
- Add platform-specific types testing in types_platform_test.dart - Add OpenIAP type alignment verification tests - Test Product/Subscription toJson platform segregation - Test Purchase toString platform-specific field display - Test OfferDetail/PricingPhase creation and JSON conversion - Test DiscountIOS complete functionality - Verify Purchase id/ids getters work correctly - Add comprehensive type alignment tests with TypeScript OpenIAP spec - Test platform enum representations and error code alignment - Ensure all types match OpenIAP specification requirements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (22)
docs/docs/api/types/product-type.md (2)
68-84: Fix constructor/field mismatch for Product.type and align platform enum name.Field is non-nullable but constructor accepts a nullable type; also enum name should follow Iap naming used elsewhere.
Apply this diff within the snippet to make type default to "inapp" and use IapPlatform:
class Product extends BaseProduct { - final String type; + final String type; final bool? isFamilyShareable; Product({ required String productId, required String price, String? currency, String? localizedPrice, String? title, String? description, - required IAPPlatform platform, - String? type, + required IapPlatform platform, + this.type = 'inapp', this.isFamilyShareable, }); }Additionally (outside this selected range), update BaseProduct’s platform type to IapPlatform for consistency.
abstract class BaseProduct { // ... - final IAPPlatform platform; + final IapPlatform platform; }
250-282: Unify platform enum naming in conversion snippet (IAPPlatform → IapPlatform).Docs should mirror the public Dart API and naming guidelines.
Apply this diff inside the conversion snippet:
- platform: Platform.isIOS ? IAPPlatform.ios : IAPPlatform.android, + platform: Platform.isIOS ? IapPlatform.ios : IapPlatform.android,…and similarly for the non-subs branch:
- platform: Platform.isIOS ? IAPPlatform.ios : IAPPlatform.android, + platform: Platform.isIOS ? IapPlatform.ios : IapPlatform.android,Note: If the sample is standalone, add
import 'dart:io' show Platform;to avoid confusion.docs/docs/guides/purchases.md (1)
649-666: Fix: use instance stream access in Complete ExampleThe example uses FlutterInappPurchase.purchaseUpdated and .purchaseError statically, whereas other snippets use the instance. Keep it consistent and correct.
- _purchaseUpdatedSubscription = FlutterInappPurchase.purchaseUpdated.listen( + _purchaseUpdatedSubscription = _iap.purchaseUpdated.listen( (purchasedItem) { if (purchasedItem != null) { _handlePurchaseSuccess(purchasedItem); } }, ); - _purchaseErrorSubscription = FlutterInappPurchase.purchaseError.listen( + _purchaseErrorSubscription = _iap.purchaseError.listen( (purchaseError) { if (purchaseError != null) { _handlePurchaseError(purchaseError); } }, );docs/docs/guides/subscriptions.md (1)
579-586: Dart map key must be quoted; current example doesn’t compileofferToken is used as an identifier rather than a string key.
- subscriptionOffers: [{offerToken: offerIndex.toString()}], + subscriptionOffers: [ + {'offerToken': offerIndex.toString()} + ],If a typed model is available (e.g., AndroidSubscriptionOffer), consider using that type for stronger guarantees.
docs/docs/faq.md (2)
164-172: Fix Android PurchaseState mapping in example (completed vs pending are swapped).In Google Play Billing, PURCHASED=1 and PENDING=2. The current snippet treats 0 as completed and 1 as pending, which is incorrect and will mislead app logic.
Apply this diff to correct the example:
void _handlePurchaseUpdate(PurchasedItem item) { - if (item.purchaseStateAndroid == 0) { - // Purchase completed + if (item.purchaseStateAndroid == 1) { + // Purchase completed _deliverProduct(item); - } else if (item.purchaseStateAndroid == 1) { - // Purchase pending - show pending UI + } else if (item.purchaseStateAndroid == 2) { + // Purchase pending - show pending UI _showPendingMessage(); } }
46-46: Fix broken link in FAQ
The link in docs/docs/faq.md points to./getting-started/setup-ios, but the actual file is namedios-setup.md. Update the link to match the file name:• In docs/docs/faq.md (line 46), change
[setup guides](./getting-started/setup-ios)
to
[setup guides](./getting-started/ios-setup)docs/docs/getting-started/quickstart.md (1)
132-142: Update Android consumption logic in Quickstart docsThe
PurchasedItemclass no longer hasisConsumableAndroidorpurchaseTokenAndroidproperties. You need to:
- Use the unified
purchaseTokenfield onPurchasedItem.- Maintain your own list (or set) of consumable
productIds to decide when to callconsumePurchase.- Fall back to
acknowledgePurchasefor non-consumable SKUs.For example, in
docs/docs/getting-started/quickstart.md(around lines 132–142), replace:if (Platform.isIOS) { await FlutterInappPurchase.instance.finishTransaction(productItem); } else if (productItem.isConsumableAndroid ?? false) { await FlutterInappPurchase.instance.consumePurchase( purchaseToken: productItem.purchaseTokenAndroid!, ); } else { await FlutterInappPurchase.instance.acknowledgePurchase( purchaseToken: productItem.purchaseTokenAndroid!, ); }with something like:
// Define this in your app: final Set<String> consumableProductIds = {'coin_pack', 'gem_bundle'}; if (Platform.isIOS) { await FlutterInappPurchase.instance.finishTransaction(productItem); } else { final token = productItem.purchaseToken!; if (consumableProductIds.contains(productItem.productId)) { await FlutterInappPurchase.instance.consumePurchase( purchaseToken: token, ); } else { await FlutterInappPurchase.instance.acknowledgePurchase( purchaseToken: token, ); } }This ensures you’re using the correct
purchaseTokenfield and explicitly handling consumable SKUs.android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (1)
630-706: Replacement mode mapping is incorrect and one branch won’t compile.Issues:
- when subject is an Int, but one case matches an enum constant (type mismatch, won’t compile).
- Integer mappings are wrong: values 1/2 are swapped and 1 should be WITH_TIME_PRORATION, 2 should be CHARGE_PRORATED_PRICE, 3 should be WITHOUT_PRORATION.
Apply this diff to fix mapping and remove the invalid enum case in when:
- when (replacementMode) { - -1 -> {} //ignore - no replacement mode specified - BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE -> { + when (replacementMode) { + -1 -> { /* ignore - no replacement mode specified */ } + 2 -> { // CHARGE_PRORATED_PRICE // For subscription replacement, purchaseToken is required if (purchaseToken.isNullOrEmpty()) { safeChannel.error( TAG, "buyItemByType", "Old SKU purchase information(token/id) or original external transaction id must be provided for subscription replacement." ) return } params.setOldPurchaseToken(purchaseToken) .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE) builder.setSubscriptionUpdateParams(params.build()) - if (type != BillingClient.ProductType.SUBS) { + if (type != BillingClient.ProductType.SUBS) { safeChannel.error( TAG, "buyItemByType", - "IMMEDIATE_AND_CHARGE_PRORATED_PRICE for proration mode only works in subscription purchase." + "CHARGE_PRORATED_PRICE replacement mode only works with subscriptions." ) return } } - 1 -> { // IMMEDIATE_WITHOUT_PRORATION + 3 -> { // WITHOUT_PRORATION if (purchaseToken.isNullOrEmpty()) { safeChannel.error( TAG, "buyItemByType", "Old SKU purchase information(token/id) or original external transaction id must be provided for subscription replacement." ) return } params.setOldPurchaseToken(purchaseToken) .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION) builder.setSubscriptionUpdateParams(params.build()) } 4 -> { // DEFERRED if (purchaseToken.isNullOrEmpty()) { safeChannel.error( TAG, "buyItemByType", "Old SKU purchase information(token/id) or original external transaction id must be provided for deferred subscription replacement." ) return } params.setOldPurchaseToken(purchaseToken) .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.DEFERRED) builder.setSubscriptionUpdateParams(params.build()) } - 2 -> { // IMMEDIATE_WITH_TIME_PRORATION + 1 -> { // WITH_TIME_PRORATION if (purchaseToken.isNullOrEmpty()) { safeChannel.error( TAG, "buyItemByType", "Old SKU purchase information(token/id) or original external transaction id must be provided for subscription replacement." ) return } params.setOldPurchaseToken(purchaseToken) .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION) builder.setSubscriptionUpdateParams(params.build()) } 5 -> { // IMMEDIATE_AND_CHARGE_FULL_PRICE if (purchaseToken.isNullOrEmpty()) { safeChannel.error( TAG, "buyItemByType", "Old SKU purchase information(token/id) or original external transaction id must be provided for subscription replacement." ) return } params.setOldPurchaseToken(purchaseToken) .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE) builder.setSubscriptionUpdateParams(params.build()) } else -> { - // For any other proration mode, also require purchase token - if (!purchaseToken.isNullOrEmpty()) { - params.setOldPurchaseToken(purchaseToken) - .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.UNKNOWN_REPLACEMENT_MODE) - builder.setSubscriptionUpdateParams(params.build()) - } + // Unknown replacement mode: do not set subscription update params } }docs/docs/migration/from-v5.md (1)
252-291: Correct type names in the “Before (v5.x)” example block.The “Before” block uses IapItem; it should use IAPItem to match v5.x.
Apply this diff:
- List<IapItem> _items = []; + List<IAPItem> _items = []; @@ - void _requestPurchase(IapItem item) { + void _requestPurchase(IAPItem item) { FlutterInappPurchase.instance.requestPurchase(item.productId); } @@ - Future _getProduct() async { - List<IapItem> items = await FlutterInappPurchase.instance.requestProducts(skus: _kProductIds, type: 'inapp'); + Future _getProduct() async { + List<IAPItem> items = await FlutterInappPurchase.instance.requestProducts(skus: _kProductIds, type: 'inapp'); setState(() { _items = items; }); }lib/types.dart (1)
2965-2979: priceCurrencyCode must be String in PricingPhaseAndroid (currently typed int).Google Play Billing returns a currency code string (e.g., "USD"). Using int here is incorrect and will throw at runtime.
Apply this diff:
class PricingPhaseAndroid { final String formattedPrice; - final int priceCurrencyCode; + final String priceCurrencyCode; final String billingPeriod; final int? recurrenceMode; final int billingCycleCount; final int priceAmountMicros; PricingPhaseAndroid.fromJSON(Map<String, dynamic> json) : formattedPrice = json['formattedPrice'] as String, - priceCurrencyCode = json['priceCurrencyCode'] as int, + priceCurrencyCode = json['priceCurrencyCode'] as String, billingPeriod = json['billingPeriod'] as String, recurrenceMode = json['recurrenceMode'] as int?, billingCycleCount = json['billingCycleCount'] as int, priceAmountMicros = json['priceAmountMicros'] as int;docs/docs/api/types.md (3)
94-126: Docs: IapItem.originalPrice type mismatches code (should be nullable).In lib/types.dart, IapItem.originalPrice is String?. Update the docs to reflect nullability to prevent confusion for integrators.
Apply this diff in the code block:
- final String originalPrice; // Original price + final String? originalPrice; // Original price
306-310: Use IapPlatform (not IAPPlatform) to match the codebase and naming guidelines.Per repository conventions and actual enum name, the correct casing is IapPlatform.
Apply this diff:
-enum IAPPlatform { +enum IapPlatform { ios, // iOS platform android // Android platform }
50-74: RequestPurchaseAndroid example is out of sync with the current API.The docs show purchaseToken, offerTokenIndex, prorationMode on RequestPurchaseAndroid, but the current API defines skus, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid, isOfferPersonalized. Move replacement/upgrade options to subscription-specific types in the docs to match code.
I can prepare a corrected code snippet that mirrors AndroidRequestPurchaseProps/RequestPurchaseAndroid and the subscription variants; let me know if you want me to patch the docs file.
example/lib/storekit2_demo.dart (1)
280-283: Guard against null productId before invoking _buyProduct.productId is nullable on IapItem; the null-assertion may crash. Disable the button if productId is null.
Apply:
- onPressed: - _loading ? null : () => _buyProduct(product.productId!), + onPressed: _loading || product.productId == null + ? null + : () => _buyProduct(product.productId!),docs/docs/getting-started/ios-setup.md (1)
203-209: Dispose: use endConnection() instead of finishTransaction()finishTransaction() requires a purchase object; calling it in dispose is incorrect. Use endConnection() to close the billing connection.
Apply this diff:
@override void dispose() { _purchaseUpdatedSubscription.cancel(); _purchaseErrorSubscription.cancel(); - FlutterInappPurchase.instance.finishTransaction(); + FlutterInappPurchase.instance.endConnection(); super.dispose(); }docs/docs/getting-started/android-setup.md (1)
238-244: Dispose: use endConnection() instead of finishTransaction()Same rationale as iOS: finishTransaction() needs a purchase and is not a teardown API.
Apply this diff:
@override void dispose() { _purchaseUpdatedSubscription.cancel(); _purchaseErrorSubscription.cancel(); - FlutterInappPurchase.instance.finishTransaction(); + FlutterInappPurchase.instance.endConnection(); super.dispose(); }docs/docs/guides/products.md (1)
153-166: Fix Android purchase state mapping in documentationThe validation example in docs/docs/guides/products.md currently assumes
“0 = Purchased, 1 = Pending,” but in the code (and tests) Android states are:
- 0 = UNSPECIFIED_STATE
- 1 = PURCHASED
- 2 = PENDING
Please update the example accordingly:
• File: docs/docs/guides/products.md (around lines 153–166)
// Check purchase state (Android) if (Platform.isAndroid) { - // 0 = Purchased, 1 = Pending - if (item.purchaseStateAndroid != 0) { + // 1 = Purchased, 2 = Pending + if (item.purchaseStateAndroid != 1) { return false; } }docs/docs/api/classes/iap-item.md (1)
176-182: Use requestProducts in the usage example to match the API shown elsewhereOther pages recommend requestProducts; this one uses getProducts. Standardize on a single API in docs to avoid confusing readers.
-// Fetching products -List<IapItem> products = await FlutterInappPurchase.instance.getProducts([ +// Fetching products +List<IapItem> products = await FlutterInappPurchase.instance.requestProducts( + skus: [ 'com.example.premium', 'com.example.pro', -]); +], + type: 'inapp', +);docs/docs/api/overview.md (2)
278-293: Use IOS when iOS is the final suffixvalidateReceiptIos ends with iOS as the final suffix; per your naming conventions it should be validateReceiptIOS.
-Future<Map<String, dynamic>?> validateReceiptIos({ +Future<Map<String, dynamic>?> validateReceiptIOS({ required String receiptBody, bool isTest = true, }) async
312-321: Update conceptual error‐class name in docsIn
docs/docs/api/overview.md(lines 312–321), the conceptual class for error codes should useIapError(rather thanIAPError) to match our guideline of using “Iap” when the acronym is not the final suffix. Please update this block accordingly:Files needing change:
• docs/docs/api/overview.md (lines 312–321)Suggested diff:
-class IAPError { +class IapError { static const String E_UNKNOWN = 'E_UNKNOWN'; static const String E_USER_CANCELLED = 'E_USER_CANCELLED'; static const String E_NETWORK = 'E_NETWORK'; static const String E_ITEM_UNAVAILABLE = 'E_ITEM_UNAVAILABLE'; static const String E_REMOTE_ERROR = 'E_REMOTE_ERROR'; static const String E_NOT_PREPARED = 'E_NOT_PREPARED'; static const String E_ALREADY_OWNED = 'E_ALREADY_OWNED'; }docs/docs/api/flutter-inapp-purchase.md (1)
150-158: Fix incorrect request type names in example (Ios/Android props vs actual public types).The example uses RequestPurchaseIosProps/RequestPurchaseAndroidProps, which don’t match the public types used elsewhere (e.g., tests use RequestPurchaseIOS/RequestPurchaseAndroid). This will confuse users and cause copy-paste errors.
Apply this diff to align with the public API and naming guidelines (Ios when not final; IOS when final suffix):
await FlutterInappPurchase.instance.requestPurchase( request: RequestPurchase( - ios: RequestPurchaseIosProps(sku: 'premium_upgrade'), - android: RequestPurchaseAndroidProps(skus: ['premium_upgrade']), + ios: RequestPurchaseIOS(sku: 'premium_upgrade'), + android: RequestPurchaseAndroid(skus: ['premium_upgrade']), ), type: PurchaseType.inapp, );docs/docs/examples/subscription-store.md (1)
137-140: Fix handler type for purchaseError stream.purchaseError emits PurchaseResult?, but the handler signature is PurchasedItem?. Update to prevent runtime/type confusion and match other docs.
- void _handlePurchaseError(PurchasedItem? item) { - // Handle purchase errors - _showError('Purchase failed'); - } + void _handlePurchaseError(PurchaseResult? error) { + if (error == null) return; + // Optionally branch on error.code here (e.g., ErrorCode.eUserCancelled) + _showError('Purchase failed: ${error.message}'); + }
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
Outdated
Show resolved
Hide resolved
Add comprehensive extensions for converting Product, Subscription, and Purchase
objects to OpenIAP/expo-iap compatible format with platform-specific field handling:
**Product Extension:**
- `toOpenIapFormat()` - converts to OpenIAP standard with proper type mapping
- `toExpoIapFormat()` - legacy alias for expo-iap compatibility
- Platform-specific field filtering (removes opposite platform fields)
- iOS native type conversion (consumable → inapp, autoRenewable → subs)
**Subscription Extension:**
- Full OpenIAP format conversion with 'subs' type enforcement
- Platform segregation for Android/iOS fields
- Proper handling of offer details and discount structures
**Purchase Extension:**
- OpenIAP id/ids field mapping (transaction identifier → id, productId → ids)
- Platform-specific field preservation and filtering
- Manual JSON building to avoid method resolution issues
**Features:**
- Suffix-based field filtering (.endsWith('Android'), .endsWith('IOS'))
- Type-safe conversion with null safety
- Comprehensive test coverage (11 test cases)
- Legacy compatibility with expo-iap format
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Apply dart format fixes to improve code consistency: - Fix line breaks in expect statements - Improve readability of nested map/list expectations - Maintain consistent indentation for multiline expressions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
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)
lib/types.dart (1)
1-3300: Replace riskyas double?casts with safe num-to-double conversionChange every instance of
json['field'] as double?to
(json['field'] as num?)?.toDouble()Affected locations:
- lib/flutter_inapp_purchase.dart
- line 716: originalPriceAmount
- line 780: originalPriceAmount
- lib/types.dart
- line 305: originalPriceAmount
- line 812: originalPriceAmount
- line 1259: originalPriceAmount
- line 1728: priceIOS
♻️ Duplicate comments (4)
lib/types.dart (4)
354-356: Fix null-assertion crash in Product.toString description truncationdescription! is evaluated inside the substring length calculation even when description is null, causing a runtime crash.
Apply this diff:
- buffer.writeln( - ' description: ${description?.substring(0, description!.length > 100 ? 100 : description!.length)}${description != null && description!.length > 100 ? '...' : ''},', - ); + if (description != null) { + final desc = description!; + final short = desc.length > 100 ? '${desc.substring(0, 100)}...' : desc; + buffer.writeln(' description: $short,'); + } else { + buffer.writeln(' description: null,'); + }
304-306: Avoid double-only casts in JSON parsing (int values will throw)originalPriceAmount uses
as double?which fails when the JSON value is an int. Use num?.toDouble() to safely handle both.Apply this diff:
- originalPriceAmount: json['originalPriceAmount'] as double?, + originalPriceAmount: (json['originalPriceAmount'] as num?)?.toDouble(),
812-813: Same issue: Subscription.fromJson originalPriceAmount should accept int/doubleUse num?.toDouble() to avoid runtime type errors.
- originalPriceAmount: json['originalPriceAmount'] as double?, + originalPriceAmount: (json['originalPriceAmount'] as num?)?.toDouble(),
1259-1261: Same issue: ProductAndroid.fromJson originalPriceAmount should accept int/doubleUse num?.toDouble() here as well.
- originalPriceAmount: json['originalPriceAmount'] as double?, + originalPriceAmount: (json['originalPriceAmount'] as num?)?.toDouble(),
🧹 Nitpick comments (2)
lib/types.dart (2)
3499-3501: Nit: Set platform once; avoid redundant default then override in toOpenIapFormatjson is initially seeded with platform.toString() and then overridden per-branch to 'ios'/'android'. Assign once in-branch to reduce ambiguity in logs and map diffs.
- final json = <String, dynamic>{ + final json = <String, dynamic>{ 'productId': productId, 'transactionId': transactionId, 'transactionDate': transactionDate, 'transactionReceipt': transactionReceipt, 'purchaseToken': purchaseToken, 'orderId': orderId, 'packageName': packageName, 'purchaseState': purchaseState?.toString(), - 'isAcknowledged': isAcknowledged, - 'autoRenewing': autoRenewing, - 'platform': platform.toString(), + 'isAcknowledged': isAcknowledged, + 'autoRenewing': autoRenewing, }; @@ - if (isIOS) { + if (isIOS) { json.addAll({ - 'platform': 'ios', + 'platform': 'ios', 'id': id, 'ids': ids, 'quantityIOS': quantityIOS ?? 1, 'originalTransactionDateIOS': originalTransactionDateIOS, 'originalTransactionIdentifierIOS': originalTransactionIdentifierIOS, 'environmentIOS': environmentIOS ?? 'Production', 'currencyCodeIOS': currencyCodeIOS, 'priceIOS': priceIOS, 'appBundleIdIOS': appBundleIdIOS, 'productTypeIOS': productTypeIOS, 'transactionReasonIOS': transactionReasonIOS, 'webOrderLineItemIdIOS': webOrderLineItemIdIOS, 'subscriptionGroupIdIOS': subscriptionGroupIdIOS, }); json.removeWhere((key, value) => key.endsWith('Android')); } else { - // Android + // Android json.addAll({ - 'platform': 'android', + 'platform': 'android', 'id': id, 'ids': ids, 'dataAndroid': dataAndroid, 'signatureAndroid': signatureAndroid, 'purchaseStateAndroid': purchaseStateAndroid, 'isAcknowledgedAndroid': isAcknowledgedAndroid ?? false, 'packageNameAndroid': packageNameAndroid, 'obfuscatedAccountIdAndroid': obfuscatedAccountIdAndroid, 'obfuscatedProfileIdAndroid': obfuscatedProfileIdAndroid, }); json.removeWhere((key, value) => key.endsWith('IOS')); }Also applies to: 3506-3511, 3527-3544
1835-1841: Deduplicate printed iOS fields in Purchase.toStringstoreFrontCountryCodeIOS (and a few others) are printed twice, adding noise and potential confusion in logs. Keep a single occurrence.
- if (storeFrontCountryCodeIOS != null) { - buffer.writeln( - ' storefrontCountryCodeIOS: "$storeFrontCountryCodeIOS",', - ); - } @@ - if (storeFrontCountryCodeIOS != null) { - buffer.writeln( - ' storeFrontCountryCodeIOS: $storeFrontCountryCodeIOS,', - ); - } + if (storeFrontCountryCodeIOS != null) { + buffer.writeln( + ' storefrontCountryCodeIOS: "$storeFrontCountryCodeIOS",', + ); + }If desired, we can similarly prune duplicate prints for productTypeIOS, subscriptionGroupIdIOS, currencyCodeIOS, and expirationDateIOS.
Also applies to: 1863-1867
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
lib/types.dart(48 hunks)test/extensions_test.dart(1 hunks)test/openiap_type_alignment_test.dart(1 hunks)test/types_platform_test.dart(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.dart
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.dart: For iOS-related code, when iOS is the final suffix, use the IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
For iOS-related code, when iOS is not the final suffix, use Ios (e.g., IosManager, IosHelper)
For Android-related code, use the Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)
These platform/IAP naming conventions apply to both functions and types
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Follow OpenIAP terminology for API method naming
Do not use generic prefixes like get or find; use official OpenIAP terminology instead
Files:
test/openiap_type_alignment_test.darttest/extensions_test.darttest/types_platform_test.dartlib/types.dart
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : Follow OpenIAP terminology for API method naming
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : These platform/IAP naming conventions apply to both functions and types
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For Android-related code, use the Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For iOS-related code, when iOS is the final suffix, use the IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : These platform/IAP naming conventions apply to both functions and types
Applied to files:
test/openiap_type_alignment_test.darttest/types_platform_test.dartlib/types.dart
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : Follow OpenIAP terminology for API method naming
Applied to files:
test/openiap_type_alignment_test.dartlib/types.dart
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For Android-related code, use the Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
Applied to files:
test/openiap_type_alignment_test.darttest/extensions_test.darttest/types_platform_test.dartlib/types.dart
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)
Applied to files:
test/openiap_type_alignment_test.dartlib/types.dart
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For iOS-related code, when iOS is the final suffix, use the IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
Applied to files:
test/types_platform_test.dartlib/types.dart
🔇 Additional comments (19)
test/openiap_type_alignment_test.dart (6)
7-17: OpenIAP Product id mapping and core fields — LGTMThe id/productId aliasing and required property validations align with the OpenIAP spec. Good separation of platform via platformEnum.
40-57: Android Product platform fields exercise is correctValidates nameAndroid and oneTimePurchaseOfferDetailsAndroid presence and types only when platform is android. Matches the library contract.
76-118: Subscription offer details structure coverage — solidThe nested OfferDetail and PricingPhase structure and types (including RecurrenceMode) are asserted properly. This guards against the most common Android parsing regressions.
121-154: Purchase id/ids semantics and Android-specific fields — LGTMid mapped to transactionId and ids to [productId] conforms to OpenIAP. Android-specific fields are validated for presence and types as expected.
185-234: OfferDetail → JSON structure check prevents TS shape regressionsThe test ensures pricingPhases is an object with pricingPhaseList and that priceAmountMicros is emitted as String. This is exactly what downstream TypeScript expects.
265-281: Enums and platform string representation checks — LGTMProductType string constants and RecurrenceMode coverage mirror the library’s implementation.
test/extensions_test.dart (5)
7-29: Android Product toOpenIapFormat filtering is correctIncludes only Android fields and drops iOS suffix fields. The assertions mirror the intended segregation logic with removeWhere on suffixes.
31-51: iOS Product toOpenIapFormat filtering is correctIncludes only iOS fields and removes Android suffix fields. Alias parity via toExpoIapFormat is a good guard.
67-99: Android Subscription toOpenIapFormat — LGTMEnsures platform: 'android', type: 'subs', and serializes subscriptionOfferDetailsAndroid. Properly excludes iOS fields.
123-149: Android Purchase toOpenIapFormat — LGTMValidates id/ids and key Android details (dataAndroid, signatureAndroid, purchaseStateAndroid, isAcknowledgedAndroid). Correctly omits iOS fields.
208-243: Platform field filtering tests cover mixed-field inputs wellAsserts removal of opposite-platform fields when platformEnum dictates a platform. This protects against accidental leakage in toOpenIapFormat.
test/types_platform_test.dart (5)
7-31: Product.toJson platform segregation — LGTMAndroid-only fields present, iOS-only fields absent when platform is android. The reverse is also covered below for iOS.
33-55: iOS Product.toJson platform segregation — LGTMiOS fields included and Android fields excluded as expected. Good coverage of promotionalOfferIdsIOS as well.
96-139: OfferDetail and PricingPhase JSON roundtrip shape — LGTMtoJson creates pricingPhases.pricingPhaseList and fromJson supports both list and nested object formats. This is important for Android payload compatibility.
182-218: Purchase.toString Android-only visibility — LGTMAndroid-specific attributes show up for Android and iOS fields do not leak. This helps when debugging logs.
219-269: Purchase.toString iOS-only visibility — LGTMCovers a wide set of iOS fields and ensures Android fields are absent. The test string expectations match the library formatting.
lib/types.dart (3)
613-668: Naming and platform segregation align with guidelines — LGTM
- iOS final-suffix types/fields use IOS (PurchaseIOS, DiscountIOS, etc.).
- Non-final suffix uses Ios (IosRequestPurchaseProps).
- Android types/fields use Android suffix consistently.
- IAP term uses Iap, not IAP (IapItem).
Extensions correctly filter opposite-platform fields using suffix-based removal.
Also applies to: 670-764, 1113-1196, 1198-1271, 1361-1420, 1449-1514, 3399-3438, 3440-3481
248-343: OfferDetail/PricingPhase handling and Purchase parsing — LGTM with minor suggestions applied elsewhere
- OfferDetail.fromJson supports both legacy list and nested pricingPhaseList.
- PricingPhase.fromJson/toJson handle micros and string/number variations correctly.
- Purchase.fromJson covers string/int dates and nests VerificationResult safely.
Once the numeric cast fixes (priceIOS, originalPriceAmount) are applied, these blocks look solid.
Also applies to: 766-850, 1670-1769
1-7: Public exports and getCurrentPlatform wiring — LGTMRe-exports and getCurrentPlatform usage keep platform derivation centralized in enums/errors.
…ode mapping (1=WITH_TIME_PRORATION, 2=CHARGE_PRORATED_PRICE, 3=WITHOUT_PRORATION) and fix PricingPhaseAndroid.priceCurrencyCode type to String
… Android offer details handling, update API examples to v6 format, fix documentation links, use PurchaseType enum, correct v5.x type names in migration docs, and remove unnecessary ignore print directives
- Fix priceIOS type casting to handle num type properly - Fix description truncation null-assertion bug - Ensure all CodeRabbit suggestions are properly applied
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
ios/Classes/FlutterInappPurchasePlugin.swift (4)
180-201: Coalesce optionals and stringify webOrderLineItemIdIOS to prevent JSONSerialization failures and Dart cast errorsYou're inserting optionals into the JSON payload (e.g., appAccountTokenIOS, webOrderLineItemIdIOS). JSONSerialization will throw if Optionals are present. Also, Dart expects webOrderLineItemIdIOS as String? (see lib/types.dart), but you're emitting a number.
Apply this diff to avoid Optionals in the event dictionary and align types:
- var event: [String: Any] = [ + var event: [String: Any] = [ "id": transactionId as String, // Ensure String type for OpenIAP compliance "productId": transaction.productID, "transactionId": transactionId as String, // Ensure String type "transactionDate": transaction.purchaseDate.timeIntervalSince1970 * 1000, "transactionReceipt": transaction.jsonRepresentation.base64EncodedString(), "purchaseToken": jwsRepresentation, // Unified field for iOS JWS and Android purchaseToken // TODO(v6.4.0): Remove deprecated jwsRepresentationIOS "jwsRepresentationIOS": jwsRepresentation, // Deprecated - use purchaseToken - will be removed in v6.4.0 "platform": "ios", "transactionState": getTransactionState(transaction), "quantityIOS": transaction.purchasedQuantity, "originalTransactionDateIOS": transaction.originalPurchaseDate.timeIntervalSince1970 * 1000, "originalTransactionIdentifierIOS": String(transaction.originalID), - "appAccountTokenIOS": transaction.appAccountToken?.uuidString, + // set below if present to avoid Optional in dictionary "appBundleIdIOS": transaction.appBundleID, "productTypeIOS": transaction.productType.rawValue, "subscriptionGroupIdIOS": transaction.subscriptionGroupID ?? "", "isUpgradedIOS": transaction.isUpgraded, "ownershipTypeIOS": transaction.ownershipType.rawValue, - "webOrderLineItemIdIOS": transaction.webOrderLineItemID + // set below if present to avoid Optional in dictionary ] + + if let token = transaction.appAccountToken?.uuidString { + event["appAccountTokenIOS"] = token + } + if let webOrderId = transaction.webOrderLineItemID { + event["webOrderLineItemIdIOS"] = String(webOrderId) + }
231-241: Unify expirationDateIOS/revocationDateIOS to ISO 8601 strings (Dart expects String, not number)In processTransaction you emit expirationDateIOS/revocationDateIOS as epoch millis, while getAvailableItems emits ISO strings. Purchase.fromJson parses String -> DateTime. Emit ISO strings here to avoid runtime cast errors when decoding purchase-updated events.
- if let expirationDate = transaction.expirationDate { - event["expirationDate"] = expirationDate.timeIntervalSince1970 * 1000 - event["expirationDateIOS"] = expirationDate.timeIntervalSince1970 * 1000 - } + if let expirationDate = transaction.expirationDate { + event["expirationDate"] = expirationDate.timeIntervalSince1970 * 1000 + let f = ISO8601DateFormatter() + event["expirationDateIOS"] = f.string(from: expirationDate) + } - if let revocationDate = transaction.revocationDate { - event["revocationDate"] = revocationDate.timeIntervalSince1970 * 1000 - event["revocationDateIOS"] = revocationDate.timeIntervalSince1970 * 1000 - event["revocationReason"] = transaction.revocationReason?.rawValue - event["revocationReasonIOS"] = transaction.revocationReason?.rawValue - } + if let revocationDate = transaction.revocationDate { + event["revocationDate"] = revocationDate.timeIntervalSince1970 * 1000 + let f = ISO8601DateFormatter() + event["revocationDateIOS"] = f.string(from: revocationDate) + event["revocationReason"] = transaction.revocationReason?.rawValue ?? "" + event["revocationReasonIOS"] = transaction.revocationReason?.rawValue ?? "" + }
391-413: Avoid optionals in getAvailableItems payload and stringify webOrderLineItemIdIOSSame Optional insertion issue as above; also convert webOrderLineItemIdIOS to String to match Dart.
- var purchase: [String: Any] = [ + var purchase: [String: Any] = [ "id": transactionId as String, // Ensure String type "productId": transaction.productID, "transactionId": transactionId as String, // Ensure String type "transactionDate": transaction.purchaseDate.timeIntervalSince1970 * 1000, "transactionReceipt": transaction.jsonRepresentation.base64EncodedString(), "purchaseToken": verificationResult.jwsRepresentation, // JWS for server validation (iOS 15+) // TODO(v6.4.0): Remove deprecated jwsRepresentationIOS "jwsRepresentationIOS": verificationResult.jwsRepresentation, // Deprecated - use purchaseToken - will be removed in v6.4.0 "platform": "ios", // Add iOS-specific fields "quantityIOS": transaction.purchasedQuantity, "originalTransactionDateIOS": transaction.originalPurchaseDate.timeIntervalSince1970 * 1000, "originalTransactionIdentifierIOS": String(transaction.originalID), - "appAccountTokenIOS": transaction.appAccountToken?.uuidString, + // set below if present to avoid Optional in dictionary "appBundleIdIOS": transaction.appBundleID, "productTypeIOS": transaction.productType.rawValue, "subscriptionGroupIdIOS": transaction.subscriptionGroupID ?? "", "isUpgradedIOS": transaction.isUpgraded, "ownershipTypeIOS": transaction.ownershipType.rawValue, - "webOrderLineItemIdIOS": transaction.webOrderLineItemID + // set below if present to avoid Optional in dictionary ] + + if let token = transaction.appAccountToken?.uuidString { + purchase["appAccountTokenIOS"] = token + } + if let webOrderId = transaction.webOrderLineItemID { + purchase["webOrderLineItemIdIOS"] = String(webOrderId) + }
300-305: Don’t insert Optional into productInfo (subscriptionGroupIdIOS)This assigns an Optional directly into a [String: Any] dictionary. StandardMethodCodec won’t encode Optionals; this can crash or strip the value. Guard and insert only when non-nil.
- productInfo["subscriptionGroupIdIOS"] = subscription.subscriptionGroupID + if let groupId = subscription.subscriptionGroupID { + productInfo["subscriptionGroupIdIOS"] = groupId + }
♻️ Duplicate comments (6)
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (1)
475-490: Don’t overwrite numeric price with formatted displayPrice; avoid unreliable parsing.Overwriting item["price"] with a human-readable value breaks consumers expecting a numeric string and makes your parsing return 0. This was flagged previously; same guidance applies here.
Apply:
- } else { - // Even if oneTimePurchaseOfferDetails is null, set price from displayPrice - item.put("price", item.getString("displayPrice")) - Log.d("InappPurchase", "oneTimePurchaseOfferDetails is null for INAPP product") - - // Create a basic offer details from available data - if (currency != null && displayPrice != null) { + } else { + Log.d(TAG, "oneTimePurchaseOfferDetails is null for INAPP product") + // Create a basic offer details from available data (display-only) + if (currency != "Unknown" && displayPrice != "N/A") { val basicOfferDetails = JSONObject() basicOfferDetails.put("priceCurrencyCode", currency) basicOfferDetails.put("formattedPrice", displayPrice) - // Try to extract price amount from the price field if available - val priceString = item.optString("price", "0") - val priceValue = priceString.toDoubleOrNull() ?: 0.0 - basicOfferDetails.put("priceAmountMicros", (priceValue * 1000000).toLong().toString()) - item.put("oneTimePurchaseOfferDetailsAndroid", basicOfferDetails) - Log.d("InappPurchase", "Created basic oneTimePurchaseOfferDetailsAndroid: $basicOfferDetails") + basicOfferDetails.put("priceAmountMicros", JSONObject.NULL) // unknown + item.put("oneTimePurchaseOfferDetailsAndroid", basicOfferDetails) + item.put("oneTimePurchaseOfferDetails", basicOfferDetails) // backward compatibility + Log.d(TAG, "Created basic oneTimePurchaseOfferDetailsAndroid: $basicOfferDetails") } }lib/types.dart (5)
304-306: Parse numeric JSON via num?.toDouble() to avoid int/double cast crashes (originalPriceAmount)Direct cast with as double? will throw when JSON provides an int.
- originalPriceAmount: json['originalPriceAmount'] as double?, + originalPriceAmount: (json['originalPriceAmount'] as num?)?.toDouble(),
812-813: Same fix for Subscription.fromJson (originalPriceAmount)- originalPriceAmount: json['originalPriceAmount'] as double?, + originalPriceAmount: (json['originalPriceAmount'] as num?)?.toDouble(),
1259-1261: Same fix for ProductAndroid.fromJson (originalPriceAmount)- originalPriceAmount: json['originalPriceAmount'] as double?, + originalPriceAmount: (json['originalPriceAmount'] as num?)?.toDouble(),
354-356: Null-assertion bug in description truncation — will throw when description is nullMake truncation null-safe.
- buffer.writeln( - ' description: ${description?.substring(0, description!.length > 100 ? 100 : description!.length)}${description != null && description!.length > 100 ? '...' : ''},', - ); + if (description != null) { + final desc = description!; + final short = desc.length > 100 ? '${desc.substring(0, 100)}...' : desc; + buffer.writeln(' description: $short,'); + } else { + buffer.writeln(' description: null,'); + }
1728-1729: priceIOS should accept int or double from JSON- priceIOS: json['priceIOS'] as double?, + priceIOS: (json['priceIOS'] as num?)?.toDouble(),
🧹 Nitpick comments (10)
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (7)
432-437: Unify logging: use TAG instead of hardcoded "InappPurchase".Keep logging consistent and make future changes easier by using the TAG constant.
Apply:
- // Debug logging - Log.d("InappPurchase", "Product: ${productDetails.productId}") - Log.d("InappPurchase", "Name: ${productDetails.name}") - Log.d("InappPurchase", "Type: ${productDetails.productType}") - Log.d("InappPurchase", "OneTimePurchaseOfferDetails: ${productDetails.oneTimePurchaseOfferDetails}") + // Debug logging + Log.d(TAG, "Product: ${productDetails.productId}") + Log.d(TAG, "Name: ${productDetails.name}") + Log.d(TAG, "Type: ${productDetails.productType}") + Log.d(TAG, "OneTimePurchaseOfferDetails: ${productDetails.oneTimePurchaseOfferDetails}")
452-454: Avoid duplicate price assignment.Price is set again in the INAPP block below; setting it here is redundant.
Apply:
- // Also put the price here for consistency - item.put("price", (productDetails.oneTimePurchaseOfferDetails!!.priceAmountMicros / 1000000f).toString())
458-460: Unify logging: use TAG constant.Apply:
- - Log.d("InappPurchase", "INAPP product oneTimePurchaseOfferDetails is ${if (oneTimePurchaseOfferDetails != null) "available" else "null"}") + Log.d(TAG, "INAPP product oneTimePurchaseOfferDetails is ${if (oneTimePurchaseOfferDetails != null) "available" else "null"}")
471-474: LGTM: Android-suffixed oneTimePurchaseOfferDetails mirror.The Android-specific mirror plus log is correct and consistent with the PR goals. Please also keep logs using TAG.
Optional logging tweak:
- item.put("oneTimePurchaseOfferDetailsAndroid", offerDetails) - - Log.d("InappPurchase", "Added oneTimePurchaseOfferDetailsAndroid: $offerDetails") + item.put("oneTimePurchaseOfferDetailsAndroid", offerDetails) + Log.d(TAG, "Added oneTimePurchaseOfferDetailsAndroid: $offerDetails")
534-539: LGTM: Android-suffixed subscriptionOfferDetails with backward-compat field.This cleanly separates platform fields while keeping a deprecation path. Prefer TAG for logs.
Apply:
- // Use Android suffix for platform-specific field - item.put("subscriptionOfferDetailsAndroid", subsOffers) - // TODO(v6.4.0): Remove deprecated subscriptionOfferDetails - item.put("subscriptionOfferDetails", subsOffers) // Keep for backward compatibility - Log.d("InappPurchase", "subscriptionOfferDetailsAndroid: $subsOffers") + // Use Android suffix for platform-specific field + item.put("subscriptionOfferDetailsAndroid", subsOffers) + // TODO(v6.4.0): Remove deprecated subscriptionOfferDetails + item.put("subscriptionOfferDetails", subsOffers) // Keep for backward compatibility + Log.d(TAG, "subscriptionOfferDetailsAndroid: $subsOffers")
565-568: Deprecation UX: warn when prorationMode is used.Small improvement: log a warning when the deprecated prorationMode path is taken.
Apply:
- // TODO(v6.4.0): Remove prorationMode parameter support - // Support both old and new parameter names for backward compatibility - val replacementMode = call.argument<Int>("replacementMode") ?: call.argument<Int>("prorationMode") ?: -1 + // TODO(v6.4.0): Remove prorationMode parameter support + // Support both old and new parameter names for backward compatibility + val replacementMode = call.argument<Int>("replacementMode") ?: call.argument<Int>("prorationMode") ?: -1 + if (replacementMode != -1 && call.argument<Int>("replacementMode") == null && call.argument<Int>("prorationMode") != null) { + Log.w(TAG, "prorationMode is deprecated; use replacementMode instead.") + }
630-709: Guard replacement mode to subscriptions only; fail fast for INAPP.Currently only CHARGE_PRORATED_PRICE checks type == SUBS. To avoid misconfiguration and inconsistent behavior, reject any replacementMode when purchasing INAPP products.
Apply:
- when (replacementMode) { + // Guard: subscription replacement only applies to SUBS + if (replacementMode != -1 && type != BillingClient.ProductType.SUBS) { + safeChannel.error( + TAG, + "buyItemByType", + "Subscription replacement mode only applies to subscription products." + ) + return + } + when (replacementMode) {lib/types.dart (1)
3495-3501: purchaseState serialization: consider emitting a stable code (index/name) instead of enum.toString()toString() yields values like 'PurchaseState.purchased', which is brittle across refactors and not OpenIAP-friendly. Prefer a stable code (index or a string token).
- 'purchaseState': purchaseState?.toString(), + 'purchaseState': purchaseState?.index, // or purchaseState?.name on Dart >= 2.15If the TS/OpenIAP type expects a specific shape (string vs number), align accordingly. Happy to adjust across the codebase once confirmed.
example/lib/src/widgets/product_detail_modal.dart (2)
311-316: Remove duplicate “End of Raw JSON Data” printThere are two identical trailing lines; keep a single footer.
- debugPrint('=== End of Raw JSON Data ==='); - debugPrint('=== End of Raw JSON Data ==='); + debugPrint('=== End of Raw JSON Data ===');
129-135: Simplify type checks —isalready handles nullYou can drop redundant
product != null &&checks.- final jsonData = product != null && product is Product + final jsonData = product is Product ? (product as Product).toJson() - : product != null && product is Subscription + : product is Subscription ? (product as Subscription).toJson() : _itemToMap(item);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt(11 hunks)example/lib/src/widgets/product_detail_modal.dart(4 hunks)ios/Classes/FlutterInappPurchasePlugin.swift(5 hunks)lib/types.dart(48 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.dart
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.dart: For iOS-related code, when iOS is the final suffix, use the IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
For iOS-related code, when iOS is not the final suffix, use Ios (e.g., IosManager, IosHelper)
For Android-related code, use the Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)
These platform/IAP naming conventions apply to both functions and types
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Follow OpenIAP terminology for API method naming
Do not use generic prefixes like get or find; use official OpenIAP terminology instead
Files:
lib/types.dartexample/lib/src/widgets/product_detail_modal.dart
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Ensure alignment with OpenIAP standards prior to implementation
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : These platform/IAP naming conventions apply to both functions and types
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : These platform/IAP naming conventions apply to both functions and types
Applied to files:
lib/types.dart
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)
Applied to files:
lib/types.dartios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : Follow OpenIAP terminology for API method naming
Applied to files:
lib/types.dart
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For Android-related code, use the Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
Applied to files:
lib/types.dartandroid/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For iOS-related code, when iOS is the final suffix, use the IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
Applied to files:
lib/types.dart
🪛 SwiftLint (0.57.0)
ios/Classes/FlutterInappPurchasePlugin.swift
[Warning] 187-187: TODOs should be resolved ((v6.4.0): Remove deprecated jw...)
(todo)
[Warning] 398-398: TODOs should be resolved ((v6.4.0): Remove deprecated jw...)
(todo)
🔇 Additional comments (3)
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (2)
428-428: Good addition: expose Android-specific product name via nameAndroid.This aligns with the platform-specific field segregation in the PR objectives.
511-513: LGTM: Preserve nullability of offerId.Using JSONObject.NULL when offerId is absent matches the API and avoids field omission ambiguity.
ios/Classes/FlutterInappPurchasePlugin.swift (1)
682-687: Type mapping LGTM — aligns with OpenIAP tokens ("inapp"/"subs")Correct mapping for StoreKit 2 types; defaulting to "inapp" is acceptable.
- Fix numeric type casting for originalPriceAmount fields - Fix documentation stream static modifiers - Fix originalPrice nullability in docs - Ensure consistent type handling for JSON parsing
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
docs/docs/api/methods/get-products.md (2)
75-92: Fix inconsistent type usage in Combined Example (strings → PurchaseType).The Combined Example still uses raw string literals ('inapp'/'subs'). Align it with the enum-based API to avoid confusion.
Apply this diff:
final products = await FlutterInappPurchase.instance.requestProducts( skus: ['coins_100', 'remove_ads'], - type: 'inapp', + type: PurchaseType.inapp, ); @@ final subscriptions = await FlutterInappPurchase.instance.requestProducts( skus: ['premium_monthly', 'premium_yearly'], - type: 'subs', + type: PurchaseType.subs, );
165-175: Fix inconsistent type usage in Migration section.The first call uses a string type ('inapp'), while the second uses the enum. Standardize on the enum.
Apply this diff:
final products = await FlutterInappPurchase.instance.requestProducts( skus: productIds, - type: 'inapp', + type: PurchaseType.inapp, ); final subscriptions = await FlutterInappPurchase.instance.requestProducts( skus: subscriptionIds, type: PurchaseType.subs, );android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (2)
624-703: Enforce SUBS-only constraint for all replacement modes (not just CHARGE_PRORATED_PRICE).Only the CHARGE_PRORATED_PRICE branch validates type == SUBS; other modes should also restrict to subscriptions to prevent invalid billing flows on INAPP.
Apply this refactor:
- when (replacementMode) { + when (replacementMode) { -1 -> { /* ignore - no replacement mode specified */ } - 2 -> { // CHARGE_PRORATED_PRICE + 2 -> { // CHARGE_PRORATED_PRICE + if (type != BillingClient.ProductType.SUBS) { + safeChannel.error(TAG, "buyItemByType", "CHARGE_PRORATED_PRICE replacement mode only works with subscriptions.") + return + } // For subscription replacement, purchaseToken is required if (purchaseToken.isNullOrEmpty()) { safeChannel.error( TAG, "buyItemByType", "Old SKU purchase information(token/id) or original external transaction id must be provided for subscription replacement." ) return } params.setOldPurchaseToken(purchaseToken) .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE) builder.setSubscriptionUpdateParams(params.build()) - if (type != BillingClient.ProductType.SUBS) { - safeChannel.error( - TAG, - "buyItemByType", - "CHARGE_PRORATED_PRICE replacement mode only works with subscriptions." - ) - return - } } - 3 -> { // WITHOUT_PRORATION + 3 -> { // WITHOUT_PRORATION + if (type != BillingClient.ProductType.SUBS) { + safeChannel.error(TAG, "buyItemByType", "WITHOUT_PRORATION replacement mode only works with subscriptions.") + return + } if (purchaseToken.isNullOrEmpty()) { safeChannel.error( TAG, "buyItemByType", "Old SKU purchase information(token/id) or original external transaction id must be provided for subscription replacement." ) return } params.setOldPurchaseToken(purchaseToken) .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION) builder.setSubscriptionUpdateParams(params.build()) } 4 -> { // DEFERRED + if (type != BillingClient.ProductType.SUBS) { + safeChannel.error(TAG, "buyItemByType", "DEFERRED replacement mode only works with subscriptions.") + return + } if (purchaseToken.isNullOrEmpty()) { safeChannel.error( TAG, "buyItemByType", "Old SKU purchase information(token/id) or original external transaction id must be provided for deferred subscription replacement." ) return } params.setOldPurchaseToken(purchaseToken) .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.DEFERRED) builder.setSubscriptionUpdateParams(params.build()) } 1 -> { // WITH_TIME_PRORATION + if (type != BillingClient.ProductType.SUBS) { + safeChannel.error(TAG, "buyItemByType", "WITH_TIME_PRORATION replacement mode only works with subscriptions.") + return + } if (purchaseToken.isNullOrEmpty()) { safeChannel.error( TAG, "buyItemByType", "Old SKU purchase information(token/id) or original external transaction id must be provided for subscription replacement." ) return } params.setOldPurchaseToken(purchaseToken) .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION) builder.setSubscriptionUpdateParams(params.build()) } 5 -> { // CHARGE_FULL_PRICE + if (type != BillingClient.ProductType.SUBS) { + safeChannel.error(TAG, "buyItemByType", "CHARGE_FULL_PRICE replacement mode only works with subscriptions.") + return + } if (purchaseToken.isNullOrEmpty()) { safeChannel.error( TAG, "buyItemByType", "Old SKU purchase information(token/id) or original external transaction id must be provided for subscription replacement." ) return } params.setOldPurchaseToken(purchaseToken) .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE) builder.setSubscriptionUpdateParams(params.build()) } else -> { // Unknown replacement mode: do not set subscription update params } }
793-799: Avoid logging full purchase payloads (tokens/receipts) in production logs.Logging item.toString() includes purchaseToken and originalJson, which can be sensitive. Consider redacting tokens or gating verbose logs behind a debug flag/build type.
Example redaction:
- Log.d(TAG, "Purchase data: ${item.toString()}") + val redacted = JSONObject(item.toString()).apply { + put("purchaseToken", "***") + put("transactionReceipt", "***") + put("dataAndroid", "***") + put("signatureAndroid", "***") + } + Log.d(TAG, "Purchase data (redacted): $redacted")
♻️ Duplicate comments (1)
docs/docs/api/classes/iap-item.md (1)
55-56: Resolved prior inconsistency: originalPrice now nullableThanks for aligning originalPrice with the “All string properties are nullable” note.
🧹 Nitpick comments (15)
docs/blog/2025-08-20-6.3.3.release.md (3)
31-47: Good migration example; consider labeling legacy enum explicitly.The “Before (older API)” block still uses AndroidProrationMode. To reduce confusion for readers, explicitly label that enum as deprecated/legacy in the snippet header.
Apply this minor tweak:
-#### Migration Example +#### Migration Example (Android: prorationMode → replacementMode) -Before (older API): +Before (older API - deprecated prorationMode):
73-84: Android field coverage note is helpful. Add clarifier about display vs numeric price.Nice addition of nameAndroid and oneTimePurchaseOfferDetailsAndroid. Consider adding one line to clarify that displayPrice/formattedPrice is a human-readable string, while numeric price should be sourced from priceAmountMicros to avoid parsing issues in apps.
Apply this addendum:
These fields ensure Android products now have near 100% field compatibility with the OpenIAP specification (up from 60%). + +Note: formattedPrice/displayPrice is a human-readable string. For numeric calculations, prefer priceAmountMicros (convert micros to standard currency units on the client).
125-131: “No breaking changes” claim: verify ecosystem-wide.Statement says “None.” Given enum and field deprecations, confirm no compile-time breakages for typical usage paths (e.g., requestPurchase param shapes). If any public signatures changed across the repo (docs suggest some renames), consider rewording to “No breaking changes intended; deprecations with backward-compat shims provided.”
Would you like me to scan the public Dart API surface to ensure signatures remained backward compatible?
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (4)
428-437: Unify logging tag (use TAG).Mixed use of literal "InappPurchase" and TAG makes filtering logs harder. Stick to TAG for consistency.
Apply this diff:
- // Debug logging - Log.d("InappPurchase", "Product: ${productDetails.productId}") - Log.d("InappPurchase", "Name: ${productDetails.name}") - Log.d("InappPurchase", "Type: ${productDetails.productType}") - Log.d("InappPurchase", "OneTimePurchaseOfferDetails: ${productDetails.oneTimePurchaseOfferDetails}") + // Debug logging + Log.d(TAG, "Product: ${productDetails.productId}") + Log.d(TAG, "Name: ${productDetails.name}") + Log.d(TAG, "Type: ${productDetails.productType}") + Log.d(TAG, "OneTimePurchaseOfferDetails: ${productDetails.oneTimePurchaseOfferDetails}")
449-454: Avoid misusing “introductoryPrice” for INAPP one-time price.Setting introductoryPrice for one-time purchase details can confuse consumers (introductoryPrice is an iOS concept). Consider either omitting or clearly commenting it as legacy/back-compat.
Apply this tweak:
- item.put("introductoryPrice", productDetails.oneTimePurchaseOfferDetails!!.formattedPrice) + // Legacy: Some clients read introductoryPrice for display; prefer price/localizedPrice instead for INAPP. + // item.put("introductoryPrice", productDetails.oneTimePurchaseOfferDetails!!.formattedPrice)
456-485: Basic offer details are always created due to non-null fallbacks.currency and displayPrice are never null (defaulted to "Unknown"/"N/A"), so the block always runs. Guard against placeholder values to reduce noisy payloads.
Apply this diff:
- } else { - Log.d(TAG, "oneTimePurchaseOfferDetails is null for INAPP product") - // Still expose display-only details without mutating numeric price - if (currency != null && displayPrice != null) { + } else { + Log.d(TAG, "oneTimePurchaseOfferDetails is null for INAPP product") + // Expose display-only details only when meaningful + if (currency != "Unknown" && displayPrice != "N/A") { val basicOfferDetails = JSONObject() basicOfferDetails.put("priceCurrencyCode", currency) basicOfferDetails.put("formattedPrice", displayPrice) basicOfferDetails.put("priceAmountMicros", JSONObject.NULL) // unknown item.put("oneTimePurchaseOfferDetailsAndroid", basicOfferDetails) Log.d(TAG, "Created basic oneTimePurchaseOfferDetailsAndroid: $basicOfferDetails") } }
341-389: “getPurchaseHistoryByType” uses queryPurchasesAsync (active purchases).Since queryPurchaseHistoryAsync was removed in v8, you switched to queryPurchasesAsync, which returns current purchases, not full history. Ensure docs reflect this behavioral difference, or consider renaming the method and exposing an alternative history path if applicable.
lib/types.dart (4)
19-24: Duplicate concept with PurchaseType; consider deprecating ProductType.ProductType duplicates PurchaseType (from enums.dart). Keeping both invites drift. Prefer a single source of truth (PurchaseType) and deprecate ProductType.
Apply this deprecation marker:
-/// Product type enum (OpenIAP compliant) -class ProductType { +/// Product type enum (OpenIAP compliant) +@Deprecated('Use PurchaseType from enums.dart') +class ProductType { static const String inapp = 'inapp'; static const String subs = 'subs'; }
237-246: Default platformEnum to iOS can mislabel Android products.Defaulting to IapPlatform.ios may produce incorrect platform strings in toJson. Consider inferring from platform string or leaving platformEnum required in constructors.
Possible tweak:
- platformEnum: platformEnum ?? IapPlatform.ios, + platformEnum: platformEnum ?? + (platform == 'android' ? IapPlatform.android : IapPlatform.ios),
1504-1516: Emit priceAmountMicros as string; verify downstream expects string.You serialize priceAmountMicros as a string. If TypeScript/OpenIAP expects string (common in Android bridges), this is fine. If numeric is expected, consider emitting as number.
3399-3406: Helpful conversion extensions; consider reducing duplication between Product/Subscription toJson logic.There’s notable duplication between Product and Subscription toJson and the OpenIAP conversion extensions. Extract shared helpers to keep behavior in sync.
docs/docs/api/classes/flutter-inapp-purchase.md (1)
342-350: Migration note is clear; ensure all examples use the listener forms.Good call-out to use purchaseUpdatedListener/purchaseErrorListener. Double-check examples elsewhere in docs use the new listener names consistently and avoid static access patterns.
docs/docs/api/classes/iap-item.md (3)
119-139: Document newly supported Android fields for completenessPer the PR objectives (“Fixed Product parsing for Android fields: nameAndroid and oneTimePurchaseOfferDetailsAndroid”), these fields should be reflected in the Android-specific properties. Add them alongside the existing list.
### Android-Specific Properties @@ ```dart final String? signatureAndroid@@
final String? subscriptionPeriodAndroid@@
final List<SubscriptionOfferAndroid>? subscriptionOffersAndroid@@
final String? iconUrl+
+final String? nameAndroid +
+The title/name returned by Google Play for the product (Android-only).
+
++final OneTimePurchaseOfferDetailsAndroid? oneTimePurchaseOfferDetailsAndroid +
+One-time purchase offer details for in-app products on Android (e.g., price, formattedPrice, currencyCode).If these are intentionally documented elsewhere, cross-link here. --- `161-164`: **Add missing import in example for jsonEncode** The example uses convert.jsonEncode(...) without showing its import. Add it to avoid confusion. ```diff **Example:** ```dart +import 'dart:convert' as convert; String jsonString = convert.jsonEncode(item.toJson());--- `197-202`: **Optional: Showcase more Android offer details** Given the presence of SubscriptionOfferAndroid and PricingPhaseAndroid, consider expanding the example to print pricing phase details (price, formattedPrice, billingPeriod) to help users navigate multi-phase offers. No change required; purely a docs enhancement for clarity. </blockquote></details> </blockquote></details> <details> <summary>📜 Review details</summary> **Configuration used: CodeRabbit UI** **Review profile: CHILL** **Plan: Pro** **💡 Knowledge Base configuration:** - MCP integration is disabled by default for public repositories - Jira integration is disabled by default for public repositories - Linear integration is disabled by default for public repositories You can enable these sources in your CodeRabbit configuration. <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between dd45de34206762fe4e8b8457ab3e9bc8e2379e55 and 98de6197dde85ac02d18d48e27cc4757575f5687. </details> <details> <summary>📒 Files selected for processing (8)</summary> * `android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt` (11 hunks) * `docs/blog/2025-08-20-6.3.3.release.md` (1 hunks) * `docs/docs/api/classes/flutter-inapp-purchase.md` (7 hunks) * `docs/docs/api/classes/iap-item.md` (4 hunks) * `docs/docs/api/methods/get-products.md` (9 hunks) * `docs/docs/migration/from-v5.md` (4 hunks) * `example/lib/src/widgets/product_detail_modal.dart` (4 hunks) * `lib/types.dart` (48 hunks) </details> <details> <summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary> * example/lib/src/widgets/product_detail_modal.dart </details> <details> <summary>🧰 Additional context used</summary> <details> <summary>📓 Path-based instructions (1)</summary> <details> <summary>**/*.dart</summary> **📄 CodeRabbit Inference Engine (CLAUDE.md)** > `**/*.dart`: For iOS-related code, when iOS is the final suffix, use the IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS) > For iOS-related code, when iOS is not the final suffix, use Ios (e.g., IosManager, IosHelper) > For Android-related code, use the Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid) > For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase) > These platform/IAP naming conventions apply to both functions and types > Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription) > Follow OpenIAP terminology for API method naming > Do not use generic prefixes like get or find; use official OpenIAP terminology instead Files: - `lib/types.dart` </details> </details><details> <summary>🧠 Learnings (8)</summary> <details> <summary>📓 Common learnings</summary>Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Ensure alignment with OpenIAP standards prior to implementationLearnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : These platform/IAP naming conventions apply to both functions and types</details> <details> <summary>📚 Learning: 2025-08-18T17:24:36.721Z</summary>Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)**Applied to files:** - `docs/docs/migration/from-v5.md` - `lib/types.dart` - `docs/docs/api/classes/flutter-inapp-purchase.md` - `docs/docs/api/methods/get-products.md` - `docs/docs/api/classes/iap-item.md` - `docs/blog/2025-08-20-6.3.3.release.md` </details> <details> <summary>📚 Learning: 2025-08-18T17:24:36.721Z</summary>Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : Follow OpenIAP terminology for API method naming**Applied to files:** - `docs/docs/migration/from-v5.md` - `lib/types.dart` - `docs/docs/api/classes/flutter-inapp-purchase.md` - `docs/docs/api/classes/iap-item.md` </details> <details> <summary>📚 Learning: 2025-08-18T17:24:36.721Z</summary>Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : These platform/IAP naming conventions apply to both functions and types**Applied to files:** - `docs/docs/migration/from-v5.md` - `lib/types.dart` - `docs/docs/api/classes/flutter-inapp-purchase.md` - `docs/docs/api/methods/get-products.md` - `docs/docs/api/classes/iap-item.md` </details> <details> <summary>📚 Learning: 2025-08-18T17:24:36.721Z</summary>Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)**Applied to files:** - `docs/docs/migration/from-v5.md` - `docs/docs/api/classes/flutter-inapp-purchase.md` - `docs/docs/api/methods/get-products.md` - `docs/blog/2025-08-20-6.3.3.release.md` </details> <details> <summary>📚 Learning: 2025-08-18T17:24:36.721Z</summary>Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : Do not use generic prefixes like get or find; use official OpenIAP terminology instead**Applied to files:** - `docs/docs/migration/from-v5.md` </details> <details> <summary>📚 Learning: 2025-08-18T17:24:36.721Z</summary>Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For Android-related code, use the Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)**Applied to files:** - `lib/types.dart` - `docs/docs/api/classes/flutter-inapp-purchase.md` - `docs/docs/api/methods/get-products.md` - `android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt` - `docs/blog/2025-08-20-6.3.3.release.md` </details> <details> <summary>📚 Learning: 2025-08-18T17:24:36.721Z</summary>Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For iOS-related code, when iOS is the final suffix, use the IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)**Applied to files:** - `lib/types.dart` </details> </details><details> <summary>🪛 LanguageTool</summary> <details> <summary>docs/docs/api/classes/flutter-inapp-purchase.md</summary> [style] ~41-~41: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym. Context: ...onnectionResult> connectionUpdated ``` Stream that emits connection state updates. `... (ENGLISH_WORD_REPEAT_BEGINNING_RULE) --- [style] ~47-~47: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym. Context: ...t Stream<String?> purchasePromoted ``` Stream that emits promoted product IDs (iOS on... (ENGLISH_WORD_REPEAT_BEGINNING_RULE) --- [style] ~53-~53: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym. Context: ...t Stream<int?> inAppMessageAndroid ``` Stream that emits in-app message codes (Androi... (ENGLISH_WORD_REPEAT_BEGINNING_RULE) --- [style] ~93-~93: To form a complete sentence, be sure to include a subject. Context: ...tion to the platform's billing service. Should be called when the app no longer needs ... (MISSING_IT_THERE) --- [grammar] ~169-~169: There might be a mistake here. Context: ...orm-specific purchase request parameters - `type`: Type of purchase (inapp or subscriptio... (QB_NEW_EN) --- [grammar] ~196-~196: There might be a mistake here. Context: ...** - `purchase`: The purchase to finish - `isConsumable`: Whether the product is consumable (And... (QB_NEW_EN) </details> <details> <summary>docs/docs/api/methods/get-products.md</summary> [grammar] ~25-~25: There might be a mistake here. Context: ... | | --------- | -------------- | -------- ... (QB_NEW_EN) --- [grammar] ~26-~26: There might be a mistake here. Context: ...-------------------------------------- | | `skus` | `List<String>` | Yes ... (QB_NEW_EN) --- [grammar] ~27-~27: There might be a mistake here. Context: ... | | `type` | `PurchaseType` | No ... (QB_NEW_EN) --- [grammar] ~32-~32: There might be a mistake here. Context: ...bscriptions) | ## Returns - **Type**: `Future<List<IapItem>>` - **Description**: A list of product items w... (QB_NEW_EN) --- [grammar] ~107-~107: There might be a mistake here. Context: ...e purchased multiple times (coins, gems) - **Non-consumables**: Items purchased once ... (QB_NEW_EN) </details> <details> <summary>docs/blog/2025-08-20-6.3.3.release.md</summary> [grammar] ~24-~24: There might be a mistake here. Context: ...de` enum → `AndroidReplacementMode` enum - Updated enum values for better clarity: ... (QB_NEW_EN) --- [grammar] ~77-~77: There might be a mistake here. Context: ...id`**: The product name from Google Play - **`oneTimePurchaseOfferDetailsAndroid`**: Structured pricing details including: ... (QB_NEW_EN) --- [grammar] ~78-~78: There might be a mistake here. Context: ...*: Structured pricing details including: - `priceCurrencyCode`: ISO 4217 currency code - `formattedP... (QB_NEW_EN) --- [grammar] ~79-~79: There might be a mistake here. Context: ...iceCurrencyCode`: ISO 4217 currency code - `formattedPrice`: Formatted price string - `priceAmoun... (QB_NEW_EN) --- [grammar] ~80-~80: There might be a mistake here. Context: ...`formattedPrice`: Formatted price string - `priceAmountMicros`: Price in micro-units These fields ens... (QB_NEW_EN) --- [grammar] ~95-~95: There might be a mistake here. Context: ...ment Mode - ❌ `prorationMode` parameter - ❌ `prorationModeAndroid` parameter - ❌ `... (QB_NEW_EN) --- [grammar] ~96-~96: There might be a mistake here. Context: ...ter - ❌ `prorationModeAndroid` parameter - ❌ `AndroidProrationMode` enum - ✅ Use `r... (QB_NEW_EN) --- [grammar] ~97-~97: There might be a mistake here. Context: ...arameter - ❌ `AndroidProrationMode` enum - ✅ Use `replacementMode`, `replacementMod... (QB_NEW_EN) </details> </details> </details> <details> <summary>⏰ 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)</summary> * GitHub Check: build </details> <details> <summary>🔇 Additional comments (13)</summary><blockquote> <details> <summary>docs/blog/2025-08-20-6.3.3.release.md (1)</summary> `165-170`: **Links look sane; keep them current.** The docs and Slack links now point to project resources. Make sure these are the canonical URLs you’d like users to bookmark. </details> <details> <summary>docs/docs/api/methods/get-products.md (3)</summary> `16-21`: **Signature update to PurchaseType enum looks good.** Returning Future<List<IapItem>> and using PurchaseType default improves type safety and consistency across docs. --- `25-29`: **Parameter table aligns with enum usage.** The table now documents PurchaseType.inapp/PurchaseType.subs, which prevents stringly-typed mistakes. --- `39-44`: **Example consistency LGTM (enum usage).** Using PurchaseType.inapp in examples avoids raw strings and matches the signature. </details> <details> <summary>lib/types.dart (5)</summary> `297-305`: **Safe num→double conversion LGTM.** Switch to (json['originalPriceAmount'] as num?)?.toDouble() avoids cast exceptions when the value is int. --- `1409-1423`: **OfferDetail JSON shape matches Android nested pricingPhases.** Good handling of both legacy list and new pricingPhases.pricingPhaseList shapes, and emitting the nested structure in toJson for TypeScript alignment. --- `1731-1733`: **priceIOS conversion fix LGTM.** Parsing via num?.toDouble() prevents runtime errors when JSON supplies ints. --- `276-325`: **Back-compat Android subscription offer mapping is solid.** subscriptionOfferDetailsAndroid takes precedence, with fallback to subscriptionOfferDetails. This helps consumers migrate gradually. --- `2756-2762`: **PurchasedItem originalJsonAndroid backfill from dataAndroid is a good compatibility fix.** This ensures older payloads are still consumable without breaking consumers relying on originalJsonAndroid. </details> <details> <summary>docs/docs/api/classes/flutter-inapp-purchase.md (1)</summary> `56-67`: **Expo-compatible listener names are documented well.** The addition of purchaseUpdatedListener and purchaseErrorListener clarifies the migration path from expo-iap. </details> <details> <summary>docs/docs/migration/from-v5.md (2)</summary> `301-301`: **LGTM: Type rename aligned with migration narrative** List<IapItem> _items = [] correctly reflects the v6 rename from IAPItem to IapItem. --- `339-345`: **LGTM: Parameter type updated to IapItem** The _requestPurchase signature using IapItem matches the new type naming and the surrounding examples. </details> <details> <summary>docs/docs/api/classes/iap-item.md (1)</summary> `3-7`: **LGTM: Consistent renaming to IapItem** Title, heading, and overview consistently use IapItem, matching the project-wide rename. Also applies to: 12-13 </details> </blockquote></details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
- Fix type mismatch when accessing expirationDateIOS field - PurchaseIOS has int? while Purchase has DateTime? for expirationDateIOS - Add explicit type checks and casting to handle both cases properly - Remove unused import from flutter_inapp_purchase.dart - Fix lint issues including unnecessary type check and unused variable
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (9)
docs/docs/examples/basic-store.md (1)
110-121: Use instance streams and connection updatesSwitch static stream access to the instance to match the documented API and avoid confusing readers.
- _purchaseSubscription = FlutterInappPurchase.purchaseUpdated.listen( + _purchaseSubscription = _iap.purchaseUpdated.listen( (purchase) { if (purchase != null) { _handlePurchaseSuccess(purchase); } }, onError: (error) { _showError('Purchase stream error: $error'); }, ); - _errorSubscription = FlutterInappPurchase.purchaseError.listen( + _errorSubscription = _iap.purchaseError.listen( (error) { if (error != null) { _handlePurchaseError(error); } }, ); - _connectionSubscription = FlutterInappPurchase.connectionUpdated.listen( + _connectionSubscription = _iap.connectionUpdated.listen( (connectionResult) {Also applies to: 122-130, 131-143
docs/docs/guides/subscriptions.md (5)
132-144: Use RequestSubscriptionAndroid for subscription upgrades and update proration enum.RequestPurchaseAndroid doesn’t have purchaseTokenAndroid or replacementModeAndroid; these belong to RequestSubscriptionAndroid. Also, AndroidProrationMode is deprecated in favor of AndroidReplacementMode. Pass subscriptionOffers (required) even if empty.
Apply this diff:
- await iap.requestPurchase( - request: RequestPurchase( - ios: RequestPurchaseIOS(sku: subscriptionId), - android: RequestPurchaseAndroid( - skus: [subscriptionId], - purchaseTokenAndroid: currentToken, // [DEPRECATED] Use purchaseToken instead - replacementModeAndroid: prorationMode ?? - AndroidProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE, - obfuscatedAccountIdAndroid: await _getUserId(), - ), - ), - type: PurchaseType.subs, - ); + await iap.requestPurchase( + request: RequestPurchase( + ios: RequestPurchaseIOS(sku: subscriptionId), + android: RequestSubscriptionAndroid( + skus: [subscriptionId], + purchaseTokenAndroid: currentToken, // required for replacement + replacementModeAndroid: prorationMode ?? + AndroidReplacementMode.immediateAndChargeProratedPrice.value, + subscriptionOffers: const [], // required param + obfuscatedAccountIdAndroid: await _getUserId(), + ), + ), + type: PurchaseType.subs, + );
199-209: Fix Android active-state check.purchaseStateAndroid is an int; comparing to 'purchased' (string) won’t work. Use the enum constant or the numeric value.
- if (Platform.isAndroid) { - return purchase.purchaseStateAndroid == 'purchased'; - } + if (Platform.isAndroid) { + // 0 = PURCHASED, 1 = PENDING + return purchase.purchaseStateAndroid == PurchaseAndroidState.PURCHASED; + }If you want to avoid importing the enum, compare to 0.
549-563: Construct PaymentDiscount instead of passing a raw Map.RequestPurchaseIOS.withOffer takes a PaymentDiscount object; a Map literal will not type-check.
- ios: RequestPurchaseIOS( - sku: subscriptionId, - withOffer: { - 'identifier': offerId, - 'keyIdentifier': keyId, - 'nonce': nonce, - 'signature': signature, - 'timestamp': timestamp, - }, - ), + ios: RequestPurchaseIOS( + sku: subscriptionId, + withOffer: PaymentDiscount( + identifier: offerId, + keyIdentifier: keyId, + nonce: nonce, + signature: signature, + timestamp: timestamp.toString(), + ), + ),
571-587: Use RequestSubscriptionAndroid and proper offer shape.subscriptionOffers expects a List; the example currently passes a Map with an unquoted key.
- request: RequestPurchase( - ios: RequestPurchaseIOS(sku: subscriptionId), - android: RequestPurchaseAndroid( - skus: [subscriptionId], - subscriptionOffers: [{offerToken: offerIndex.toString()}], - ), - ), + request: RequestPurchase( + ios: RequestPurchaseIOS(sku: subscriptionId), + android: RequestSubscriptionAndroid( + skus: [subscriptionId], + subscriptionOffers: [ + SubscriptionOfferAndroid( + sku: subscriptionId, + offerToken: offerIndex.toString(), + ) + ], + ), + ),
597-610: Update replacement flow to subscription-aware request and enum.Same rationale as above: use RequestSubscriptionAndroid and AndroidReplacementMode.
- await FlutterInappPurchase.instance.requestPurchase( - request: RequestPurchase( - ios: RequestPurchaseIOS(sku: newSubscriptionId), - android: RequestPurchaseAndroid( - skus: [newSubscriptionId], - purchaseTokenAndroid: oldToken, - replacementModeAndroid: prorationMode, - ), - ), - type: PurchaseType.subs, - ); + await FlutterInappPurchase.instance.requestPurchase( + request: RequestPurchase( + ios: RequestPurchaseIOS(sku: newSubscriptionId), + android: RequestSubscriptionAndroid( + skus: [newSubscriptionId], + purchaseTokenAndroid: oldToken, + replacementModeAndroid: prorationMode ?? + AndroidReplacementMode.immediateAndChargeProratedPrice.value, + subscriptionOffers: const [], + ), + ), + type: PurchaseType.subs, + );lib/flutter_inapp_purchase.dart (3)
398-403: Bug: replacementMode parameter is a no‑opThe public
replacementModeparameter is accepted but never used. Callers passingreplacementModewill see no behavior change, which is a breaking usability bug.Apply this diff to map
replacementModeto the Android-specific field when on Android, and preserve the deprecatedprorationModeas a fallback:Future<void> requestPurchaseAuto({ required String sku, required iap_types.PurchaseType type, @@ - @Deprecated('Use replacementMode instead') int? prorationMode, - int? replacementMode, + @Deprecated('Use replacementMode instead') int? prorationMode, + int? replacementMode, @@ }) async {And update the Android request construction (see next comment) to actually use
replacementMode.
416-431: Wire replacementMode into Android request; keep prorationMode compatibilityCurrently only
replacementModeAndroidis passed through. Ensure the platform-agnosticreplacementModeis honored for Android callers and keep the deprecatedprorationModeas a fallback.Apply this diff:
android: _platform.isAndroid ? (type == iap_types.PurchaseType.subs ? iap_types.RequestSubscriptionAndroid( skus: [sku], obfuscatedAccountIdAndroid: obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid: obfuscatedProfileIdAndroid, isOfferPersonalized: isOfferPersonalized, purchaseTokenAndroid: purchaseToken, - replacementModeAndroid: replacementModeAndroid, + // Prefer platform-agnostic replacementMode; fallback to Android-specific, then deprecated prorationMode. + replacementModeAndroid: replacementMode ?? replacementModeAndroid ?? prorationMode, subscriptionOffers: subscriptionOffers ?? [], ) : iap_types.RequestPurchaseAndroid( skus: [sku], obfuscatedAccountIdAndroid: obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid: obfuscatedProfileIdAndroid, isOfferPersonalized: isOfferPersonalized, ))
672-722: Harden numeric parsing for originalPriceAmount and mixed-type fields
originalPriceAmountis cast asdouble?, which will throw if the source is anintor aString. Prefernumthen toDouble to support all native payload variants.Apply this diff:
- originalPriceAmount: json['originalPriceAmount'] as double?, + originalPriceAmount: + (json['originalPriceAmount'] is num) + ? (json['originalPriceAmount'] as num).toDouble() + : (json['originalPriceAmount'] is String) + ? double.tryParse(json['originalPriceAmount']) + : null,Optional: Consider the same defensive parsing for other numbers that can be strings across platforms (e.g., any amount fields).
♻️ Duplicate comments (2)
docs/docs/migration/from-expo-iap.md (1)
502-503: Fixed Flutter Community link — resolved.The link now points to https://flutter.dev/community, addressing the earlier review note.
lib/flutter_inapp_purchase.dart (1)
1845-1874: Avoid 30-day fallback; rely only on actual expiration when availableThis reintroduces a 30-day assumption for iOS subscription expiry when
expirationDateIOSis absent. Even with the TODO, this can produce misleading UX.Apply this diff to compute expiration only when you have a validated timestamp; otherwise leave fields null:
- if (purchase is iap_types.PurchaseIOS) { - final purchaseIOS = purchase as iap_types.PurchaseIOS; - if (purchaseIOS.expirationDateIOS != null) { - expirationDate = DateTime.fromMillisecondsSinceEpoch( - purchaseIOS.expirationDateIOS!, - ); - } else { - // Fallback to 30-day assumption for demo purposes only - expirationDate = transDate.add(const Duration(days: 30)); - } - } else { - // For regular Purchase class (not PurchaseIOS) - if (purchase.expirationDateIOS != null) { - // Purchase class has expirationDateIOS as DateTime already - expirationDate = purchase.expirationDateIOS; - } else { - // Fallback to 30-day assumption for demo purposes only - expirationDate = transDate.add(const Duration(days: 30)); - } - } + if (purchase is iap_types.PurchaseIOS && + (purchase as iap_types.PurchaseIOS).expirationDateIOS != null) { + expirationDate = DateTime.fromMillisecondsSinceEpoch( + (purchase as iap_types.PurchaseIOS).expirationDateIOS!, + ); + } else if (purchase.expirationDateIOS != null) { + // Already a DateTime on Purchase + expirationDate = purchase.expirationDateIOS; + } else { + // No validated expiration available; leave null + expirationDate = null; + } @@ - daysUntilExpiration = - expirationDate!.difference(DateTime.now()).inDays; - willExpireSoon = daysUntilExpiration <= 7; + if (expirationDate != null) { + daysUntilExpiration = + expirationDate!.difference(DateTime.now()).inDays; + willExpireSoon = daysUntilExpiration <= 7; + } else { + daysUntilExpiration = null; + willExpireSoon = null; + }If you need help pulling real expiry from receipt validation, I can sketch the integration points.
🧹 Nitpick comments (16)
docs/docs/getting-started/setup-android.md (2)
133-138: Avoid null assertion on purchaseToken; guard and early-return insteadDocs should not encourage using
!as it can crash apps when copy-pasted. Add a simple guard to keep the snippet robust.// Acknowledge a purchase -await FlutterInappPurchase.instance.acknowledgePurchase( - purchaseToken: purchase.purchaseToken!, -); +final token = purchase.purchaseToken; +if (token == null || token.isEmpty) { + // Token should be present on Android purchases, but avoid crashing in example code. + return; +} +await FlutterInappPurchase.instance.acknowledgePurchase( + purchaseToken: token, +);
145-149: Mirror the safer token handling for consumePurchaseSame rationale as above—remove the bang operator and add a defensive check to prevent copy/paste crashes.
// Consume a purchase -await FlutterInappPurchase.instance.consumePurchase( - purchaseToken: purchase.purchaseToken!, -); +final token = purchase.purchaseToken; +if (token == null || token.isEmpty) { + // Token should be present on Android consumables. + return; +} +await FlutterInappPurchase.instance.consumePurchase( + purchaseToken: token, +);docs/docs/api/methods/request-subscription.md (3)
16-25: Surface the deprecation in the signature blockYou’ve called out the deprecation in the parameter docs and examples, but the signature snippet doesn’t highlight it. Add an inline deprecation note to make it unmissable at the point of reference.
Future requestSubscription( String productId, { int? prorationModeAndroid, String? obfuscatedAccountIdAndroid, String? obfuscatedProfileIdAndroid, - String? purchaseTokenAndroid, + String? purchaseTokenAndroid, // [DEPRECATED] Use purchaseToken instead int? offerTokenIndex, })
155-172: Guard iOS-specific finish call or use the unified finisherUnconditionally calling finishTransactionIOS in a cross-platform example can confuse or cause misuse on Android. Either gate by platform or show the unified finisher API for clarity.
// Verify and activate subscription _activateSubscription(item); // Finish the transaction - _iap.finishTransactionIOS(item); + if (Platform.isIOS) { + _iap.finishTransactionIOS(item); + } else if (Platform.isAndroid) { + // Acknowledge/consume as appropriate on Android + // e.g., await _iap.acknowledgePurchaseAndroid(purchaseToken: item.purchaseToken!); + } + // Alternatively, if using the unified API: + // await _iap.finishTransaction(item, isConsumable: false);
129-150: Clarify proration deprecation and show replacementMode alternativeGiven the migration to replacementMode/AndroidReplacementMode, explicitly mark prorationModeAndroid usage as deprecated in this method and add a short “replacementMode” example using requestPurchase to steer users to the modern path.
Proposed addendum (after the “Proration Modes (Android)” header):
+Note: prorationModeAndroid and AndroidProrationMode are deprecated as of v6.3.3 and will be removed in v6.4.0. Prefer replacementModeAndroid with the requestPurchase API: + +```dart +await FlutterInappPurchase.instance.requestPurchase( + request: RequestPurchase( + android: RequestPurchaseAndroid( + skus: ['com.example.yearly'], + replacementModeAndroid: AndroidReplacementMode.withTimeProration, + purchaseTokenAndroid: monthlyToken, // [DEPRECATED] Use purchaseToken instead + ), + ), + type: PurchaseType.subs, +); +```Also applies to: 33-35, 62-65, 100-105
docs/docs/api/overview.md (1)
309-321: Consider renaming IAPError to IapError (consistency)Minor naming consistency with the rest of the docs (IapItem, etc.). If the code keeps IAPError, ignore; otherwise, update to IapError across docs.
docs/docs/api/classes/flutter-inapp-purchase.md (1)
133-137: Use enum values in the “Unified method” descriptionThe bullet list under the second requestProducts() overload still refers to string literals; switch to enum names to match the signature.
- - `type: 'inapp'` - for regular products (consumables and non-consumables) - - `type: 'subs'` - for subscription products + - `type: PurchaseType.inapp` - for regular products (consumables and non-consumables) + - `type: PurchaseType.subs` - for subscription productsAlso applies to: 124-131
docs/docs/examples/basic-store.md (1)
721-727: Prefer the unified finisher in the how-to sectionThis section can reinforce the new API by showing finishTransaction rather than platform branches.
-// iOS -await _iap.finishTransactionIOS(purchase, isConsumable: true); - -// Android -await _iap.consumePurchaseAndroid(purchaseToken: token); +await _iap.finishTransaction(purchase, isConsumable: true);docs/docs/guides/subscriptions.md (1)
148-156: Also use RequestSubscriptionAndroid for “new subscription or iOS” branch.Keep the Android side consistent with subscription flows. RequestSubscriptionAndroid requires subscriptionOffers; provide an empty list when not selecting a specific offer token.
- await iap.requestPurchase( - request: RequestPurchase( - ios: RequestPurchaseIOS(sku: subscriptionId), - android: RequestPurchaseAndroid( - skus: [subscriptionId], - obfuscatedAccountIdAndroid: await _getUserId(), - ), - ), - type: PurchaseType.subs, - ); + await iap.requestPurchase( + request: RequestPurchase( + ios: RequestPurchaseIOS(sku: subscriptionId), + android: RequestSubscriptionAndroid( + skus: [subscriptionId], + subscriptionOffers: const [], + obfuscatedAccountIdAndroid: await _getUserId(), + ), + ), + type: PurchaseType.subs, + );docs/docs/guides/products.md (1)
42-44: Prefer using PurchaseType over string literals for type.For consistency with the rest of the docs and stronger typing, use PurchaseType.inapp instead of 'inapp'.
- List<IapItem> products = await FlutterInappPurchase.instance - .requestProducts(skus: productIds, type: 'inapp'); + List<IapItem> products = await FlutterInappPurchase.instance + .requestProducts(skus: productIds, type: PurchaseType.inapp);lib/types.dart (3)
345-441: Minor: Product.toString contains branches that can’t be hit by Subscription.This is Product’s toString but it checks
this is Subscription; Subscription doesn’t extend Product. Not harmful, but dead code.- // For Subscription class, add subscription info - if (this is Subscription) { - final sub = this as Subscription; - if (sub.subscription != null) { - buffer.writeln(' subscription: ${sub.subscription},'); - } - if (sub.subscriptionGroupIdIOS != null) { - buffer.writeln( - ' subscriptionGroupIdIOS: ${sub.subscriptionGroupIdIOS},', - ); - } - if (sub.subscriptionOffersAndroid != null && - sub.subscriptionOffersAndroid!.isNotEmpty) { - buffer.writeln( - ' subscriptionOffersAndroid: ${sub.subscriptionOffersAndroid!.length} offer(s),', - ); - } - } + // Note: Subscription does not extend Product; omit Subscription-specific prints here.
856-942: Subscription.toString prints are comprehensive; consider trimming duplicates.Not critical, but the output is verbose. At minimum, avoid printing displayPrice and platform details multiple times for readability.
1776-1949: toString: remove duplicated iOS prints (currencyCodeIOS, expirationDateIOS, productTypeIOS).Several fields are printed twice, which hurts readability. Optional clean-up below.
- if (currencyCodeIOS != null) { - buffer.writeln(' currencyCodeIOS: "$currencyCodeIOS",'); - } ... - if (productTypeIOS != null) { - buffer.writeln(' productTypeIOS: "$productTypeIOS",'); - } ... - if (currencyCodeIOS != null) { - buffer.writeln(' currencyCodeIOS: "$currencyCodeIOS",'); - } ... - if (expirationDateIOS != null) { - buffer.writeln(' expirationDateIOS: $expirationDateIOS,'); - } + // avoid duplicate prints: currencyCodeIOS, productTypeIOS, expirationDateIOS already logged abovelib/flutter_inapp_purchase.dart (3)
724-787: iOS/Android Product mapping is correct; apply the same numeric parsing hardeningThe subtype split (ProductIOS vs Product) reads well and follows the naming rules. Mirror the safer
originalPriceAmountparsing here too.Apply this diff:
- originalPriceAmount: json['originalPriceAmount'] as double?, + originalPriceAmount: + (json['originalPriceAmount'] is num) + ? (json['originalPriceAmount'] as num).toDouble() + : (json['originalPriceAmount'] is String) + ? double.tryParse(json['originalPriceAmount']) + : null,
1148-1176: Back-compat wrappers (getProducts) are fine; consider preserving selected platform-specific fieldsConversion from ProductCommon to IapItem preserves core fields. For Android, you've included originalPrice and originalPriceAmount. If consumers relied on additional Android/iOS fields in IapItem JSON (e.g., displayPrice/displayName on Android or discounts on iOS), consider threading them through as well for smoother migration.
1185-1217: Back-compat wrappers (getSubscriptions) look good; small nit on iOS fieldsYou surface
discountsIOSandsubscriptionGroupIdentifierIOS. If possible, also preservedisplayPrice/displayNamesymmetry when available to help clients that display OpenIAP-aligned pricing text.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (19)
docs/blog/2025-08-19-6.3.0.release.md(2 hunks)docs/blog/2025-08-20-6.3.3.release.md(1 hunks)docs/docs/api/classes/flutter-inapp-purchase.md(7 hunks)docs/docs/api/methods/request-subscription.md(4 hunks)docs/docs/api/overview.md(10 hunks)docs/docs/examples/basic-store.md(24 hunks)docs/docs/examples/complete-implementation.md(18 hunks)docs/docs/examples/subscription-store.md(16 hunks)docs/docs/faq.md(21 hunks)docs/docs/getting-started/quickstart.md(11 hunks)docs/docs/getting-started/setup-android.md(2 hunks)docs/docs/guides/products.md(17 hunks)docs/docs/guides/subscriptions.md(25 hunks)docs/docs/guides/troubleshooting.md(1 hunks)docs/docs/migration/from-expo-iap.md(18 hunks)docs/docs/migration/from-v5.md(4 hunks)docs/docs/troubleshooting.md(3 hunks)lib/flutter_inapp_purchase.dart(32 hunks)lib/types.dart(48 hunks)
✅ Files skipped from review due to trivial changes (3)
- docs/blog/2025-08-19-6.3.0.release.md
- docs/docs/guides/troubleshooting.md
- docs/docs/troubleshooting.md
🚧 Files skipped from review as they are similar to previous changes (4)
- docs/docs/migration/from-v5.md
- docs/docs/examples/subscription-store.md
- docs/docs/getting-started/quickstart.md
- docs/docs/faq.md
🧰 Additional context used
📓 Path-based instructions (1)
**/*.dart
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.dart: For iOS-related code, when iOS is the final suffix, use the IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
For iOS-related code, when iOS is not the final suffix, use Ios (e.g., IosManager, IosHelper)
For Android-related code, use the Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)
These platform/IAP naming conventions apply to both functions and types
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Follow OpenIAP terminology for API method naming
Do not use generic prefixes like get or find; use official OpenIAP terminology instead
Files:
lib/types.dartlib/flutter_inapp_purchase.dart
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Ensure alignment with OpenIAP standards prior to implementation
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : These platform/IAP naming conventions apply to both functions and types
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Applied to files:
docs/blog/2025-08-20-6.3.3.release.mddocs/docs/guides/subscriptions.mddocs/docs/api/overview.mddocs/docs/api/classes/flutter-inapp-purchase.mddocs/docs/examples/basic-store.mddocs/docs/guides/products.mddocs/docs/migration/from-expo-iap.md
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For Android-related code, use the Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
Applied to files:
docs/blog/2025-08-20-6.3.3.release.mddocs/docs/getting-started/setup-android.mddocs/docs/api/methods/request-subscription.mddocs/docs/guides/subscriptions.mdlib/types.dartdocs/docs/api/overview.mddocs/docs/examples/complete-implementation.mddocs/docs/api/classes/flutter-inapp-purchase.mddocs/docs/guides/products.mddocs/docs/migration/from-expo-iap.md
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)
Applied to files:
docs/blog/2025-08-20-6.3.3.release.mddocs/docs/getting-started/setup-android.mddocs/docs/guides/subscriptions.mdlib/types.dartlib/flutter_inapp_purchase.dartdocs/docs/api/overview.mddocs/docs/examples/complete-implementation.mddocs/docs/api/classes/flutter-inapp-purchase.mddocs/docs/examples/basic-store.mddocs/docs/guides/products.mddocs/docs/migration/from-expo-iap.md
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : These platform/IAP naming conventions apply to both functions and types
Applied to files:
docs/docs/guides/subscriptions.mdlib/types.dartlib/flutter_inapp_purchase.dartdocs/docs/api/overview.mddocs/docs/examples/complete-implementation.mddocs/docs/api/classes/flutter-inapp-purchase.mddocs/docs/examples/basic-store.mddocs/docs/guides/products.mddocs/docs/migration/from-expo-iap.md
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : Follow OpenIAP terminology for API method naming
Applied to files:
lib/types.dartlib/flutter_inapp_purchase.dartdocs/docs/api/overview.mddocs/docs/examples/complete-implementation.mddocs/docs/api/classes/flutter-inapp-purchase.mddocs/docs/examples/basic-store.mddocs/docs/guides/products.mddocs/docs/migration/from-expo-iap.md
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Applies to **/*.dart : For iOS-related code, when iOS is the final suffix, use the IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
Applied to files:
lib/types.dartdocs/docs/api/overview.mddocs/docs/migration/from-expo-iap.md
📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-18T17:24:36.721Z
Learning: Ensure alignment with OpenIAP standards prior to implementation
Applied to files:
docs/docs/examples/complete-implementation.md
🪛 LanguageTool
docs/blog/2025-08-20-6.3.3.release.md
[grammar] ~24-~24: There might be a mistake here.
Context: ...deenum →AndroidReplacementMode` enum - Updated enum values for better clarity: ...
(QB_NEW_EN)
[grammar] ~77-~77: There might be a mistake here.
Context: ...id**: The product name from Google Play - **oneTimePurchaseOfferDetailsAndroid`**: Structured pricing details including: ...
(QB_NEW_EN)
[grammar] ~78-~78: There might be a mistake here.
Context: ...*: Structured pricing details including: - priceCurrencyCode: ISO 4217 currency code - `formattedP...
(QB_NEW_EN)
[grammar] ~79-~79: There might be a mistake here.
Context: ...iceCurrencyCode: ISO 4217 currency code - formattedPrice: Formatted price string - priceAmoun...
(QB_NEW_EN)
[grammar] ~80-~80: There might be a mistake here.
Context: ...formattedPrice: Formatted price string - priceAmountMicros: Price in micro-units These fields ens...
(QB_NEW_EN)
[grammar] ~95-~95: There might be a mistake here.
Context: ...ment Mode - ❌ prorationMode parameter - ❌ prorationModeAndroid parameter - ❌ `...
(QB_NEW_EN)
[grammar] ~96-~96: There might be a mistake here.
Context: ...ter - ❌ prorationModeAndroid parameter - ❌ AndroidProrationMode enum - ✅ Use `r...
(QB_NEW_EN)
[grammar] ~97-~97: There might be a mistake here.
Context: ...arameter - ❌ AndroidProrationMode enum - ✅ Use replacementMode, `replacementMod...
(QB_NEW_EN)
docs/docs/api/overview.md
[grammar] ~137-~137: There might be a mistake here.
Context: ...: - skus: List of product identifiers - type: Product type - 'inapp' for regular p...
(QB_NEW_EN)
docs/docs/api/classes/flutter-inapp-purchase.md
[style] ~41-~41: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...onnectionResult> connectionUpdated ``` Stream that emits connection state updates. `...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~47-~47: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...t Stream<String?> purchasePromoted ``` Stream that emits promoted product IDs (iOS on...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~53-~53: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...t Stream<int?> inAppMessageAndroid ``` Stream that emits in-app message codes (Androi...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~93-~93: To form a complete sentence, be sure to include a subject.
Context: ...tion to the platform's billing service. Should be called when the app no longer needs ...
(MISSING_IT_THERE)
[grammar] ~169-~169: There might be a mistake here.
Context: ...orm-specific purchase request parameters - type: Type of purchase (inapp or subscriptio...
(QB_NEW_EN)
[grammar] ~196-~196: There might be a mistake here.
Context: ...** - purchase: The purchase to finish - isConsumable: Whether the product is consumable (And...
(QB_NEW_EN)
docs/docs/examples/basic-store.md
[grammar] ~703-~703: There might be a mistake here.
Context: ...ptions - Product IDs must be configured in store console ### 3. Purchase Flow ``...
(QB_NEW_EN)
[grammar] ~730-~730: There might be a mistake here.
Context: ...finishTransactionIOS for all purchases - Android: consumePurchaseAndroid for co...
(QB_NEW_EN)
[grammar] ~745-~745: There might be a mistake here.
Context: ...ual product IDs 2. Configure Stores: - iOS: Add products to App Store Connect ...
(QB_NEW_EN)
docs/docs/guides/products.md
[grammar] ~15-~15: There might be a mistake here.
Context: ...times: - Virtual currency (coins, gems) - Power-ups or boosters - Extra lives or h...
(QB_NEW_EN)
[grammar] ~16-~16: There might be a mistake here.
Context: ...cy (coins, gems) - Power-ups or boosters - Extra lives or hints ### Non-Consumable...
(QB_NEW_EN)
docs/docs/migration/from-expo-iap.md
[grammar] ~16-~16: There might be a mistake here.
Context: ...** - Hook-based architecture (useIAP) - Built for Expo/React Native ecosystem - ...
(QB_NEW_EN)
[grammar] ~17-~17: There might be a mistake here.
Context: ...k-based architecture (useIAP) - Built for Expo/React Native ecosystem - JavaScrip...
(QB_NEW_EN)
[grammar] ~17-~17: There might be a mistake here.
Context: ... - Built for Expo/React Native ecosystem - JavaScript/TypeScript **flutter_inapp_p...
(QB_NEW_EN)
[grammar] ~22-~22: There might be a mistake here.
Context: ...Flutter):** - Stream-based architecture - Built for Flutter ecosystem - Dart langu...
(QB_NEW_EN)
[grammar] ~23-~23: There might be a mistake here.
Context: ...chitecture - Built for Flutter ecosystem - Dart language ## API Comparison ### In...
(QB_NEW_EN)
[grammar] ~440-~440: There might be a mistake here.
Context: ...o-iap:** - Auto-acknowledgment handling - Simplified billing flow **flutter_inapp...
(QB_NEW_EN)
🔇 Additional comments (25)
docs/docs/api/classes/flutter-inapp-purchase.md (1)
199-206: Type consistency: Purchase vs PurchasedItemThese methods return List, but the rest of the examples use PurchasedItem. Ensure both the return types and example handlers use the same type across the docs.
Would you like me to scan and propose a consistent set (Purchase or PurchasedItem) across the docs pages?
Also applies to: 211-214
docs/blog/2025-08-20-6.3.3.release.md (1)
31-63: Migration examples are clear and aligned with v6 APIThe before/after blocks accurately demonstrate the enum rename and the RequestPurchase shape with replacementModeAndroid. The deprecation note for purchaseTokenAndroid is helpful.
docs/docs/guides/subscriptions.md (2)
58-82: IapItem rename looks consistent across samples.List usage and the updated _extractPrice signature align with the repo-wide rename. No issues spotted here.
699-699: Doc link looks good and consistent with API naming.docs/docs/guides/products.md (2)
136-144: Ack/consume flow uses purchaseToken correctly.Android examples reflect the unified purchaseToken field. Looks good.
183-205: Server-side verification: Android receipt source updated correctly.Using item.purchaseToken aligns with the new mapping; the payload shape is reasonable for server verification.
docs/docs/examples/complete-implementation.md (3)
106-113: getProducts/getSubscriptions return types updated to List.Matches the rename and the broader API updates in this PR. Good.
196-211: Android completion paths correctly use purchaseToken for consume/acknowledge.Completion flow is consistent and correct given the new mappings.
248-256: ValidationResult constructors are clear and safe.The valid/invalid factory pattern is straightforward and idiomatic.
docs/docs/migration/from-expo-iap.md (2)
56-71: Initialization sample aligns with IapItem and getProducts updates.Showing List and getProducts usage is consistent with repo-wide changes.
96-101: Using requestProducts with type 'inapp'/'subs' is consistent with migration guidance.Docs match the surrounding guides; no issues.
lib/types.dart (8)
248-343: JSON parsing for Product aligns with OpenIAP and Android suffix fields.
- nameAndroid and oneTimePurchaseOfferDetailsAndroid are parsed under Android with correct typing.
- originalPriceAmount safely handles int/double via num?.toDouble().
- subscriptionOfferDetailsAndroid fallback to legacy subscriptionOfferDetails is a solid compatibility bridge.
459-614: toJson platform segregation looks correct and TypeScript-friendly.
- iOS vs Android suffix fields are gated by platformEnum and filtered out with endsWith('IOS'/'Android') as appropriate.
- displayPrice/localizedPrice output is guarded to avoid duplication.
- subscriptionOfferDetailsAndroid is always used in the Android JSON, with a legacy fallback. This meets the PR objective.
770-854: Subscription.fromJson uses the corrected Android fields and safe numeric parsing.The Android pieces mirror Product.fromJson and apply num?.toDouble() to originalPriceAmount. Looks good.
960-1113: Subscription.toJson mirrors Product.toJson with proper platform-specific fields.Filtering by suffix and providing both new and legacy Android fields in a compatible way adheres to the PR goals.
1470-1518: PricingPhase micros conversion and TS field names are handled correctly.
- priceAmountMicros is serialized as string via toStringAsFixed(0), matching typical Android/TS expectations.
- RecurrenceMode index mapping is safe.
1673-1773: Purchase.fromJson applies safe conversions and corrected dataAndroid mapping.
- transactionDate accepts int or string.
- priceIOS parsed via num?.toDouble().
- dataAndroid now mapped from JSON directly (fix from PR objective). Good.
2211-2240: Good: RequestSubscriptionAndroid asserts token presence when replacementModeAndroid is used.This prevents invalid replacement flows at call sites and aligns with Play Billing requirements.
3489-3554: Purchase.toOpenIapFormat normalizes purchaseState to index and segregates platform fields.This matches OpenIAP expectations and fixes the earlier ambiguity around enum serialization and platform key duplication.
lib/flutter_inapp_purchase.dart (6)
181-214: requestProducts routing and JSON handling look goodPlatform routing (iOS: getItems; Android: getProducts/getSubscriptions) and robust decoding to List<Map<String,dynamic>> are correct and align with OpenIAP expectations.
Also applies to: 225-242
651-663: Platform detection fallback is sensibleDeriving platform from payload when present and falling back to the current device is reasonable and keeps JSON-source agnostic behavior.
851-889: Pricing phases parsing handles Android payload variants wellGood job normalizing
priceAmountMicrosfrom both numeric and string inputs and mappingrecurrenceModeto the public enum. This will prevent subtle parsing bugs across BillingClient versions.
1279-1317: Android replacement mode validation is correct and messages are actionableGood enforcement of token requirement when using replacement modes, and the error message clearly directs developers to the remedy. The effectiveReplacementMode cascade maintains backward compatibility.
1663-1664: Explicitly pass replacementMode -1 for in-app purchasesSetting replacementMode to -1 for non-sub purchases is correct to avoid unintended replacement behavior. Nice touch.
339-347: Android: replacementMode usage aligns with requestSubscription; good consistencyPassing
replacementModeAndroidto requestSubscription and settingreplacementMode: -1for in-app purchase ensure consistent semantics across entry points.Also applies to: 361-364
… in documentation, use PurchaseType enum instead of strings, update finishTransaction signature, migrate examples to requestProducts API, and remove verbose debug logging
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
docs/docs/examples/basic-store.md (2)
251-260: Update _verifyPurchase to accept Purchase and handle platform-specific receipt/token safely.The signature and fields reflect the old PurchasedItem. Also, substring on a possibly null/short receipt can throw.
Apply:
- Future<bool> _verifyPurchase(PurchasedItem purchase) async { + Future<bool> _verifyPurchase(Purchase purchase) async { // In a real app, send the receipt to your server for verification // For this example, we'll just simulate a successful verification await Future.delayed(Duration(milliseconds: 500)); - print('🔍 Verifying purchase: ${purchase.productId}'); - print('Receipt: ${purchase.transactionReceipt?.substring(0, 50)}...'); + print('🔍 Verifying purchase: ${purchase.productId}'); + if (Platform.isIOS) { + // NOTE: Confirm exact property name on Purchase for iOS receipts in this release. + // Many implementations expose `transactionReceipt`. Adjust if the field was renamed. + final receipt = purchase.transactionReceipt; + if (receipt != null && receipt.isNotEmpty) { + final preview = receipt.length > 50 ? '${receipt.substring(0, 50)}...' : receipt; + print('Receipt (iOS): $preview'); + } else { + print('Receipt (iOS): <none>'); + } + } else if (Platform.isAndroid) { + // Per PR: Purchase.dataAndroid mapping fixed; use purchaseToken from dataAndroid + final token = purchase.dataAndroid?.purchaseToken; + final orderId = purchase.dataAndroid?.orderId; + print('Token (Android): ${token ?? "<none>"}'); + print('OrderId (Android): ${orderId ?? "<none>"}'); + } return true; // Assume verification successful }Note: If the iOS receipt property name differs (e.g., receiptIOS), update accordingly.
296-322: Update _finishTransaction to accept Purchase and use Android token from dataAndroid.The current signature uses PurchasedItem and reads purchaseToken directly, which is outdated per your PR notes.
Apply:
- Future<void> _finishTransaction(PurchasedItem purchase) async { + Future<void> _finishTransaction(Purchase purchase) async { try { if (Platform.isAndroid) { - // For Android, consume the purchase if it's a consumable product - if (purchase.purchaseToken != null) { - await _iap.consumePurchaseAndroid( - purchaseToken: purchase.purchaseToken!, - ); + // For Android, consume the purchase if it's a consumable product + final token = purchase.dataAndroid?.purchaseToken; + if (token != null && _isConsumableProduct(purchase.productId)) { + await _iap.consumePurchaseAndroid(purchaseToken: token); print('✅ Android purchase consumed'); } } else if (Platform.isIOS) { // For iOS, finish the transaction - await _iap.finishTransactionIOS( - purchase, - isConsumable: _isConsumableProduct(purchase.productId), - ); + await _iap.finishTransactionIOS( + purchase, + isConsumable: _isConsumableProduct(purchase.productId), + ); print('✅ iOS transaction finished'); } setState(() { _latestPurchase = null; }); } catch (e) { _showError('Failed to finish transaction: $e'); } }If finishTransactionIOS expects a different parameter shape in 6.3.3 (e.g., transactionId), adjust accordingly.
docs/docs/api/overview.md (3)
51-73: Fix IapItem fields: remove purchase-only Android fields and reflect actual backward-compat surface.IapItem here documents fields that belong to purchases (e.g., signatureAndroid, originalJsonAndroid). In code, IapItem is only produced by deprecated wrappers with a minimal product surface (productId/title/description/price/localizedPrice/currency, and sometimes originalPrice fields).
Proposed update:
class IapItem { final String? productId; final String? price; final String? currency; final String? localizedPrice; final String? title; final String? description; - // iOS specific fields - final String? introductoryPrice; - final String? introductoryPricePaymentModeIOS; - final String? introductoryPriceNumberOfPeriodsIOS; - final String? introductoryPriceSubscriptionPeriodIOS; - final String? subscriptionPeriodNumberIOS; - final String? subscriptionPeriodUnitIOS; - - // Android specific fields - final String? signatureAndroid; - final String? originalJsonAndroid; - final String? developerPayloadAndroid; - final String? isConsumableAndroid; + // Note: This legacy type is returned only by deprecated wrappers. + // Platform-specific fields are limited; subscription details are available via ProductCommon. + // For Android, some wrappers may also populate: + // final String? originalPrice; + // final double? originalPriceAmount; }And add a short note below the block:
- “For full product detail (including subscription offer details, pricing phases, etc.), use requestProducts(RequestProductsParams) which returns ProductCommon (Product or Subscription).”
281-285: validateReceiptIos signature mismatch.Code returns http.Response and expects Map<String, String> for receiptBody. The docs show Map<String, dynamic>? return and String receiptBody.
-// Validate receipt locally -Future<Map<String, dynamic>?> validateReceiptIos({ - required String receiptBody, - bool isTest = true, -}) async +// Validate receipt with Apple's servers +Future<http.Response> validateReceiptIos({ + required Map<String, String> receiptBody, + bool isTest = true, +}) async
311-320: Outdated error type: replace IAPError class with ErrorCode enum or PurchaseError.The codebase uses ErrorCode (enum) and PurchaseError objects, not an IAPError class with string constants.
-```dart -class IAPError { - static const String E_UNKNOWN = 'E_UNKNOWN'; - static const String E_USER_CANCELLED = 'E_USER_CANCELLED'; - static const String E_NETWORK = 'E_NETWORK'; - static const String E_ITEM_UNAVAILABLE = 'E_ITEM_UNAVAILABLE'; - static const String E_REMOTE_ERROR = 'E_REMOTE_ERROR'; - static const String E_NOT_PREPARED = 'E_NOT_PREPARED'; - static const String E_ALREADY_OWNED = 'E_ALREADY_OWNED'; -} -``` +```dart +enum ErrorCode { + eUnknown, + eUserCancelled, + eNetwork, + eItemUnavailable, + eRemoteError, + eNotPrepared, + eAlreadyOwned, + eServiceError, + eDeveloperError, + eBillingUnavailable, + eProductAlreadyOwned, + ePurchaseNotAllowed, +} +``` +Errors are surfaced as PurchaseError with a code: ErrorCode and message/debugMessage.lib/flutter_inapp_purchase.dart (2)
1383-1390: PII leakage: mask purchaseToken when logging (consumption).This branch logs the full purchase token, which is sensitive. Mask similarly to the acknowledge path and guard with kDebugMode.
- if (isConsumable) { - debugPrint( - '[FlutterInappPurchase] Android: Consuming product with token: ${purchase.purchaseToken}', - ); + if (isConsumable) { + if (kDebugMode) { + final maskedToken = + (purchase.purchaseToken ?? '').replaceAll(RegExp(r'.(?=.{4})'), '*'); + debugPrint( + '[FlutterInappPurchase] Android: Consuming product with token: $maskedToken', + ); + } await _channel.invokeMethod('consumeProduct', <String, dynamic>{ 'purchaseToken': purchase.purchaseToken, });
1570-1580: Close all controllers in _removePurchaseListener to avoid leaks.Currently only purchase and purchaseError controllers are closed. connectionUpdated, purchasePromoted, and onInAppMessage remain open, leaking listeners across init/end cycles.
Future<dynamic> _removePurchaseListener() async { _purchaseController ?..add(null) ..close(); _purchaseController = null; _purchaseErrorController ?..add(null) ..close(); _purchaseErrorController = null; + + _connectionController + ?..add(iap_types.ConnectionResult( + connected: false, + message: 'Disconnected', + )) + ..close(); + _connectionController = null; + + _purchasePromotedController + ?..add(null) + ..close(); + _purchasePromotedController = null; + + _onInAppMessageController + ?..add(null) + ..close(); + _onInAppMessageController = null; }docs/docs/api/classes/flutter-inapp-purchase.md (1)
281-289: validateReceiptAndroid return type mismatch.Implementation returns Future<Map<String, dynamic>?>, not http.Response.
-```dart -Future<http.Response> validateReceiptAndroid({ +```dart +Future<Map<String, dynamic>?> validateReceiptAndroid({ required String packageName, required String productId, required String productToken, required String accessToken, - bool isSubscription = false, + bool isSub = false, })</blockquote></details> </blockquote></details>♻️ Duplicate comments (4)
docs/docs/examples/basic-store.md (3)
166-170: Great: Migrated to requestProducts API with explicit type.This replaces the deprecated getProducts usage and specifies PurchaseType.inapp as discussed previously.
653-655: Nice: Prevented null-assertion crash on productId.Button is disabled when productId is null, avoiding a crash.
697-701: Docs snippet correctly shows requestProducts with type.Matches the implementation above and removes deprecated usage.
docs/docs/api/overview.md (1)
145-153: Good: examples use PurchaseType enum instead of raw strings.This addresses previous feedback to avoid string literals 'inapp'/'subs'.
🧹 Nitpick comments (11)
docs/docs/examples/basic-store.md (6)
223-247: Consistent error code property across docs.Here you use error.responseCode, but later in the “Customization Options” snippet you reference error.code. Keep it consistent with the actual API.
I proposed a diff below (lines 771-779) to use error.responseCode in the snippet as well. Please verify which property is canonical in this release and unify both locations accordingly.
371-401: Restore flow: iOS-specific retrieval; clarify/branch Android behavior.You call getAvailableItemsIOS unconditionally. On Android, this likely returns nothing or isn’t appropriate. Consider platform checks and, if available, an Android counterpart or a generic getAvailablePurchases.
For safety and clarity:
try { await _iap.restorePurchases(); - - // Get available purchases - final availablePurchases = await _iap.getAvailableItemsIOS(); + // Get available purchases (platform-specific) + final availablePurchases = Platform.isIOS + ? await _iap.getAvailableItemsIOS() + : null; // TODO: If a cross-platform or Android-specific API exists, use it here.Would you like me to scan the repo for an Android/getAvailablePurchases API and propose the exact code?
771-779: Unify error code access in docs: use responseCode (matches implementation above).The earlier handler uses error.responseCode; this snippet uses error.code. Keep them consistent.
Apply:
-void _handlePurchaseError(PurchaseError error) { - switch (error.code) { +void _handlePurchaseError(PurchaseError error) { + switch (error.responseCode) { case 1: /* User cancelled */ case 2: /* Network error */ case 7: /* Already owned */ // Add your custom error handling } }
703-706: Minor wording: add “the” before “store console”.Improves readability.
Apply:
-- Product IDs must be configured in store console +- Product IDs must be configured in the store console
731-734: Clarify Android phrasing in bullet list.Slight rewording for clarity.
Apply:
-- Android: `consumePurchaseAndroid` for consumables +- Android: Use `consumePurchaseAndroid` for consumables
746-747: List formatting: ensure spacing/numbering renders properly.Markdown lists sometimes merge when numbers follow text without a blank line; adding a blank line avoids rendering hiccups.
Apply:
-1. **Replace Product IDs**: Update `_productIds` with your actual product IDs -2. **Configure Stores**: +1. **Replace Product IDs**: Update `_productIds` with your actual product IDs + +2. **Configure Stores**:lib/flutter_inapp_purchase.dart (5)
195-198: Gate verbose logs behind kDebugMode to avoid noise in release.requestProducts logs can be noisy; gate to debug builds.
- debugPrint( + if (kDebugMode) { + debugPrint( '[flutter_inapp_purchase] requestProducts called with productIds: ${params.productIds}', - ); + ); + }
225-228: Same for “Received N items” log.- debugPrint( - '[flutter_inapp_purchase] Received ${result.length} items from native', - ); + if (kDebugMode) { + debugPrint( + '[flutter_inapp_purchase] Received ${result.length} items from native', + ); + }
1416-1421: Gate iOS finish log behind kDebugMode to avoid exposing transaction IDs.- debugPrint( - '[FlutterInappPurchase] iOS: Finishing transaction with ID: $transactionId', - ); + if (kDebugMode) { + final masked = + transactionId.replaceAll(RegExp(r'.(?=.{4})'), '*'); + debugPrint( + '[FlutterInappPurchase] iOS: Finishing transaction with ID: $masked', + ); + }
398-404: Unused parameter: replacementMode in requestPurchaseAuto.
replacementModeis accepted but never used. Either wire it through or remove it to avoid confusion.- @Deprecated('Use replacementMode instead') int? prorationMode, - int? replacementMode, + @Deprecated('Use replacementMode instead') int? prorationMode, + // int? replacementMode, // Not used; remove or implement if neededOr, if intended for Android in-app (which does not support replacement), document it as ignored for non-subscriptions.
1757-1816: Subscription expiry fallback is marked TODO; consider gating “willExpireSoon” when expiry unknown.Where expirationDate cannot be determined, defaulting to 30 days can mislead UI. Prefer leaving willExpireSoon null (unknown) unless an actual expiry is available.
- // Fallback to 30-day assumption for demo purposes only - expirationDate = transDate.add(const Duration(days: 30)); + // No validated expiry — leave null to indicate unknown + expirationDate = null; ... - daysUntilExpiration = - expirationDate!.difference(DateTime.now()).inDays; - willExpireSoon = daysUntilExpiration <= 7; + if (expirationDate != null) { + daysUntilExpiration = + expirationDate!.difference(DateTime.now()).inDays; + willExpireSoon = daysUntilExpiration <= 7; + } else { + daysUntilExpiration = null; + willExpireSoon = null; + }📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
docs/docs/api/classes/flutter-inapp-purchase.md(7 hunks)docs/docs/api/overview.md(9 hunks)docs/docs/examples/basic-store.md(21 hunks)lib/flutter_inapp_purchase.dart(32 hunks)🧰 Additional context used
📓 Path-based instructions (1)
**/*.dart
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.dart: For iOS-related code, when iOS is the final suffix, use the IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
For iOS-related code, when iOS is not the final suffix, use Ios (e.g., IosManager, IosHelper)
For Android-related code, use the Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)
These platform/IAP naming conventions apply to both functions and types
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Follow OpenIAP terminology for API method naming
Do not use generic prefixes like get or find; use official OpenIAP terminology insteadFiles:
lib/flutter_inapp_purchase.dart🧠 Learnings (8)
📓 Common learnings
Learnt from: CR PR: hyochan/flutter_inapp_purchase#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-18T17:24:36.721Z Learning: Ensure alignment with OpenIAP standards prior to implementationLearnt from: CR PR: hyochan/flutter_inapp_purchase#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-18T17:24:36.721Z Learning: Applies to **/*.dart : For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)Learnt from: CR PR: hyochan/flutter_inapp_purchase#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-18T17:24:36.721Z Learning: Applies to **/*.dart : These platform/IAP naming conventions apply to both functions and types📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR PR: hyochan/flutter_inapp_purchase#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-18T17:24:36.721Z Learning: Applies to **/*.dart : For IAP-related code, when IAP is not the final suffix, use Iap (e.g., IapPurchase; not IAPPurchase)Applied to files:
docs/docs/examples/basic-store.mddocs/docs/api/overview.mdlib/flutter_inapp_purchase.dartdocs/docs/api/classes/flutter-inapp-purchase.md📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR PR: hyochan/flutter_inapp_purchase#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-18T17:24:36.721Z Learning: Applies to **/*.dart : Follow OpenIAP terminology for API method namingApplied to files:
docs/docs/examples/basic-store.mddocs/docs/api/overview.mdlib/flutter_inapp_purchase.dartdocs/docs/api/classes/flutter-inapp-purchase.md📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR PR: hyochan/flutter_inapp_purchase#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-18T17:24:36.721Z Learning: Applies to **/*.dart : These platform/IAP naming conventions apply to both functions and typesApplied to files:
docs/docs/examples/basic-store.mddocs/docs/api/overview.mdlib/flutter_inapp_purchase.dartdocs/docs/api/classes/flutter-inapp-purchase.md📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR PR: hyochan/flutter_inapp_purchase#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-18T17:24:36.721Z Learning: Applies to **/*.dart : Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)Applied to files:
docs/docs/examples/basic-store.mddocs/docs/api/overview.mddocs/docs/api/classes/flutter-inapp-purchase.md📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR PR: hyochan/flutter_inapp_purchase#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-18T17:24:36.721Z Learning: Applies to **/*.dart : For Android-related code, use the Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)Applied to files:
docs/docs/examples/basic-store.mddocs/docs/api/overview.mddocs/docs/api/classes/flutter-inapp-purchase.md📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR PR: hyochan/flutter_inapp_purchase#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-18T17:24:36.721Z Learning: Applies to **/*.dart : Do not use generic prefixes like get or find; use official OpenIAP terminology insteadApplied to files:
docs/docs/api/overview.md📚 Learning: 2025-08-18T17:24:36.721Z
Learnt from: CR PR: hyochan/flutter_inapp_purchase#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-18T17:24:36.721Z Learning: Applies to **/*.dart : For iOS-related code, when iOS is the final suffix, use the IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)Applied to files:
docs/docs/api/overview.md🪛 LanguageTool
docs/docs/examples/basic-store.md
[grammar] ~705-~705: There might be a mistake here.
Context: ...ptions - Product IDs must be configured in store console ### 3. Purchase Flow ``...(QB_NEW_EN)
[grammar] ~732-~732: There might be a mistake here.
Context: ...finishTransactionIOSfor all purchases - Android:consumePurchaseAndroidfor co...(QB_NEW_EN)
[grammar] ~747-~747: There might be a mistake here.
Context: ...ual product IDs 2. Configure Stores: - iOS: Add products to App Store Connect ...(QB_NEW_EN)
docs/docs/api/overview.md
[grammar] ~138-~138: There might be a mistake here.
Context: ...: -skus: List of product identifiers -type: Product type -PurchaseType.inapp(r...(QB_NEW_EN)
docs/docs/api/classes/flutter-inapp-purchase.md
[style] ~41-~41: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...onnectionResult> connectionUpdated ``` Stream that emits connection state updates. `...(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~47-~47: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...t Stream<String?> purchasePromoted ``` Stream that emits promoted product IDs (iOS on...(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~53-~53: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...t Stream<int?> inAppMessageAndroid ``` Stream that emits in-app message codes (Androi...(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~93-~93: To form a complete sentence, be sure to include a subject.
Context: ...tion to the platform's billing service. Should be called when the app no longer needs ...(MISSING_IT_THERE)
[grammar] ~169-~169: There might be a mistake here.
Context: ...orm-specific purchase request parameters -type: Type of purchase (inapp or subscriptio...(QB_NEW_EN)
[grammar] ~196-~196: There might be a mistake here.
Context: ...** -purchase: The purchase to finish -isConsumable: Whether the product is consumable (And...(QB_NEW_EN)
⏰ 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: build
🔇 Additional comments (8)
docs/docs/examples/basic-store.md (3)
67-73: Good alignment with OpenIAP naming and types.Switching to IapItem for products and using Purchase/PurchaseError for streams matches the PR’s OpenIAP alignment and the retrieved naming learnings. Looks consistent.
111-126: Listener setup looks correct and robust.The success and error streams are wired to strongly-typed handlers, and errors are surfaced via _showError. This is clean and aligns with the updated APIs.
724-729: Docs snippet looks good; confirm parameter shape for finishTransactionIOS.If finishTransactionIOS expects Purchase in 6.3.3, you’re aligned. If it expects a transaction identifier or a different object, update both snippet and implementation to match.
Do you want me to audit lib/public API in this branch and confirm exact method signatures?
docs/docs/api/overview.md (2)
40-43: Good: instance-based expo-iap compatible streams.purchaseUpdatedListener and purchaseErrorListener are documented as instance getters and match the implementation.
104-121: Update documentation to use initConnection()The
initialize()method no longer exists—replace it with the newinitConnection()API.• File:
docs/docs/api/overview.md
• Lines: 104–121-#### initialize() - -```dart -Future<String?> initialize() async -``` - -Establishes connection to the store. Must be called before any other methods. - -**Returns**: Connection result message or error - -**Example**: - -```dart -String? result = await FlutterInappPurchase.instance.initialize(); -if (result == 'Billing is unavailable') { - // Handle unavailable billing -} -``` +#### initConnection() + +```dart +Future<bool> initConnection() async +``` + +Establishes connection to the store. Must be called before any other methods. + +**Returns**: `true` if initialized successfully; throws a `PurchaseError` on failure. + +**Example**: + +```dart +final initialized = await FlutterInappPurchase.instance.initConnection(); +if (!initialized) { + // Handle unavailable billing +} +```lib/flutter_inapp_purchase.dart (1)
760-772: No field-name mismatch—each constructor uses its own parameter name
Both constructors intentionally differ:
- In
Subscription, the constructor signature isSubscription({…, required IapPlatform platform, …}), so it acceptsplatform: ….- In
Product, the constructor signature isProduct({…, String? platform, …, IapPlatform? platformEnum, …}), so it acceptsplatformEnum: ….Your calls in
flutter_inapp_purchase.dartare correct (platform:forSubscription,platformEnum:forProduct). No change needed.docs/docs/api/classes/flutter-inapp-purchase.md (2)
99-103: Good: primary requestProducts signature matches ProductCommon + params.This aligns with the new types and parsing path.
56-68: LGTM: expo-iap compatible instance streams are clearly documented.The additional listener streams and migration notes improve discoverability.
| ```dart | ||
| Future<void> initConnection() | ||
| ``` | ||
|
|
||
| Initializes the connection to the platform's billing service. Must be called before any other purchase-related operations. | ||
|
|
||
| **Throws:** | ||
|
|
||
| - `PurchaseError` with `E_ALREADY_INITIALIZED` if already initialized | ||
| - `PurchaseError` with `E_NOT_INITIALIZED` if initialization fails | ||
|
|
||
| #### endConnection() | ||
|
|
||
| ```dart | ||
| Future<void> endConnection() | ||
| ``` | ||
|
|
There was a problem hiding this comment.
initConnection/endConnection return types should be bool, not void.
The lib returns Future for both.
-```dart
-Future<void> initConnection()
-```
+```dart
+Future<bool> initConnection()
+```
...
-```dart
-Future<void> endConnection()
-```
+```dart
+Future<bool> endConnection()
+```🤖 Prompt for AI Agents
In docs/docs/api/classes/flutter-inapp-purchase.md around lines 76 to 92, the
documented return types for initConnection and endConnection are mistakenly
listed as Future<void>; update both method signatures and any inline code blocks
to show Future<bool> instead (i.e., change "Future<void> initConnection()" to
"Future<bool> initConnection()" and "Future<void> endConnection()" to
"Future<bool> endConnection()") so the docs match the library's actual return
types.
| ```dart | ||
| Future<List<IAPItem>> requestProducts({ | ||
| Future<List<IapItem>> requestProducts({ | ||
| required List<String> skus, | ||
| String type = 'inapp', | ||
| PurchaseType type = PurchaseType.inapp, | ||
| }) | ||
| ``` | ||
|
|
||
| Unified method to retrieve products or subscriptions. |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Remove legacy secondary overload of requestProducts from class docs (not implemented).
The class does not implement a second overload returning List with raw args. This causes confusion.
-```dart
-Future<List<IapItem>> requestProducts({
- required List<String> skus,
- PurchaseType type = PurchaseType.inapp,
-})
-```
-
-Unified method to retrieve products or subscriptions.
-
-- `type: 'inapp'` - for regular products (consumables and non-consumables)
-- `type: 'subs'` - for subscription products
+// Use the params-based overload above. For legacy wrappers, see getProducts/getSubscriptions (deprecated).🤖 Prompt for AI Agents
In docs/docs/api/classes/flutter-inapp-purchase.md around lines 126 to 133,
remove the legacy secondary overload block for requestProducts that is not
implemented and replace it with a single-line note directing users to the
params-based overload and legacy wrappers; specifically delete the unused dart
code block and the following explanatory bullets, and insert the comment "Use
the params-based overload above. For legacy wrappers, see
getProducts/getSubscriptions (deprecated)." ensuring Markdown code
fence/formatting remains valid.
| #### finishTransaction() | ||
|
|
||
| ```dart | ||
| Future<String?> finishTransaction(Purchase purchase, {bool isConsumable = false}) | ||
| ``` | ||
|
|
||
| Completes a transaction and removes it from the queue. |
There was a problem hiding this comment.
finishTransaction return type mismatch.
Update to Future to match implementation.
-```dart
-Future<String?> finishTransaction(Purchase purchase, {bool isConsumable = false})
-```
+```dart
+Future<void> finishTransaction(Purchase purchase, {bool isConsumable = false})
+```🤖 Prompt for AI Agents
In docs/docs/api/classes/flutter-inapp-purchase.md around lines 186 to 192, the
documented signature for finishTransaction shows Future<String?> but the
implementation returns Future<void>; update the doc block to use Future<void>
and adjust the fenced code block content accordingly so the declared return type
matches the implementation.
| ```dart | ||
| Future<AppStoreInfo?> getStorefrontIOS() | ||
| ``` | ||
|
|
||
| Retrieves the current App Store storefront information. |
There was a problem hiding this comment.
🛠️ Refactor suggestion
getStorefrontIOS return type mismatch.
Implementation returns a storefront countryCode as String. Adjust docs accordingly.
-```dart
-Future<AppStoreInfo?> getStorefrontIOS()
-```
+```dart
+Future<String> getStorefrontIOS()
+```🤖 Prompt for AI Agents
In docs/docs/api/classes/flutter-inapp-purchase.md around lines 239 to 243, the
documented signature and description are wrong: the implementation returns a
storefront countryCode as a String, not an AppStoreInfo. Update the Dart code
block signature to "Future<String> getStorefrontIOS()" and change the short
description below to state that it returns the current App Store storefront
countryCode as a String (or null if not available) to match the implementation.
| ```dart | ||
| Future<void> deepLinkToSubscriptionsAndroid({ | ||
| required String sku, | ||
| required String packageName, | ||
| }) | ||
| ``` | ||
|
|
||
| Opens the Google Play subscription management screen for a specific product. |
There was a problem hiding this comment.
🛠️ Refactor suggestion
deepLinkToSubscriptionsAndroid signature mismatch.
The lib method only accepts an optional sku and does not require packageName. Update docs.
-```dart
-Future<void> deepLinkToSubscriptionsAndroid({
- required String sku,
- required String packageName,
-})
-```
+```dart
+Future<void> deepLinkToSubscriptionsAndroid({ String? sku })
+```🤖 Prompt for AI Agents
In docs/docs/api/classes/flutter-inapp-purchase.md around lines 249 to 256, the
documented signature for deepLinkToSubscriptionsAndroid is incorrect: it shows
required sku and packageName but the actual library method accepts an optional
sku only. Update the code block and description to match the real API by
replacing the function signature with Future<void>
deepLinkToSubscriptionsAndroid({ String? sku }) and remove any mention of
packageName or required parameters so the docs reflect the optional sku
parameter.
| // Product management | ||
| Future<List<IAPItem>> requestProducts({ | ||
| Future<List<IapItem>> requestProducts({ | ||
| required List<String> skus, | ||
| String type = 'inapp', // 'inapp' or 'subs' | ||
| PurchaseType type = PurchaseType.inapp, | ||
| }); | ||
|
|
There was a problem hiding this comment.
Align requestProducts() signature with implementation (use params object and ProductCommon).
The docs still show the legacy overload returning List with raw skus argument. The implementation exposes a params-based API returning List.
Apply:
- // Product management
- Future<List<IapItem>> requestProducts({
- required List<String> skus,
- PurchaseType type = PurchaseType.inapp,
- });
+ // Product management
+ Future<List<ProductCommon>> requestProducts(
+ RequestProductsParams params,
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Product management | |
| Future<List<IAPItem>> requestProducts({ | |
| Future<List<IapItem>> requestProducts({ | |
| required List<String> skus, | |
| String type = 'inapp', // 'inapp' or 'subs' | |
| PurchaseType type = PurchaseType.inapp, | |
| }); | |
| // Product management | |
| Future<List<ProductCommon>> requestProducts( | |
| RequestProductsParams params, | |
| ); |
🤖 Prompt for AI Agents
In docs/docs/api/overview.md around lines 23 to 28, update the documented
requestProducts() signature to match the implementation: replace the legacy
overload that takes raw skus and returns Future<List<IapItem>> with the
params-based API that returns Future<List<ProductCommon>>; specifically document
the single params object argument (named to match the implementation) and the
new return type List<ProductCommon>, plus a brief description of the params
fields consistent with the implementation.
| Future<List<PurchasedItem>?> getAvailablePurchases() async | ||
| ``` | ||
|
|
||
| Retrieves all available purchases (including pending and non-consumed). | ||
|
|
||
| **Returns**: List of purchases or null |
There was a problem hiding this comment.
Return type mismatch: getAvailablePurchases now returns List.
The implementation returns List (OpenIAP-compliant), not List? as documented here.
-```dart
-Future<List<PurchasedItem>?> getAvailablePurchases() async
-```
+```dart
+Future<List<Purchase>> getAvailablePurchases() async
+```🤖 Prompt for AI Agents
In docs/docs/api/overview.md around lines 185 to 190, the documented return type
for getAvailablePurchases is incorrect (it shows Future<List<PurchasedItem>?>)
while the implementation returns Future<List<Purchase>>; update the code block
and surrounding text to show the correct OpenIAP-compliant signature
Future<List<Purchase>> getAvailablePurchases() async and adjust the descriptive
sentence to say it returns a List of Purchase (non-nullable) instead of
List<PurchasedItem>? so the docs match the implementation.
| ```dart | ||
| Future<String?> finishTransaction(PurchasedItem purchase) async | ||
| Future<String?> finishTransaction(Purchase purchase, {bool isConsumable = false}) async | ||
| ``` | ||
| **iOS only**: Completes a transaction. Must be called for all purchases. | ||
|
|
||
| Completes a transaction and removes it from the queue. | ||
|
|
There was a problem hiding this comment.
finishTransaction return type mismatch.
Implementation returns Future, not Future<String?>.
-```dart
-Future<String?> finishTransaction(Purchase purchase, {bool isConsumable = false}) async
-```
+```dart
+Future<void> finishTransaction(Purchase purchase, {bool isConsumable = false}) async
+```🤖 Prompt for AI Agents
In docs/docs/api/overview.md around lines 196 to 201, the documented signature
for finishTransaction is incorrect (shows Future<String?>) while the
implementation returns Future<void>; update the code fence signature to match
the implementation by changing it to Future<void> finishTransaction(Purchase
purchase, {bool isConsumable = false}) async and ensure any surrounding prose
that mentions a returned value is adjusted to reflect that the method returns no
value (void).
| Future<void> _handlePurchaseSuccess(Purchase purchase) async { | ||
| print('✅ Purchase successful: ${purchase.productId}'); | ||
|
|
||
| setState(() { | ||
| _latestPurchase = purchase; | ||
| _errorMessage = null; | ||
| }); | ||
|
|
||
| // Show success message | ||
| _showSuccessSnackBar('Purchase successful: ${purchase.productId}'); | ||
|
|
||
| try { | ||
| // 1. Here you would typically verify the purchase with your server | ||
| final isValid = await _verifyPurchase(purchase); | ||
|
|
||
| if (isValid) { | ||
| // 2. Deliver the product to the user | ||
| await _deliverProduct(purchase.productId); | ||
|
|
||
| // 3. Finish the transaction | ||
| await _finishTransaction(purchase); | ||
|
|
||
| print('✅ Purchase completed and delivered'); | ||
| } else { | ||
| _showError('Purchase verification failed'); |
There was a problem hiding this comment.
Type mismatch: _handlePurchaseSuccess uses Purchase but downstream methods expect PurchasedItem.
_purchaseUpdatedListener delivers Purchase, and you pass Purchase into _verifyPurchase and _finishTransaction, but both methods currently accept PurchasedItem. This will cause type errors and outdated field usage.
Apply the following changes in the method declarations to accept Purchase and align field access downstream (see detailed diffs on those methods below).
🤖 Prompt for AI Agents
In docs/docs/examples/basic-store.md around lines 190 to 214,
_handlePurchaseSuccess is using Purchase but downstream methods
(_verifyPurchase, _deliverProduct, _finishTransaction and any other helpers)
currently expect PurchasedItem; change those downstream method signatures to
accept Purchase instead and update their internal field access to use Purchase's
properties (e.g., productId, transactionId, purchaseToken or platform-specific
fields) rather than PurchasedItem-only fields. Ensure any type annotations,
imports, and tests are updated accordingly so all calls from
_handlePurchaseSuccess pass the Purchase object directly and compile without
type mismatches.
| _platform.isAndroid ? item.purchaseStateAndroid : null, | ||
| originalJson: _platform.isAndroid ? item.originalJsonAndroid : null, | ||
| dataAndroid: _platform.isAndroid ? item.originalJsonAndroid : null, | ||
| signatureAndroid: _platform.isAndroid ? item.signatureAndroid : null, |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Validate dataAndroid mapping (may be miswired).
You assign both originalJson and dataAndroid from item.originalJsonAndroid. The PR objective mentions fixing dataAndroid mapping; ensure dataAndroid points to the intended raw “data” (if available) rather than duplicating originalJson.
Run to inspect PurchasedItem fields:
If PurchasedItem has dataAndroid, prefer:
- originalJson: _platform.isAndroid ? item.originalJsonAndroid : null,
- dataAndroid: _platform.isAndroid ? item.originalJsonAndroid : null,
+ originalJson: _platform.isAndroid ? item.originalJsonAndroid : null,
+ dataAndroid: _platform.isAndroid ? item.dataAndroid : null,🏁 Script executed:
#!/bin/bash
set -e
rg -n -C3 --type dart 'class\\s+PurchasedItem\\b' lib
rg -n -C2 --type dart 'originalJsonAndroid|dataAndroid' libLength of output: 64
Let's refine our lookup to locate the PurchasedItem definition and its Android‐specific fields, then inspect the mapping in context:
🏁 Script executed:
#!/bin/bash
set -e
# 1. Locate where PurchasedItem is defined
rg -n --type dart 'class PurchasedItem' -C5 lib
# 2. Find Android‐specific properties on PurchasedItem
rg -n --type dart 'dataAndroid' -C3 lib
rg -n --type dart 'originalJsonAndroid' -C3 lib
# 3. Inspect the mapping block in flutter_inapp_purchase.dart
rg -n -C3 'dataAndroid:' lib/flutter_inapp_purchase.dart
rg -n -C3 'originalJsonAndroid' lib/flutter_inapp_purchase.dartLength of output: 14097
Fix miswired dataAndroid mapping
The dataAndroid field is currently set from item.originalJsonAndroid, duplicating the original JSON rather than using the raw “data” payload. It should map to item.dataAndroid.
• File: lib/flutter_inapp_purchase.dart
- Line 1048
Suggested diff:
- dataAndroid: _platform.isAndroid ? item.originalJsonAndroid : null,
+ dataAndroid: _platform.isAndroid ? item.dataAndroid : null,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| _platform.isAndroid ? item.purchaseStateAndroid : null, | |
| originalJson: _platform.isAndroid ? item.originalJsonAndroid : null, | |
| dataAndroid: _platform.isAndroid ? item.originalJsonAndroid : null, | |
| signatureAndroid: _platform.isAndroid ? item.signatureAndroid : null, | |
| _platform.isAndroid ? item.purchaseStateAndroid : null, | |
| originalJson: _platform.isAndroid ? item.originalJsonAndroid : null, | |
| dataAndroid: _platform.isAndroid ? item.dataAndroid : null, | |
| signatureAndroid: _platform.isAndroid ? item.signatureAndroid : null, |
🤖 Prompt for AI Agents
In lib/flutter_inapp_purchase.dart around lines 1046 to 1049, the dataAndroid
field is incorrectly assigned item.originalJsonAndroid (duplicating
originalJson); change the mapping so dataAndroid uses item.dataAndroid when
_platform.isAndroid, otherwise null, keeping the same ternary/check pattern as
the other Android-specific fields.
Summary
Changes
nameAndroid,oneTimePurchaseOfferDetailsAndroid)dataAndroidfield mappingReady for release.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests
Chores