Skip to content

migration: consolidate OpenIAP packages (gql, google, apple)#555

Merged
hyochan merged 16 commits intomainfrom
fix/lint-curly-braces
Sep 18, 2025
Merged

migration: consolidate OpenIAP packages (gql, google, apple)#555
hyochan merged 16 commits intomainfrom
fix/lint-curly-braces

Conversation

@hyochan
Copy link
Copy Markdown
Owner

@hyochan hyochan commented Sep 17, 2025

Summary

Summary by CodeRabbit

  • New Features

    • Android purchases now support subscription offers.
    • Added helpers for restoring purchases and checking active subscriptions.
  • Bug Fixes

    • Android: fixed honoring subscription offerToken.
  • Refactor

    • Standardized enum and type names (e.g., platform and product types) and introduced platform-specific models and structured results.
  • Documentation

    • Updated contributor guides and workflows; added guidance for generated files; refreshed changelog.
  • Tests

    • Added widget and unit tests for purchase, subscription, and product flows.
  • Chores

    • Upgraded OpenIAP dependencies (Google 1.1.11, Apple 1.1.12); version bumped to 6.7.0; improved CI, formatting, and linting.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Sep 17, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Broad refactor aligning APIs and enums to new OpenIAP models; major Dart type and builder changes; platform plugins updated for new error codes and parameters; CI/lints adjusted; dependencies bumped (Android/iOS); example app and tests migrated; generated types introduced via script; numerous legacy tests removed and new targeted tests added.

Changes

Cohort / File(s) Summary
CI and PR analysis
.github/workflows/ci.yml, .github/workflows/ci-gemini-pr.yml
Format step excludes generated files; Gemini PR analysis enabled with code-assist, model set, env/permissions adjusted.
Linting and analysis config
analysis_options.yaml, tool/lints/*
Uses local flutter_lints copy; excludes lib/types.dart and example/**; adds core/recommended/flutter lint rule sets.
Docs and guidelines
CLAUDE.md, AGENTS.md, CONTRIBUTING.md, CHANGELOG.md, GEMINI.md
Clarifies generated files and commit conventions; updates OpenIAP versions in docs; adds 6.7.0 changelog; removes GEMINI.md; AGENTS.md includes CLAUDE.md pointer.
Generated types pipeline
scripts/generate-type.sh, .vscode/settings.json
Adds script to fetch/overwrite lib/types.dart from release; excludes lib/types.dart from on-save formatting; adds cSpell word.
Core enums/errors/events
lib/enums.dart, lib/errors.dart, lib/events.dart
Enum casing and value model updates; adds mapping helpers; introduces new error codes; events now typedef to generated types.
Public API surface (Dart)
lib/flutter_inapp_purchase.dart, lib/builders.dart
Large API migration: new/renamed builders, props-based requests, legacy-compat extensions/wrappers, result wrappers, stream type changes, request/consume signatures updated.
Types cleanup/restructure
lib/types/iap_android_types.dart, lib/types/iap_ios_types.dart, lib/types_additions.dart
Removes legacy platform-specific type files and additions; replaced by generated lib/types.dart (fetched by script).
Android plugin
android/src/main/kotlin/.../AndroidInappPurchasePlugin.kt, android/build.gradle
Switches to RequestPurchaseParams, supports subscriptionOffers, unified error payloads/codes, dependency bump to openiap-google:1.1.11.
iOS plugin
ios/Classes/FlutterInappPurchasePlugin.swift, ios/flutter_inapp_purchase.podspec, example/ios/Podfile, example/ios/Flutter/Flutter.podspec
Main-thread handling via dispatcher; new error codes; adjusts method payloads; OpenIAP pod pinned to 1.1.12; iOS deployment target 12.0 in example podspec.
Example app updates
example/lib/src/screens/*, example/lib/src/widgets/product_detail_modal.dart, example/lib/test_storekit.dart
Enum casing updates, builder renames, props-based calls, platform detection guard, UI shows JSON for payment mode; updates product type usage.
Example tests (added)
example/test/*
Adds widget tests for available purchases, purchase flow, and subscription flow with mocked channels.
Repo tests (added/modified)
test/fetch_products_all_test.dart, test/flutter_inapp_purchase_active_subscriptions_test.dart, test/openiap_type_alignment_test.dart, test/refactored_types_test.dart
New tests for product fetching, active subscriptions, generated types, and serialization.
Repo tests (removed)
test/available_purchases_test.dart, test/enums_test.dart, test/errors_test.dart, test/events_test.dart, test/extensions_test.dart, test/flutter_inapp_purchase_test.dart, test/proration_mode_validation_test.dart, test/purchase_flow_test.dart, test/subscription_flow_test.dart, test/utils_android_response_test.dart, test/utils_test.dart
Removes legacy tests aligned to old API/types and flows.
Misc repo config
pubspec.yaml, .gitignore, example/android/app/build.gradle
Bumps package version to 6.7.0; ignore updates; pins Android NDK version in example app.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as App UI
  participant D as Dart API (flutter_inapp_purchase)
  participant C as Platform Channel
  participant A as Android Plugin
  participant O as OpenIAP (Android)

  rect rgba(230,245,255,0.4)
  note over U,D: Request In-App Purchase (Android)
  U->>D: requestPurchase(props: RequestPurchaseProps.inApp)
  D->>C: invokeMethod('requestPurchase', props)
  C->>A: requestPurchase(params)
  A->>O: requestPurchase(RequestPurchaseParams{subscriptionOffers?})
  O-->>A: Result / Error
  A-->>C: success map or legacyErrorJson
  C-->>D: map
  D-->>U: PurchaseResult / error stream
  end

  alt consume (Android only)
    U->>D: consumePurchaseAndroid(token)
    D->>C: invokeMethod('consumePurchaseAndroid', token)
    C->>A: consumePurchaseAndroid
    A->>O: consumePurchase
    O-->>A: VoidResult JSON
    A-->>D: VoidResult
  else not Android
    D-->>U: throws PlatformException
  end
Loading
sequenceDiagram
  autonumber
  actor U as App UI
  participant D as Dart API
  participant C as Platform Channel
  participant I as iOS Plugin
  participant O as OpenIAP (Apple)

  rect rgba(240,255,240,0.4)
  note over U,D: Fetch Products (iOS)
  U->>D: fetchProducts(skus, type: ProductType.InApp/Subs)
  D->>C: invokeMethod('fetchProducts', {skus, type})
  C->>I: fetchProducts
  I->>O: query products (type mapping)
  O-->>I: products
  I-->>C: product list (JSON)
  C-->>D: product list
  D-->>U: ProductIOS/ProductSubscriptionIOS
  end

  rect rgba(255,240,240,0.35)
  note over I: Error mapping (new codes)
  I-->>D: FlutterError{code: QueryProduct/PurchaseError/...}
  D-->>U: PurchaseResult/ConnectionResult via streams
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

Possibly related PRs

Suggested labels

፦ refactor, 🤖 android, :apple: ios, :package: update packages

Poem

Hop, hop! I tweak my props,
Builders bloom and enums pop.
Channels hum, results return,
Offers map and receipts churn.
Lints align, the tests reset—
6.7.0, my best hop yet! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "migration: consolidate OpenIAP packages (gql, google, apple)" is concise and directly reflects the PR's primary intent—migrating and consolidating OpenIAP-related packages—which is supported by the PR objectives and the changes (integrating openiap-gql, openiap-google, and openiap-apple). It is specific, single-sentence, and free of noise or vague phrasing, so a reviewer scanning history can understand the main change at a glance.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/lint-curly-braces

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @hyochan, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request represents a substantial internal refactoring and modernization of the in-app purchase plugin. While the title suggests a minor lint fix, the changes encompass a broad overhaul of type definitions, error handling, and data parsing mechanisms. The update aims to enhance the plugin's robustness, maintainability, and type safety by introducing platform-specific product and purchase types, streamlining error reporting, and simplifying certain API aspects. Notably, the "getPurchaseHistories()" method has been removed, and Android receipt validation is no longer supported.

Highlights

  • Core Refactoring & Type Safety: Significant internal restructuring to improve type safety and consistency, especially for product and purchase objects, by introducing platform-specific types (e.g., "ProductSubscriptionIOS", "PurchaseAndroid").
  • Streamlined Error Handling: Consolidated error handling by removing platform-specific arguments from "PurchaseError" and standardizing error codes, enhancing clarity and reducing redundancy.
  • Simplified API & Deprecations: Removed the "getPurchaseHistories()" method and the "finalize()" method, simplifying the public API. Android receipt validation is now explicitly unsupported.
  • Updated Purchase Flow: Refactored "requestPurchase" to use a new "RequestPurchaseProps" object, improving the flexibility and robustness of purchase requests.
  • New Internal Utility Classes: Introduced local "PurchaseResult" and "ConnectionResult" classes, along with numerous private helper methods for more robust and type-safe data parsing.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a very large refactoring of the core logic, significantly improving type safety and code structure, likely as part of a migration to freezed data classes. While the title and description suggest a minor lint fix, the changes are substantial and touch many parts of the library's public API, including method signatures and data models. Key changes include the removal of client-side Android receipt validation, which is a good security improvement, and a move towards more robust, platform-specific data types for products and purchases. I've identified a couple of areas for improvement: one concerning a potentially problematic fallback for purchase IDs, and another suggesting a safer way to handle type casting.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

Codex Review: Here are some suggestions.

Reply with @codex fix comments to fix any unresolved comments.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Comment on lines +549 to +556
return iap_types.ProductSubscriptionAndroid(
currency: currency,
description: description,
displayPrice: displayPrice,
id: productId,
nameAndroid: json['nameAndroid']?.toString() ?? productId,
platform: platform,
subscriptionOfferDetailsAndroid: subscriptionOffers,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P0] Replace OfferDetail parsing with non-existent ProductSubscription types

The code now constructs iap_types.ProductSubscriptionAndroid and returns lists of iap_types.ProductSubscriptionAndroidOfferDetails, but none of these classes (nor PricingPhasesAndroid/PricingPhaseAndroid) exist in types.dart or elsewhere in the repository—the previous implementation used the existing OfferDetail and PricingPhase types. As a result the file does not compile because these identifiers cannot be resolved.

Useful? React with 👍 / 👎.

Comment on lines +733 to +737
return iap_types.PurchaseState.Pending;
case 'purchased':
case 'restored':
return iap_types.PurchaseState.Purchased;
case 'failed':
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P0] Purchase state mapping uses undefined enum values

Both _parsePurchaseStateIOS and _mapAndroidPurchaseState now return values such as iap_types.PurchaseState.Purchased and Pending, but the exported PurchaseState enum defines only lowercase members (pending, purchased, unspecified). These identifiers are invalid and will trigger analyzer errors before the code can build.

Useful? React with 👍 / 👎.

@hyochan hyochan changed the title Fix lint curly braces migration: openiap-gql integration Sep 17, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
example/lib/src/screens/error_handling_example.dart (1)

227-231: Fix: calling user-friendly message with ErrorCode always returns the generic fallback

getUserFriendlyErrorMessage expects a PurchaseError; passing ErrorCode yields "An unexpected error occurred". Use the ErrorCode extension instead.

-                    .map((code) =>
-                        '${code.toString().split('.').last}:\n  "${getUserFriendlyErrorMessage(code)}"')
+                    .map((code) =>
+                        '${code.toString().split('.').last}:\n  "${code.userFriendlyMessage}"')
example/lib/src/screens/purchase_flow_screen.dart (1)

219-228: NPE/RangeError risk in receipt/token substring formatting

  • transactionReceipt?.substring(...) still evaluates transactionReceipt!.length and can NPE.
  • purchaseToken?.substring(0, 30) can RangeError if length < 30.

Apply:

-      _purchaseResult = '''
+      _purchaseResult = '''
 ✅ Purchase successful (${Platform.operatingSystem})
 Product: ${purchase.productId}
 ID: ${purchase.id.isNotEmpty ? purchase.id : "N/A"}
 Transaction ID: ${purchase.transactionId ?? "N/A"}
 Date: ${purchase.transactionDate ?? "N/A"}
-Receipt: ${purchase.transactionReceipt?.substring(0, purchase.transactionReceipt!.length > 50 ? 50 : purchase.transactionReceipt!.length)}...
-Purchase Token: ${purchase.purchaseToken?.substring(0, 30)}...
+Receipt: $receiptSnippet
+Purchase Token: $tokenSnippet
       '''
           .trim();

Add before the setState (same method scope):

final receipt = purchase.transactionReceipt;
final receiptSnippet = receipt == null
    ? 'N/A'
    : (receipt.length > 50 ? '${receipt.substring(0, 50)}…' : receipt);

final token = purchase.purchaseToken;
final tokenSnippet = token == null
    ? 'N/A'
    : (token.length > 30 ? '${token.substring(0, 30)}…' : token);
lib/enums.dart (1)

8-9: Rename enum members to IOS / Android (fix casing mismatch)

lib/enums.dart declares enum IapPlatform { ios, android } but the codebase and tests use IapPlatform.IOS / IapPlatform.Android; rename the enum members to IOS and Android (or update all usages / gate exports so only one authoritative IapPlatform is exported) to avoid type collisions and breakage.
Location: lib/enums.dart:8-9; usages: lib/errors.dart, lib/types.dart, test/*.

example/lib/src/screens/subscription_flow_screen.dart (1)

312-313: Prevent RangeError on token substring (use safe prefix/masking).

substring with a fixed end can throw if token length < N. Add a safe helper and use it where tokens are printed.

-        debugPrint(
-            '  - ${p.productId}: token=${p.purchaseToken?.substring(0, 20)}...');
+        final tp = _safePrefix(p.purchaseToken, 20, mask: true);
+        debugPrint('  - ${p.productId}: token=$tp...');
@@
-          _purchaseResult =
-              'Active: ${_currentSubscription!.productId}\nToken: ${_currentSubscription!.purchaseToken?.substring(0, 30)}...';
+          final tp = _safePrefix(_currentSubscription!.purchaseToken, 30, mask: true);
+          _purchaseResult = 'Active: ${_currentSubscription!.productId}\nToken: $tp';
@@
-      debugPrint('Using test token: ${testToken.substring(0, 20)}...');
+      debugPrint('Using test token: ${_safePrefix(testToken, 20, mask: true)}');
@@
-                label: Text(
-                  'Token: ${_currentSubscription!.purchaseToken?.substring(0, 10)}...',
+                label: Text(
+                  'Token: ${_safePrefix(_currentSubscription!.purchaseToken, 10, mask: true)}',

Add inside _SubscriptionFlowScreenState:

String _safePrefix(String? s, int max, {bool mask = false}) {
  final v = s ?? '';
  if (v.isEmpty) return '';
  final head = v.length <= max ? v : v.substring(0, max);
  if (!mask) return '$head...';
  final masked = head.replaceAll(RegExp(r'.(?=.{4})'), '*');
  return '$masked...';
}

Also applies to: 331-333, 498-499, 805-808

♻️ Duplicate comments (2)
lib/flutter_inapp_purchase.dart (2)

1186-1193: Don’t synthesize purchase IDs with timestamps (Android path).

Using DateTime.now().millisecondsSinceEpoch risks collisions and corrupts purchase identity. Drop the record or throw.

-        'id': (transactionId?.isNotEmpty ?? false)
-            ? transactionId
-            : (productId.isNotEmpty
-                ? productId
-                : DateTime.now().millisecondsSinceEpoch.toString()),
+        'id': (transactionId?.isNotEmpty ?? false)
+            ? transactionId
+            : (productId.isNotEmpty ? productId : (throw iap_types.PurchaseError(
+                code: iap_types.ErrorCode.ServiceError,
+                message: 'Invalid purchase payload: missing id and productId',
+              ))),

1240-1245: Same issue on iOS path: avoid timestamp fallback for purchase ID.

-  'id': (transactionId?.isNotEmpty ?? false)
-      ? transactionId
-      : (productId.isNotEmpty
-          ? productId
-          : DateTime.now().millisecondsSinceEpoch.toString()),
+  'id': (transactionId?.isNotEmpty ?? false)
+      ? transactionId
+      : (productId.isNotEmpty ? productId : (throw iap_types.PurchaseError(
+          code: iap_types.ErrorCode.ServiceError,
+          message: 'Invalid purchase payload: missing id and productId',
+        ))),
🧹 Nitpick comments (29)
example/lib/src/screens/error_handling_example.dart (1)

174-175: Prefer runtime platform detection over hardcoded IapPlatform values

Use iap_err.getCurrentPlatform() to avoid drift and ensure accurate telemetry (per project guidance).

-                  platform: IapPlatform.IOS,
+                  platform: iap_err.getCurrentPlatform(),

Add once at top of file:

import 'package:flutter_inapp_purchase/errors.dart' as iap_err;

Also applies to: 193-194, 252-253, 327-328

lib/events.dart (1)

5-7: Alias to generated enum looks good; consider re-exporting key types for DX

The typedef preserves public name. To reduce churn for consumers, re-export Purchase, PurchaseError (optional).

Example (outside this hunk):

export 'types.dart' show Purchase, PurchaseError;
.github/workflows/ci-gemini-pr.yml (1)

95-106: Model mismatch between step input and settings payload

with.gemini_model is 2.5-pro, while settings.model is 1.5-flash. Pick one to avoid ambiguity.

-              "model": "gemini-1.5-flash"
+              "model": "gemini-2.5-pro"
lib/modules/android.dart (1)

50-54: Guard enum index to avoid RangeError on bad payloads

Indexing values[...] without bounds checks can throw if backend sends unexpected numbers.

   factory InAppMessage.fromMap(Map<String, dynamic> map) {
     return InAppMessage(
-      messageId: map['messageId']?.toString() ?? '',
-      campaignName: map['campaignName']?.toString() ?? '',
-      messageType: legacy
-          .InAppMessageType.values[(map['messageType'] as num?)?.toInt() ?? 0],
+      messageId: map['messageId']?.toString() ?? '',
+      campaignName: map['campaignName']?.toString() ?? '',
+      messageType: () {
+        final idx = (map['messageType'] as num?)?.toInt() ?? 0;
+        final vals = legacy.InAppMessageType.values;
+        return (idx >= 0 && idx < vals.length) ? vals[idx] : vals.first;
+      }(),
     );
   }
lib/builders.dart (4)

25-45: Duplication between iOS purchase/subscription builders

Fields and build mapping are identical. Consider a shared private base to reduce drift.

I can draft a minimal mixin/abstract base if helpful.


94-121: Type normalizer is flexible; add minor guardrails

String heuristic uses contains('sub'), which is fine. Consider rejecting empty strings to avoid accidental defaults.

   set type(Object value) {
+    if (value is String && value.trim().isEmpty) {
+      throw ArgumentError('type cannot be an empty string');
+    }

125-165: Validate presence of at least one platform payload

iosProps and androidProps can both be null; building such a payload is likely invalid upstream.

   RequestPurchaseProps build() {
     final iosProps = ios.sku.isNotEmpty ? ios.build() : null;
     final androidProps = android.skus.isNotEmpty ? android.build() : null;
+    if (iosProps == null && androidProps == null) {
+      throw StateError('Provide at least one platform payload (iOS or Android).');
+    }

189-218: Dedicated subscription builder: solid; mirrors purchase builder

No functional concerns. Consider the same "at least one platform payload" validation here too.

   RequestPurchaseProps build() {
     final iosProps = ios.sku.isNotEmpty ? ios.build() : null;
     final androidProps = android.skus.isNotEmpty ? android.build() : null;
+    if (iosProps == null && androidProps == null) {
+      throw StateError('Provide at least one platform payload (iOS or Android).');
+    }
example/lib/src/screens/available_purchases_screen.dart (1)

67-73: Don't silently default to Android on platform detection failure

Catching all and returning Android can mask iOS and hide history. At minimum, log the exception and consider a runtime fallback (e.g., Platform.isIOS) instead of hard‑coding Android.

Apply:

 IapPlatform _platformOrDefault() {
   try {
     return getCurrentPlatform();
-  } catch (_) {
-    return IapPlatform.Android;
+  } catch (e) {
+    debugPrint('getCurrentPlatform() failed, defaulting by OS: $e');
+    // Prefer a runtime OS check to avoid misclassifying iOS as Android.
+    return IapPlatform.Android; // Or: return Platform.isIOS ? IapPlatform.IOS : IapPlatform.Android;
   }
 }
example/test/purchase_flow_screen_test.dart (1)

67-73: Assert the channel purchase call and avoid flake

After tapping Buy, wait for settles and assert requestPurchase was invoked.

Apply:

-    await tester.tap(find.text('Buy'));
-    await tester.pump();
-
-    expect(find.text('Processing...'), findsOneWidget);
+    await tester.tap(find.text('Buy'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Processing...'), findsOneWidget);
+    expect(log.any((call) => call.method == 'requestPurchase'), isTrue);
example/lib/src/screens/purchase_flow_screen.dart (1)

575-579: RangeError risk when previewing purchaseToken in debug block

Substring(0, 20) will throw when token length < 20. Precompute a safe summary.

Apply:

-${purchases.map((p) => '- ${p.productId}: ${p.purchaseToken?.substring(0, 20)}...').join('\n')}
+${purchasesSummary}

Add before building _purchaseResult in that handler:

final purchasesSummary = purchases.map((p) {
  final t = p.purchaseToken;
  final safe = t == null ? 'N/A' : (t.length > 20 ? '${t.substring(0, 20)}…' : t);
  return '- ${p.productId}: $safe';
}).join('\n');
test/refactored_types_test.dart (4)

1-2: Scope the lint ignore to the smallest region possible

The file-wide ignore is fine for tests, but you already use many const constructors. Consider scoping or removing if no longer needed.


7-25: Strengthen round‑trip assertions for ActiveSubscription

Also assert transactionId and expirationDateIOS to catch regressions in ID/timestamp handling.

Apply this diff:

       final restored = ActiveSubscription.fromJson(json);

       expect(restored.environmentIOS, 'Sandbox');
       expect(restored.productId, 'premium_sub');
       expect(restored.willExpireSoon, isTrue);
+      expect(restored.transactionId, 'txn_123');
+      expect(restored.expirationDateIOS, 1700001200);

29-47: Add a couple more asserts for PurchaseAndroid

Validating id and quantity improves coverage without adding noise.

       final restored = PurchaseAndroid.fromJson(json);

       expect(restored.purchaseToken, 'android_token');
       expect(restored.platform, IapPlatform.Android);
       expect(restored.purchaseState, PurchaseState.Purchased);
+      expect(restored.id, 'txn_android');
+      expect(restored.quantity, 1);

49-68: Consider asserting critical iOS fields beyond StoreKit metadata

Add id and transactionDate checks to make the serialization contract explicit.

       final restored = PurchaseIOS.fromJson(json);

       expect(restored.environmentIOS, 'Production');
       expect(restored.subscriptionGroupIdIOS, 'group_a');
       expect(restored.platform, IapPlatform.IOS);
+      expect(restored.id, 'txn_ios');
+      expect(restored.transactionDate, 1700000300);
lib/errors.dart (3)

5-5: Prefer package: import to avoid relative import cycles

Using a package import makes internal/external imports consistent and reduces cyclic-import pitfalls.

-import 'types.dart';
+import 'package:flutter_inapp_purchase/types.dart';

102-135: Provide user‑friendly messages for newly added error codes

Add messages for BillingUnavailable and FeatureNotSupported to avoid falling back to the generic string.

   switch (code) {
     case ErrorCode.UserCancelled:
       return 'Purchase was cancelled by user';
     case ErrorCode.NetworkError:
       return 'Network connection error. Please check your internet connection and try again.';
     case ErrorCode.ItemUnavailable:
     case ErrorCode.SkuNotFound:
       return 'This item is not available for purchase';
+    case ErrorCode.BillingUnavailable:
+      return 'Billing is not available on this device/account';
     case ErrorCode.AlreadyOwned:
       return 'You already own this item';
+    case ErrorCode.FeatureNotSupported:
+      return 'This feature is not supported on your device';

182-184: Include code/platform in PurchaseError.toString for quicker diagnostics

Small DX boost when scanning logs.

-  String toString() => '$name: $message';
+  String toString() {
+    final codeStr = code?.toString().split('.').last ?? 'Unknown';
+    final platStr = platform?.toString().split('.').last ?? 'Unknown';
+    return '$name: [$platStr/$codeStr] $message';
+  }
test/types_additions_test.dart (3)

10-20: Assert more AppTransaction fields in round‑trip

Add signedDate and appVersionId (and optionally appId) to tighten coverage.

   final back = AppTransaction.fromJson(map);
   expect(back.bundleId, 'com.example');
   expect(back.environment, 'Sandbox');
   expect(back.appVersion, '1.0.0');
+  expect(back.appVersionId, 456);
+  expect(back.signedDate, 1700000020);
+  expect(back.appId, 123);

Also applies to: 24-26


28-44: Subscription test: verify ID/timestamps as well

Helps ensure seconds vs milliseconds aren’t regressed.

   final json = sub.toJson();
   expect(json['productId'], 'sub');
   expect(json['environmentIOS'], 'Sandbox');
   expect(json['willExpireSoon'], true);
+  expect(json['transactionId'], 't1');
+  expect(json['expirationDateIOS'], 1700001000);
+  expect(json['transactionDate'], 1700000100);

46-59: PurchaseIOS: add simple ID/state assertion

Minor, but guards against future enum/name shifts.

   expect(p.expirationDateIOS, 1700005000);
   expect(p.platform, IapPlatform.IOS);
+  expect(p.id, 't');
+  expect(p.purchaseState, PurchaseState.Purchased);
lib/enums.dart (1)

125-134: Value→enum helper is fine; handle unexpected negatives?

Optional: clamp unknown/negative values to Unknown (current default).

example/lib/src/screens/subscription_flow_screen.dart (4)

395-399: Use the new builder/props API instead of deprecated request parameter.

requestPurchase(request: ...) is deprecated; migrate to requestPurchaseWithBuilder for compile‑time safety and to avoid deprecation churn.

- final request = RequestPurchase(
-   android: RequestSubscriptionAndroid(
-     skus: [item.id],
-     subscriptionOffers: selectedOffer != null ? [selectedOffer] : [],
-     purchaseTokenAndroid: _currentSubscription!.purchaseToken,
-     replacementModeAndroid: _selectedProrationMode,
-   ),
-   type: ProductType.Subs,
- );
- await _iap.requestPurchase(request: request);
+ await _iap.requestPurchaseWithBuilder(build: (r) => r
+   ..type = ProductType.Subs
+   ..withAndroid((a) {
+     a.skus = [item.id];
+     if (selectedOffer != null) a.subscriptionOffers = [selectedOffer];
+     a.purchaseTokenAndroid = _currentSubscription!.purchaseToken;
+     a.replacementModeAndroid = _selectedProrationMode;
+   })
+ );

Similarly replace the other two call sites (new Android purchase and iOS purchase) with the builder:

- final request = RequestPurchase(
-   android: RequestSubscriptionAndroid(
-     skus: [item.id],
-     subscriptionOffers: selectedOffer != null ? [selectedOffer] : [],
-   ),
-   type: ProductType.Subs,
- );
- await _iap.requestPurchase(request: request);
+ await _iap.requestPurchaseWithBuilder(build: (r) => r
+   ..type = ProductType.Subs
+   ..withAndroid((a) {
+     a.skus = [item.id];
+     if (selectedOffer != null) a.subscriptionOffers = [selectedOffer];
+   })
+ );
- final request = RequestPurchase(
-   ios: RequestPurchaseIOS(sku: item.id),
-   type: ProductType.Subs,
- );
- await _iap.requestPurchase(request: request);
+ await _iap.requestPurchaseWithBuilder(build: (r) => r
+   ..type = ProductType.Subs
+   ..withIOS((i) => i..sku = item.id)
+ );

Apply the same migration in the two testing helpers below.

Also applies to: 403-412, 415-423, 466-469, 511-514


172-175: Confirm Pending/Unknown mapping semantics.

Treating Android Unknown as “pending” in UI may be misleading. Consider a distinct “unknown” state, or gate on token presence.


309-314: Minimize exposure of purchase tokens in logs/UI.

Purchase tokens are sensitive; prefer masked previews (last 4 chars) and avoid storing in UI state.

Also applies to: 553-555, 805-811


665-695: Prefer enum constants for proration (AndroidReplacementMode) over magic ints.

Replace hardcoded 1..5 with AndroidReplacementMode.*.value for clarity and future‑proofing.

test/openiap_type_alignment_test.dart (1)

81-102: LGTM: Pricing phases round‑trip via JSON verified.

Consider adding an assert for recurrenceMode value to catch schema drift.

lib/flutter_inapp_purchase.dart (2)

1474-1519: Avoid unsafe cast in finishTransaction (Android).

Rely on type checks to prevent runtime cast errors.

-      final androidPurchase = purchase as iap_types.PurchaseAndroid;
+      if (purchase is! iap_types.PurchaseAndroid) {
+        throw iap_types.PurchaseError(
+          code: iap_types.ErrorCode.DeveloperError,
+          message: 'Expected PurchaseAndroid on Android finishTransaction',
+        );
+      }
+      final iap_types.PurchaseAndroid androidPurchase = purchase;

706-734: iOS available purchases filter may drop valid edge cases.

Filtering on non-empty purchaseToken could exclude some restored transactions; consider making this filter configurable or aligning with PurchaseOptions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e3673c and 518a1dc.

⛔ Files ignored due to path filters (1)
  • example/ios/Podfile.lock is excluded by !**/*.lock
📒 Files selected for processing (41)
  • .github/workflows/ci-gemini-pr.yml (3 hunks)
  • .vscode/settings.json (2 hunks)
  • AGENTS.md (1 hunks)
  • CLAUDE.md (2 hunks)
  • analysis_options.yaml (1 hunks)
  • example/ios/Flutter/Flutter.podspec (1 hunks)
  • example/lib/src/screens/available_purchases_screen.dart (2 hunks)
  • example/lib/src/screens/builder_demo_screen.dart (8 hunks)
  • example/lib/src/screens/error_handling_example.dart (4 hunks)
  • example/lib/src/screens/purchase_flow_screen.dart (4 hunks)
  • example/lib/src/screens/subscription_flow_screen.dart (7 hunks)
  • example/lib/src/widgets/product_detail_modal.dart (2 hunks)
  • example/lib/test_storekit.dart (1 hunks)
  • example/test/available_purchases_screen_test.dart (1 hunks)
  • example/test/purchase_flow_screen_test.dart (1 hunks)
  • example/test/subscription_flow_screen_test.dart (1 hunks)
  • lib/builders.dart (5 hunks)
  • lib/enums.dart (3 hunks)
  • lib/errors.dart (7 hunks)
  • lib/events.dart (1 hunks)
  • lib/flutter_inapp_purchase.dart (36 hunks)
  • lib/modules/android.dart (4 hunks)
  • lib/types/iap_android_types.dart (0 hunks)
  • lib/types/iap_ios_types.dart (0 hunks)
  • lib/types_additions.dart (0 hunks)
  • scripts/generate-type.sh (1 hunks)
  • test/available_purchases_test.dart (0 hunks)
  • test/enums_test.dart (0 hunks)
  • test/errors_test.dart (0 hunks)
  • test/events_test.dart (0 hunks)
  • test/extensions_test.dart (0 hunks)
  • test/flutter_inapp_purchase_test.dart (0 hunks)
  • test/openiap_type_alignment_test.dart (1 hunks)
  • test/proration_mode_validation_test.dart (0 hunks)
  • test/purchase_flow_test.dart (0 hunks)
  • test/refactored_types_test.dart (1 hunks)
  • test/subscription_flow_test.dart (0 hunks)
  • test/types_additions_test.dart (1 hunks)
  • test/types_platform_test.dart (1 hunks)
  • test/utils_android_response_test.dart (0 hunks)
  • test/utils_test.dart (0 hunks)
💤 Files with no reviewable changes (14)
  • test/errors_test.dart
  • test/utils_test.dart
  • test/subscription_flow_test.dart
  • test/utils_android_response_test.dart
  • test/enums_test.dart
  • test/proration_mode_validation_test.dart
  • test/events_test.dart
  • test/flutter_inapp_purchase_test.dart
  • test/available_purchases_test.dart
  • test/purchase_flow_test.dart
  • lib/types/iap_ios_types.dart
  • lib/types_additions.dart
  • lib/types/iap_android_types.dart
  • test/extensions_test.dart
🧰 Additional context used
📓 Path-based instructions (7)
**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

**/*.dart: Code must be formatted with dart format
Code should pass flutter analyze

**/*.dart: Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed
Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)
Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)
When using generic functions like showModalBottomSheet, specify explicit type arguments (e.g., showModalBottomSheet) to avoid type inference errors
iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)
For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)
Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)
Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)
Exception: standalone iOS fields that end with ID should use all-caps ID (e.g., transactionID, webOrderLineItemID in iOS-only contexts)
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Do not use generic prefixes like get or find for APIs; follow OpenIAP terminology

Files:

  • example/test/available_purchases_screen_test.dart
  • test/refactored_types_test.dart
  • example/test/purchase_flow_screen_test.dart
  • lib/modules/android.dart
  • example/lib/test_storekit.dart
  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/widgets/product_detail_modal.dart
  • example/test/subscription_flow_screen_test.dart
  • example/lib/src/screens/available_purchases_screen.dart
  • example/lib/src/screens/error_handling_example.dart
  • lib/events.dart
  • test/types_platform_test.dart
  • test/openiap_type_alignment_test.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/errors.dart
  • lib/enums.dart
  • test/types_additions_test.dart
  • example/lib/src/screens/subscription_flow_screen.dart
  • lib/builders.dart
{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}: Add appropriate error handling for edge cases
Add comments only for complex logic that isn't self-explanatory

Files:

  • example/test/available_purchases_screen_test.dart
  • test/refactored_types_test.dart
  • example/test/purchase_flow_screen_test.dart
  • lib/modules/android.dart
  • example/lib/test_storekit.dart
  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/widgets/product_detail_modal.dart
  • example/test/subscription_flow_screen_test.dart
  • example/lib/src/screens/available_purchases_screen.dart
  • example/lib/src/screens/error_handling_example.dart
  • lib/events.dart
  • test/types_platform_test.dart
  • test/openiap_type_alignment_test.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/errors.dart
  • lib/enums.dart
  • test/types_additions_test.dart
  • example/lib/src/screens/subscription_flow_screen.dart
  • lib/builders.dart
example/**

📄 CodeRabbit inference engine (GEMINI.md)

Update the example app when needed for new features

Files:

  • example/test/available_purchases_screen_test.dart
  • example/test/purchase_flow_screen_test.dart
  • example/lib/test_storekit.dart
  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/widgets/product_detail_modal.dart
  • example/test/subscription_flow_screen_test.dart
  • example/lib/src/screens/available_purchases_screen.dart
  • example/ios/Flutter/Flutter.podspec
  • example/lib/src/screens/error_handling_example.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • example/lib/src/screens/subscription_flow_screen.dart
test/**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

test/**/*.dart: Add tests for new functionality or bug fixes
Ensure new code is covered by tests
For new features, add comprehensive tests
For bug fixes, write a failing test that demonstrates the bug

Files:

  • test/refactored_types_test.dart
  • test/types_platform_test.dart
  • test/openiap_type_alignment_test.dart
  • test/types_additions_test.dart
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using emojis in documentation, especially in headings

Files:

  • AGENTS.md
  • CLAUDE.md
lib/**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

Follow the simplified API design where methods use direct parameters instead of parameter objects

Files:

  • lib/modules/android.dart
  • lib/events.dart
  • lib/flutter_inapp_purchase.dart
  • lib/errors.dart
  • lib/enums.dart
  • lib/builders.dart
{lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Files:

  • lib/modules/android.dart
  • lib/events.dart
  • lib/flutter_inapp_purchase.dart
  • lib/errors.dart
  • lib/enums.dart
  • lib/builders.dart
🧠 Learnings (32)
📓 Common learnings
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: All changes must pass `flutter test`

Applied to files:

  • example/test/available_purchases_screen_test.dart
  • test/refactored_types_test.dart
  • example/test/purchase_flow_screen_test.dart
  • example/test/subscription_flow_screen_test.dart
  • test/openiap_type_alignment_test.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to test/**/*.dart : Add tests for new functionality or bug fixes

Applied to files:

  • example/test/available_purchases_screen_test.dart
  • test/refactored_types_test.dart
  • example/test/purchase_flow_screen_test.dart
  • example/test/subscription_flow_screen_test.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to test/**/*.dart : For new features, add comprehensive tests

Applied to files:

  • example/test/available_purchases_screen_test.dart
  • test/refactored_types_test.dart
  • example/test/purchase_flow_screen_test.dart
  • example/test/subscription_flow_screen_test.dart
  • test/openiap_type_alignment_test.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)

Applied to files:

  • example/test/available_purchases_screen_test.dart
  • test/refactored_types_test.dart
  • lib/modules/android.dart
  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/available_purchases_screen.dart
  • lib/events.dart
  • test/types_platform_test.dart
  • test/openiap_type_alignment_test.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/errors.dart
  • lib/enums.dart
  • test/types_additions_test.dart
  • example/lib/src/screens/subscription_flow_screen.dart
  • lib/builders.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)

Applied to files:

  • test/refactored_types_test.dart
  • lib/modules/android.dart
  • example/lib/test_storekit.dart
  • example/lib/src/screens/purchase_flow_screen.dart
  • lib/events.dart
  • test/types_platform_test.dart
  • test/openiap_type_alignment_test.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/subscription_flow_screen.dart
  • lib/builders.dart
📚 Learning: 2025-09-09T06:04:46.743Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.

Applied to files:

  • test/refactored_types_test.dart
  • lib/modules/android.dart
  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/available_purchases_screen.dart
  • example/lib/src/screens/error_handling_example.dart
  • lib/events.dart
  • test/types_platform_test.dart
  • test/openiap_type_alignment_test.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/errors.dart
  • lib/enums.dart
  • test/types_additions_test.dart
  • example/lib/src/screens/subscription_flow_screen.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Read the existing CLAUDE.md file before making changes

Applied to files:

  • AGENTS.md
  • CLAUDE.md
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)

Applied to files:

  • lib/modules/android.dart
  • CLAUDE.md
  • example/lib/src/screens/error_handling_example.dart
  • test/types_platform_test.dart
  • test/openiap_type_alignment_test.dart
  • lib/flutter_inapp_purchase.dart
  • lib/errors.dart
  • lib/enums.dart
  • example/lib/src/screens/subscription_flow_screen.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)

Applied to files:

  • lib/modules/android.dart
  • example/lib/src/screens/purchase_flow_screen.dart
  • CLAUDE.md
  • example/lib/src/screens/error_handling_example.dart
  • lib/events.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/errors.dart
  • test/types_additions_test.dart
  • example/lib/src/screens/subscription_flow_screen.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification for parameter naming. According to the specification at https://www.openiap.dev/docs/types#request-types, the parameter name should be "skus" (not "productIds") when requesting products, as confirmed by the project maintainer hyochan.

Applied to files:

  • example/lib/test_storekit.dart
  • example/lib/src/screens/purchase_flow_screen.dart
  • lib/events.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/subscription_flow_screen.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed

Applied to files:

  • example/lib/test_storekit.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T02:25:46.402Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/index.md:65-69
Timestamp: 2025-08-21T02:25:46.402Z
Learning: When reviewing flutter_inapp_purchase documentation, `purchaseUpdatedListener` and `purchaseErrorListener` are the correct listener names in the public API, not `purchaseUpdated` and `purchaseError`.

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • lib/events.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/subscription_flow_screen.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • CLAUDE.md
  • example/lib/src/screens/error_handling_example.dart
  • test/types_platform_test.dart
  • lib/flutter_inapp_purchase.dart
  • lib/errors.dart
  • test/types_additions_test.dart
📚 Learning: 2025-08-21T02:01:17.634Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification at https://www.openiap.dev/docs/types#request-types for parameter naming conventions. According to hyochan, "skus" is the correct parameter name based on this specification, not "productIds".

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: According to the OpenIAP specification at https://www.openiap.dev/docs/types#request-types, the parameter name for requesting products should be "skus" rather than "productIds" to maintain compliance with the OpenIAP standard.

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to **/*.dart : Code must be formatted with `dart format`

Applied to files:

  • .vscode/settings.json
  • CLAUDE.md
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to **/*.dart : Code should pass `flutter analyze`

Applied to files:

  • .vscode/settings.json
  • analysis_options.yaml
  • CLAUDE.md
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Before PR, run: `dart format --set-exit-if-changed .`, `flutter analyze`, `flutter test`

Applied to files:

  • .vscode/settings.json
  • CLAUDE.md
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Use --set-exit-if-changed with dart format locally to match CI behavior

Applied to files:

  • .vscode/settings.json
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Do not use generic prefixes like get or find for APIs; follow OpenIAP terminology

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Exception: standalone iOS fields that end with ID should use all-caps ID (e.g., transactionID, webOrderLineItemID in iOS-only contexts)

Applied to files:

  • CLAUDE.md
  • example/lib/src/screens/error_handling_example.dart
  • lib/errors.dart
  • test/types_additions_test.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : When using generic functions like showModalBottomSheet, specify explicit type arguments (e.g., showModalBottomSheet<void>) to avoid type inference errors

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-08-21T02:09:07.921Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/classes/flutter-inapp-purchase.md:97-123
Timestamp: 2025-08-21T02:09:07.921Z
Learning: For flutter_inapp_purchase PRs, focus on the current version being developed (e.g., 6.4) and ignore versioned documentation files (e.g., version-6.3 folders) as those are historical snapshots that should not be modified.

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Before committing, run and ensure all pass in order: dart format --set-exit-if-changed ., flutter analyze, flutter test, then dart format --set-exit-if-changed . again; only commit if all succeed

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)

Applied to files:

  • example/lib/src/screens/error_handling_example.dart
  • test/types_platform_test.dart
  • lib/errors.dart
  • test/types_additions_test.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to {lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}} : Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Applied to files:

  • example/lib/src/screens/error_handling_example.dart
  • test/types_platform_test.dart
  • lib/flutter_inapp_purchase.dart
  • lib/errors.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)

Applied to files:

  • example/lib/src/screens/error_handling_example.dart
  • test/types_platform_test.dart
  • lib/errors.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to {**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}} : Add appropriate error handling for edge cases

Applied to files:

  • example/lib/src/screens/error_handling_example.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)

Applied to files:

  • lib/events.dart
  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/subscription_flow_screen.dart
  • lib/builders.dart
📚 Learning: 2025-08-20T23:53:15.912Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/methods/finish-transaction.md:66-77
Timestamp: 2025-08-20T23:53:15.912Z
Learning: In flutter_inapp_purchase, the correct way to listen to purchase updates is `FlutterInappPurchase.instance.purchaseUpdatedListener.listen`, not `FlutterInappPurchase.purchaseUpdated.listen`.

Applied to files:

  • lib/flutter_inapp_purchase.dart
🪛 GitHub Actions: Flutter CI
lib/flutter_inapp_purchase.dart

[info] 1-1: Dart format reformatted this file.

lib/builders.dart

[info] 1-1: Dart format reformatted this file.

🔇 Additional comments (46)
.vscode/settings.json (2)

10-12: Appropriate exclusion of generated file from formatting.

This change correctly prevents auto-formatting of the generated lib/types.dart file, which aligns with the new code generation workflow introduced in this PR.


68-68: Spelling dictionary update supports IAP terminology.

The addition of "skus" to the spell-check dictionary supports the OpenIAP terminology used throughout the project's in-app purchase APIs.

AGENTS.md (1)

1-1: LGTM.

This references the updated CLAUDE.md file that provides Flutter-specific guidelines for the project.

example/lib/test_storekit.dart (1)

61-61: Enum naming updated correctly.

The change from ProductType.inapp to ProductType.InApp aligns with the new capitalized enum naming convention introduced in this PR.

analysis_options.yaml (1)

34-36: Appropriate exclusions for generated file and examples.

The exclusion of lib/types.dart from analysis is correct since this is a generated file (per the new scripts/generate-type.sh workflow). Excluding example/** also makes sense to avoid analyzing extensive example code changes in CI.

example/lib/src/widgets/product_detail_modal.dart (2)

306-309: JSON representation for payment mode display.

Using discount.paymentMode.toJson() instead of the raw object provides a more structured JSON representation in the UI, which is consistent with the broader migration to JSON-friendly data models in this PR.


346-349: Consistent JSON display for subscription discount payment mode.

This mirrors the same change applied to Product discounts, providing consistent JSON representation for payment modes in the subscription discount display.

CLAUDE.md (2)

46-51: Clear guidance for generated files.

This section provides essential guidance for the new code generation workflow, emphasizing that lib/types.dart should never be manually edited and must be regenerated using the provided script. This prevents developers from accidentally breaking the generated types.


74-79: Helpful commit message convention guidance.

The Angular-style commit message convention provides clear structure for consistent commit history, which is valuable for maintaining project quality.

scripts/generate-type.sh (3)

17-25: Proper dependency validation.

The script correctly checks for required dependencies (curl and unzip) and provides clear error messages if they're missing.


10-13: Good cleanup pattern.

The script uses proper temporary directory cleanup with trap handling to ensure temporary files are removed even if the script fails.


33-36: Appropriate error handling for missing file.

The validation ensures that the required types.dart file exists in the downloaded archive before proceeding with the replacement.

example/ios/Flutter/Flutter.podspec (1)

14-14: iOS deployment target lowered to 12.0 — verify StoreKit usage

iOS 12 supports the legacy StoreKit APIs (SKProductsRequest, SKPayment, SKPaymentQueue, SKReceipt, SKDownload, restoreCompletedTransactions); StoreKit 2 requires iOS 15+. If the app uses StoreKit 2 (Product/Transaction async APIs or Swift concurrency StoreKit calls) revert the target to 15.0+ or implement a runtime fallback; otherwise the change is acceptable. Location: example/ios/Flutter/Flutter.podspec (s.ios.deployment_target = '12.0').

lib/builders.dart (5)

3-23: iOS purchase props builder: API shape looks good

Matches the props-based design and naming. No issues spotted.


56-63: Android purchase props builder: LGTM

Straightforward mapping; nullable personalization preserved.


78-89: Android subscription: nulling empty offers is good; keep token/mode optional

Looks correct and consistent with props API.


168-173: Typedefs improve ergonomics

Good additions for builder configuration callbacks.


178-187: Fluent builder extension: LGTM

Chaining API is clear and side-effect free.

lib/events.dart (1)

10-10: No downstream references found — namespacing OK. Search for PurchaseUpdatedEvent / PurchaseErrorEvent returned only lib/events.dart; no other files reference these events.

example/lib/src/screens/available_purchases_screen.dart (1)

132-136: LGTM: Platform‑gated iOS history path

Using PurchaseOptions with onlyIncludeActiveItemsIOS=false on iOS is correct; Android history remains empty by API design.

example/test/subscription_flow_screen_test.dart (1)

72-84: Test intent mismatch: stub returns one subscription but test expects empty state

Either test the empty state (return []) or test a rendered subscription tile. Pick one and adjust accordingly.

Two options:

Option A (empty state):

-          if (type == 'subs') {
-            return <Map<String, dynamic>>[
-              <String, dynamic>{ ... one iOS sub ... },
-            ];
-          }
-          return [];
+          return <Map<String, dynamic>>[];

Option B (tile rendered):

-    expect(find.text('No subscriptions available'), findsOneWidget);
+    expect(find.text('Premium Monthly'), findsOneWidget);

Also applies to: 27-52

example/lib/src/screens/purchase_flow_screen.dart (3)

126-130: LGTM: IapPlatform.IOS usage

Enum casing aligns with the migration.


335-336: LGTM: fetchProducts with ProductType.InApp

Matches the new, direct-params API (no RequestProductsParams).


389-391: LGTM: requestPurchase embeds type in RequestPurchase

Correct per the new unified API surface.

test/refactored_types_test.dart (1)

71-85: LGTM: Error round‑trip coverage is adequate

Round‑trip on code/message/productId is sufficient here.

lib/errors.dart (2)

10-13: Enum casing update (IOS/Android) is consistent with the spec

Good alignment with the project’s naming rules.


75-81: Ensure mapping parity is covered by tests

New Android codes were added; please add/extend tests to assert fromPlatformCode and toPlatformCode round‑trip for each entry.

Would you like me to add a focused test file that iterates over ErrorCodeMapping.android/ios and verifies both directions?

example/lib/src/screens/builder_demo_screen.dart (5)

42-42: Enum rename to ProductType.InApp looks correct

Matches the new API.


66-66: Enum rename to ProductType.Subs looks correct

Consistent with the migration.


112-114: Props‑based call is correct for the new API

Good migration off the old request parameter.


122-124: Props‑based call is correct here as well

Consistent with the updated API surface.


223-223: Replace obsolete request: parameter with props: in requestPurchase calls

Update these call sites to the new API param name.

-await iap.requestPurchase(request: b.build(), type: ProductType.Subs);
+await iap.requestPurchase(props: b.build(), type: ProductType.Subs);

Locations:

  • example/lib/src/screens/builder_demo_screen.dart:223
  • example/lib/src/screens/subscription_flow_screen.dart:398
  • example/lib/src/screens/subscription_flow_screen.dart:411
  • example/lib/src/screens/subscription_flow_screen.dart:422
  • example/lib/src/screens/subscription_flow_screen.dart:468
  • example/lib/src/screens/subscription_flow_screen.dart:513
⛔ Skipped due to learnings
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification for parameter naming. According to the specification at https://www.openiap.dev/docs/types#request-types, the parameter name should be "skus" (not "productIds") when requesting products, as confirmed by the project maintainer hyochan.
lib/enums.dart (2)

120-124: AndroidPurchaseState casing/mapping LGTM

Naming matches upstream Play Billing states.


136-147: Enum→value extension LGTM

Clear and explicit mapping.

test/openiap_type_alignment_test.dart (4)

8-24: LGTM: Android product smoke test covers key fields.


26-44: LGTM: iOS product smoke test aligns with new enums and metadata.


46-61: LGTM: Android purchase core fields validated.


63-79: LGTM: iOS purchase fields validated (environment, quantity).

test/types_platform_test.dart (4)

5-24: LGTM: Android product JSON includes platform‑specific keys only.


26-47: LGTM: iOS product JSON includes StoreKit keys and excludes Android keys.


50-92: LGTM: Android subscription exposes pricing phases via offer details.


94-113: LGTM: iOS subscription JSON assertions look correct.

lib/flutter_inapp_purchase.dart (3)

1011-1063: LGTM: _parseOfferDetails guard clauses harden parsing.

Null/shape checks with constant empty returns and whereType are solid.


20-22: Export hygiene check.

Hiding IapPlatform, ErrorCode, PurchaseState from enums while exporting from types is good to avoid duplicate symbols. Confirm that downstream imports (example app) intentionally use the generated versions.


90-93: Verify orderIdAndroid mapping.

This getter returns developerPayloadAndroid; confirm that’s intentional and not a misnamed legacy field.

example/lib/src/screens/subscription_flow_screen.dart (1)

270-275: Verified — ProductSubscription exists and platform models extend it.
lib/types.dart defines sealed class ProductSubscription (line 2504); ProductSubscriptionAndroid (line 1020) and ProductSubscriptionIOS (line 1127) extend it — fetchProducts<ProductSubscription> is valid.

@codecov
Copy link
Copy Markdown

codecov bot commented Sep 17, 2025

Codecov Report

❌ Patch coverage is 28.83333% with 427 lines in your changes missing coverage. Please review.
✅ Project coverage is 27.55%. Comparing base (0e3673c) to head (76c35a6).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/flutter_inapp_purchase.dart 33.46% 340 Missing ⚠️
lib/builders.dart 0.00% 62 Missing ⚠️
lib/enums.dart 14.28% 12 Missing ⚠️
lib/modules/android.dart 0.00% 8 Missing ⚠️
lib/errors.dart 0.00% 5 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main     #555       +/-   ##
===========================================
- Coverage   45.81%   27.55%   -18.26%     
===========================================
  Files          10        8        -2     
  Lines        2115     2210       +95     
===========================================
- Hits          969      609      -360     
- Misses       1146     1601      +455     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/ci.yml (2)

14-17: Use Temurin JDK 17 (current Flutter/AGP baseline).

AdoptOpenJDK 14 is EOL and may break Android builds and tooling.

-      - uses: actions/setup-java@v3
-        with:
-          distribution: "adopt"
-          java-version: "14.x"
+      - uses: actions/setup-java@v4
+        with:
+          distribution: "temurin"
+          java-version: "17"

32-34: Re-enable analyzer gate.

Guidelines require “flutter analyze” in CI.

-      # - run: flutter analyze .
+      - run: flutter analyze .
♻️ Duplicate comments (2)
lib/flutter_inapp_purchase.dart (2)

1894-1896: Fix “all” type detection in fetchProducts.

Comparing the raw type argument to the string 'all' misses enum callers; use resolvedType.

-          final detectedType = (type == 'all')
+          final detectedType = (resolvedType == 'all')
               ? (itemMap['type']?.toString() ?? 'in-app')
               : resolvedType;

1184-1191: Don’t fabricate IDs from timestamps. Filter invalid purchases instead.

Fallback to DateTime.now().millisecondsSinceEpoch risks collisions and hides upstream data issues.

-        'id': (transactionId?.isNotEmpty ?? false)
-            ? transactionId
-            : (productId.isNotEmpty
-                ? productId
-                : DateTime.now().millisecondsSinceEpoch.toString()),
+        'id': (() {
+          final t = (transactionId ?? '').trim();
+          if (t.isNotEmpty) return t;
+          if (productId.isNotEmpty) return productId;
+          throw StateError('Invalid Android purchase: missing id and productId');
+        })(),

Apply the same change to the iOS map at Lines 1238-1244. Then make extractPurchases robust to skip such items:

-    List<iap_types.Purchase>? decoded = list
-        .map<iap_types.Purchase>(
-          (dynamic product) => _convertFromLegacyPurchase(
-            Map<String, dynamic>.from(product as Map),
-            Map<String, dynamic>.from(
-              product,
-            ), // Pass original JSON as well
-          ),
-        )
-        .toList();
+    final decoded = <iap_types.Purchase>[];
+    for (final product in list) {
+      try {
+        decoded.add(_convertFromLegacyPurchase(
+          Map<String, dynamic>.from(product as Map),
+          Map<String, dynamic>.from(product),
+        ));
+      } catch (e) {
+        debugPrint('[flutter_inapp_purchase] Skipping invalid purchase: $e');
+      }
+    }
🧹 Nitpick comments (2)
lib/builders.dart (1)

148-158: Subscription data lost when using RequestPurchaseBuilder for Subs.

When building Subs, Android props are reconstructed with subscriptionOffers: null, discarding offers. Either:

  • expose subscriptionOffers on RequestPurchaseAndroidBuilder, or
  • document and guard against Subs use here.
lib/flutter_inapp_purchase.dart (1)

1980-2022: Prefer type promotion over platform flag + cast.

Use is checks to avoid unsafe casts.

if (purchase is iap_types.PurchaseAndroid) {
  final isActive = (purchase.autoRenewingAndroid ?? false) &&
      purchase.purchaseState == iap_types.PurchaseState.Purchased;
  if (isActive) {
    activeSubscriptions.add(iap_types.ActiveSubscription(
      productId: purchase.productId,
      isActive: true,
      autoRenewingAndroid: purchase.autoRenewingAndroid ?? false,
      transactionDate: purchase.transactionDate,
      transactionId: purchase.id,
      purchaseToken: purchase.purchaseToken,
    ));
  }
} else if (purchase is iap_types.PurchaseIOS) {
  final isSubscription = purchase.purchaseToken != null ||
      purchase.productId.contains('sub');
  final isActive = isSubscription &&
      (purchase.purchaseState == iap_types.PurchaseState.Purchased ||
       purchase.purchaseState == iap_types.PurchaseState.Restored ||
       purchase.purchaseState == iap_types.PurchaseState.Deferred);
  if (isActive) {
    activeSubscriptions.add(iap_types.ActiveSubscription(
      productId: purchase.productId,
      isActive: true,
      expirationDateIOS: purchase.expirationDateIOS,
      environmentIOS: purchase.environmentIOS,
      purchaseToken: purchase.purchaseToken,
      transactionDate: purchase.transactionDate,
      transactionId: purchase.id,
    ));
  }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 518a1dc and 988a989.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml (1 hunks)
  • CLAUDE.md (3 hunks)
  • lib/builders.dart (5 hunks)
  • lib/flutter_inapp_purchase.dart (36 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • CLAUDE.md
🧰 Additional context used
📓 Path-based instructions (4)
lib/**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

Follow the simplified API design where methods use direct parameters instead of parameter objects

Files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
{lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

**/*.dart: Code must be formatted with dart format
Code should pass flutter analyze

**/*.dart: Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed
Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)
Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)
When using generic functions like showModalBottomSheet, specify explicit type arguments (e.g., showModalBottomSheet) to avoid type inference errors
iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)
For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)
Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)
Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)
Exception: standalone iOS fields that end with ID should use all-caps ID (e.g., transactionID, webOrderLineItemID in iOS-only contexts)
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Do not use generic prefixes like get or find for APIs; follow OpenIAP terminology

Files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}: Add appropriate error handling for edge cases
Add comments only for complex logic that isn't self-explanatory

Files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
🧠 Learnings (21)
📓 Common learnings
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Use --set-exit-if-changed with dart format locally to match CI behavior

Applied to files:

  • .github/workflows/ci.yml
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to **/*.dart : Code must be formatted with `dart format`

Applied to files:

  • .github/workflows/ci.yml
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Before committing, run and ensure all pass in order: dart format --set-exit-if-changed ., flutter analyze, flutter test, then dart format --set-exit-if-changed . again; only commit if all succeed

Applied to files:

  • .github/workflows/ci.yml
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Before PR, run: `dart format --set-exit-if-changed .`, `flutter analyze`, `flutter test`

Applied to files:

  • .github/workflows/ci.yml
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to **/*.dart : Code should pass `flutter analyze`

Applied to files:

  • .github/workflows/ci.yml
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: All changes must pass `flutter test`

Applied to files:

  • .github/workflows/ci.yml
📚 Learning: 2025-09-09T06:04:46.743Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-08-21T02:25:46.402Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/index.md:65-69
Timestamp: 2025-08-21T02:25:46.402Z
Learning: When reviewing flutter_inapp_purchase documentation, `purchaseUpdatedListener` and `purchaseErrorListener` are the correct listener names in the public API, not `purchaseUpdated` and `purchaseError`.

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification for parameter naming. According to the specification at https://www.openiap.dev/docs/types#request-types, the parameter name should be "skus" (not "productIds") when requesting products, as confirmed by the project maintainer hyochan.

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification at https://www.openiap.dev/docs/types#request-types for parameter naming conventions. According to hyochan, "skus" is the correct parameter name based on this specification, not "productIds".

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to {lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}} : Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-20T23:53:15.912Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/methods/finish-transaction.md:66-77
Timestamp: 2025-08-20T23:53:15.912Z
Learning: In flutter_inapp_purchase, the correct way to listen to purchase updates is `FlutterInappPurchase.instance.purchaseUpdatedListener.listen`, not `FlutterInappPurchase.purchaseUpdated.listen`.

Applied to files:

  • lib/flutter_inapp_purchase.dart
🔇 Additional comments (2)
lib/flutter_inapp_purchase.dart (2)

1010-1033: LGTM: guard clauses in _parseOfferDetails.

Null/shape checks and safe fallbacks are correct and prevent crashes from malformed native payloads.

Consider logging when input is malformed to aid diagnostics.


1064-1109: LGTM: resilient parsing of PricingPhases.

Defensive handling for both list and wrapped map shapes with safe defaults looks good.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/enums.dart (1)

7-9: Align platform enum with usage and guidelines (IOS/Android).

Example code uses IapPlatform.IOS; enum currently defines ios/android. Rename to IOS/Android to avoid compile errors and follow the suffix guideline.

-/// Platform detection enum
-enum IapPlatform { ios, android }
+/// Platform detection enum
+enum IapPlatform { IOS, Android }
example/lib/src/screens/purchase_flow_screen.dart (1)

222-231: Null-unsafe substring on receipt/token; can throw when null.

Compute safe previews before interpolating.

-      _purchaseResult = '''
+      final _receipt = purchase.transactionReceipt;
+      final _receiptPreview = (_receipt == null || _receipt.isEmpty)
+          ? 'N/A'
+          : '${_receipt.substring(0, _receipt.length > 50 ? 50 : _receipt.length)}...';
+      final _token = purchase.purchaseToken;
+      final _tokenPreview = (_token == null || _token.isEmpty)
+          ? 'N/A'
+          : '${_token.substring(0, _token.length > 30 ? 30 : _token.length)}...';
+      _purchaseResult = '''
 ✅ Purchase successful (${Platform.operatingSystem})
 Product: ${purchase.productId}
 ID: ${purchase.id.isNotEmpty ? purchase.id : "N/A"}
 Transaction ID: ${purchase.transactionId ?? "N/A"}
 Date: ${purchase.transactionDate ?? "N/A"}
-Receipt: ${purchase.transactionReceipt?.substring(0, purchase.transactionReceipt!.length > 50 ? 50 : purchase.transactionReceipt!.length)}...
-Purchase Token: ${purchase.purchaseToken?.substring(0, 30)}...
+Receipt: ${_receiptPreview}
+Purchase Token: ${_tokenPreview}
       '''.trim();

If you prefer to keep changes local, inline closures also work, but the above is clearer.

♻️ Duplicate comments (5)
.github/workflows/ci-gemini-pr.yml (1)

49-54: Gemini auth likely to fail — provide API key (or fully configure OIDC).

You added GITHUB_TOKEN but removed gemini_api_key; run-gemini-cli needs either AI Studio API key or Vertex AI/OIDC wiring. Add the key or complete OIDC setup.

       - name: Analyze issue and create PR if solvable
         id: analyze-pr
         uses: google-github-actions/run-gemini-cli@v0
         timeout-minutes: 20
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         with:
           # Code Assist settings
           use_gemini_code_assist: true
           gemini_model: "gemini-2.5-pro"
+          gemini_api_key: ${{ secrets.GEMINI_API_KEY }}
lib/flutter_inapp_purchase.dart (3)

1899-1902: Fix confirmed: correct handling when type == All

Using resolvedType addresses the earlier misclassification when callers pass the enum.


1163-1190: Do not synthesize purchase IDs from timestamps

Falling back to DateTime.now().millisecondsSinceEpoch risks collisions and hides upstream data issues. Prefer dropping/throwing on invalid records.

Suggested change (Android and iOS branches):

-  final map = <String, dynamic>{
-    'id': (transactionId?.isNotEmpty ?? false)
-        ? transactionId
-        : (productId.isNotEmpty
-            ? productId
-            : DateTime.now().millisecondsSinceEpoch.toString()),
+  final _id = (transactionId?.isNotEmpty ?? false) ? transactionId : (productId.isNotEmpty ? productId : null);
+  if (_id == null) {
+    throw iap_types.PurchaseError(
+      code: iap_types.ErrorCode.ServiceError,
+      message: 'Invalid purchase: missing id and productId',
+    );
+  }
+  final map = <String, dynamic>{
+    'id': _id,

If you prefer filtering instead of throwing, skip such entries in extractPurchases().

Also applies to: 1237-1243


1475-1478: Unsafe cast to PurchaseAndroid in finishTransaction

Use type promotion to avoid runtime cast errors.

Apply:

-      final androidPurchase = purchase as iap_types.PurchaseAndroid;
+      if (purchase is! iap_types.PurchaseAndroid) {
+        throw const iap_types.PurchaseError(
+          code: iap_types.ErrorCode.DeveloperError,
+          message: 'finishTransaction: expected PurchaseAndroid on Android',
+        );
+      }
+      final androidPurchase = purchase;
example/test/available_purchases_screen_test.dart (1)

50-51: Addresses prior review: assert active list, not empty state

The expectations now match the stubbed data.

🧹 Nitpick comments (13)
.github/workflows/ci-gemini-pr.yml (2)

96-108: Conflicting model settings (gemini_model vs settings.model). Pick one.

Two different models are specified; this can cause confusing behavior. Keep gemini_model as the single source of truth and drop settings.model.

           settings: |
             {
               "tools": [
                 { "name": "gh", "description": "GitHub CLI for creating branches and PRs" },
                 { "name": "git", "description": "Git commands for version control" },
                 { "name": "flutter", "description": "Flutter CLI for testing and analysis" },
                 { "name": "dart", "description": "Dart CLI for formatting and analysis" }
               ],
               "repositories": [
                 { "path": ".", "instructions": "Refer to CLAUDE.md for conventions. Keep changes minimal, add tests, and follow pre-commit checks." }
               ],
-              "model": "gemini-1.5-flash"
             }

14-18: Least privilege: keep id-token only if Vertex AI/OIDC is used.

If you stick with AI Studio API key, drop id-token: write to reduce scope.

test/fetch_products_all_test.dart (1)

18-46: Optional: make type hints explicit in mock payload.

Add explicit bools/nums to avoid dynamic surprises in parsing.

-              'isFamilyShareableIOS': false,
+              'isFamilyShareableIOS': false as bool,
-              'price': 24.99,
+              'price': 24.99 as num,
lib/modules/android.dart (2)

3-8: Avoid heavy Material import in a core module; use foundation.dart for debugPrint.

-import 'package:flutter/material.dart';
+import 'package:flutter/foundation.dart';

53-59: Guard enum indexing to avoid RangeError from malformed payloads.

   factory InAppMessage.fromMap(Map<String, dynamic> map) {
     return InAppMessage(
       messageId: map['messageId']?.toString() ?? '',
       campaignName: map['campaignName']?.toString() ?? '',
-      messageType: legacy
-          .InAppMessageType.values[(map['messageType'] as num?)?.toInt() ?? 0],
+      messageType: () {
+        final idx = (map['messageType'] as num?)?.toInt() ?? 0;
+        return (idx >= 0 && idx < legacy.InAppMessageType.values.length)
+            ? legacy.InAppMessageType.values[idx]
+            : legacy.InAppMessageType.generic;
+      }(),
     );
   }
example/lib/src/screens/purchase_flow_screen.dart (1)

126-130: Platform enum casing: ensure IapPlatform.IOS exists.

Per enums.dart it’s currently ios/android. Either adopt the enums.dart fix (preferred) or change this check to IapPlatform.ios.

lib/flutter_inapp_purchase.dart (3)

717-721: Over‑filtering available purchases on iOS

Filtering by non‑empty purchaseToken may exclude valid iOS transactions (e.g., some restored/pending states). Consider accepting entries with productId and transactionId, or platform‑specific criteria.

Suggested tweak:

-            .where((p) =>
-                p.productId.isNotEmpty &&
-                (p.purchaseToken != null && p.purchaseToken!.isNotEmpty))
+            .where((p) =>
+                p.productId.isNotEmpty &&
+                ((p.purchaseToken != null && p.purchaseToken!.isNotEmpty) ||
+                 (p.id.isNotEmpty)))

Also applies to: 728-732


1287-1290: Prefer iap_err.getCurrentPlatform() when mapping error codes

Aligns with project guidance for accurate runtime platform reporting.

Apply:

-      final detected = iap_err.ErrorCodeUtils.fromPlatformCode(
-        result.code!,
-        _platform.isIOS
-            ? iap_types.IapPlatform.IOS
-            : iap_types.IapPlatform.Android,
-      );
+      final detected = iap_err.ErrorCodeUtils.fromPlatformCode(
+        result.code!,
+        iap_err.getCurrentPlatform(),
+      );

1441-1465: consumePurchaseAndroid: swallow-and-false may hide actionable errors

Returning success: false on exceptions reduces diagnosability. Consider returning a typed error or logging with an error code, or surfacing PurchaseError to the caller.

Example:

-    } catch (error) {
-      debugPrint('Error consuming purchase: $error');
-      return const iap_types.VoidResult(success: false);
-    }
+    } catch (error) {
+      throw iap_types.PurchaseError(
+        code: iap_types.ErrorCode.ServiceError,
+        message: 'consumePurchaseAndroid failed: $error',
+      );
+    }
example/test/available_purchases_screen_test.dart (4)

10-37: Hard‑fail unmocked channel methods to catch regressions

Return null masks missing mocks. Throw to surface unexpected calls.

-    channel.setMockMethodCallHandler((MethodCall call) async {
-      switch (call.method) {
+    channel.setMockMethodCallHandler((MethodCall call) async {
+      switch (call.method) {
         case 'initConnection':
           return true;
         case 'getAvailablePurchases':
           return <Map<String, dynamic>>[
             <String, dynamic>{
               'platform': 'android',
               'id': 'txn_123',
               'productId': 'dev.hyo.martie.premium',
               'purchaseToken': 'token_abc',
               'purchaseState': 'PURCHASED',
               'isAutoRenewing': true,
               'autoRenewingAndroid': true,
               'transactionDate': 1700000000.0,
             },
           ];
         case 'getPurchaseHistory':
         case 'getPendingPurchases':
         case 'getAvailableItems':
           return <Map<String, dynamic>>[];
         case 'endConnection':
           return true;
-      }
-      return null;
+        default:
+          throw MissingPluginException('Unmocked method: ${call.method}');
+      }
     });

28-31: Keep mocks minimal

If the screen doesn’t call getPurchaseHistory/getPendingPurchases/getAvailableItems, drop these cases to reduce noise. Re-add only when used.


39-41: Consider tearDownAll if reusing the handler across tests

For multiple tests, moving to tearDownAll reduces setup churn. Fine as‑is for a single test.


43-54: Add negative assertion and settle after dispose

Ensure empty state isn’t shown and let endConnection complete.

   await tester.pumpAndSettle();

   expect(find.text('Active Purchases'), findsOneWidget);
   expect(find.text('dev.hyo.martie.premium'), findsOneWidget);
+  expect(find.text('No purchases found'), findsNothing);

-  await tester.pumpWidget(const SizedBox.shrink());
+  await tester.pumpWidget(const SizedBox.shrink());
+  await tester.pumpAndSettle();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 988a989 and 28b2286.

📒 Files selected for processing (8)
  • .github/workflows/ci-gemini-pr.yml (3 hunks)
  • example/lib/src/screens/builder_demo_screen.dart (8 hunks)
  • example/lib/src/screens/purchase_flow_screen.dart (4 hunks)
  • example/test/available_purchases_screen_test.dart (1 hunks)
  • lib/enums.dart (5 hunks)
  • lib/flutter_inapp_purchase.dart (36 hunks)
  • lib/modules/android.dart (4 hunks)
  • test/fetch_products_all_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • example/lib/src/screens/builder_demo_screen.dart
🧰 Additional context used
📓 Path-based instructions (6)
**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

**/*.dart: Code must be formatted with dart format
Code should pass flutter analyze

**/*.dart: Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed
Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)
Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)
When using generic functions like showModalBottomSheet, specify explicit type arguments (e.g., showModalBottomSheet) to avoid type inference errors
iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)
For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)
Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)
Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)
Exception: standalone iOS fields that end with ID should use all-caps ID (e.g., transactionID, webOrderLineItemID in iOS-only contexts)
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Do not use generic prefixes like get or find for APIs; follow OpenIAP terminology

Files:

  • test/fetch_products_all_test.dart
  • example/test/available_purchases_screen_test.dart
  • lib/modules/android.dart
  • lib/enums.dart
  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/purchase_flow_screen.dart
test/**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

test/**/*.dart: Add tests for new functionality or bug fixes
Ensure new code is covered by tests
For new features, add comprehensive tests
For bug fixes, write a failing test that demonstrates the bug

Files:

  • test/fetch_products_all_test.dart
{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}: Add appropriate error handling for edge cases
Add comments only for complex logic that isn't self-explanatory

Files:

  • test/fetch_products_all_test.dart
  • example/test/available_purchases_screen_test.dart
  • lib/modules/android.dart
  • lib/enums.dart
  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/purchase_flow_screen.dart
example/**

📄 CodeRabbit inference engine (GEMINI.md)

Update the example app when needed for new features

Files:

  • example/test/available_purchases_screen_test.dart
  • example/lib/src/screens/purchase_flow_screen.dart
lib/**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

Follow the simplified API design where methods use direct parameters instead of parameter objects

Files:

  • lib/modules/android.dart
  • lib/enums.dart
  • lib/flutter_inapp_purchase.dart
{lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Files:

  • lib/modules/android.dart
  • lib/enums.dart
  • lib/flutter_inapp_purchase.dart
🧠 Learnings (21)
📓 Common learnings
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: All changes must pass `flutter test`

Applied to files:

  • test/fetch_products_all_test.dart
  • example/test/available_purchases_screen_test.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)

Applied to files:

  • test/fetch_products_all_test.dart
  • lib/modules/android.dart
  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to test/**/*.dart : Add tests for new functionality or bug fixes

Applied to files:

  • test/fetch_products_all_test.dart
  • example/test/available_purchases_screen_test.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to test/**/*.dart : For new features, add comprehensive tests

Applied to files:

  • test/fetch_products_all_test.dart
  • example/test/available_purchases_screen_test.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)

Applied to files:

  • test/fetch_products_all_test.dart
  • example/test/available_purchases_screen_test.dart
  • lib/modules/android.dart
  • lib/enums.dart
  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-09-09T06:04:46.743Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.

Applied to files:

  • test/fetch_products_all_test.dart
  • lib/modules/android.dart
  • lib/enums.dart
  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)

Applied to files:

  • lib/modules/android.dart
  • lib/enums.dart
  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)

Applied to files:

  • lib/modules/android.dart
  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-08-21T02:25:46.402Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/index.md:65-69
Timestamp: 2025-08-21T02:25:46.402Z
Learning: When reviewing flutter_inapp_purchase documentation, `purchaseUpdatedListener` and `purchaseErrorListener` are the correct listener names in the public API, not `purchaseUpdated` and `purchaseError`.

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification for parameter naming. According to the specification at https://www.openiap.dev/docs/types#request-types, the parameter name should be "skus" (not "productIds") when requesting products, as confirmed by the project maintainer hyochan.

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification at https://www.openiap.dev/docs/types#request-types for parameter naming conventions. According to hyochan, "skus" is the correct parameter name based on this specification, not "productIds".

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to {lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}} : Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-20T23:53:15.912Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/methods/finish-transaction.md:66-77
Timestamp: 2025-08-20T23:53:15.912Z
Learning: In flutter_inapp_purchase, the correct way to listen to purchase updates is `FlutterInappPurchase.instance.purchaseUpdatedListener.listen`, not `FlutterInappPurchase.purchaseUpdated.listen`.

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T02:01:17.634Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: According to the OpenIAP specification at https://www.openiap.dev/docs/types#request-types, the parameter name for requesting products should be "skus" rather than "productIds" to maintain compliance with the OpenIAP standard.

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Before PR, run: `dart format --set-exit-if-changed .`, `flutter analyze`, `flutter test`

Applied to files:

  • .github/workflows/ci-gemini-pr.yml
🔇 Additional comments (16)
lib/enums.dart (3)

127-148: androidPurchaseStateFromValue + value extension look solid.

Mapping covers all cases and defaults to Unknown.


204-221: AndroidReplacementMode.value mapping is consistent (0..5).

LGTM.


98-115: Approve — deprecated enums look correct; no internal usages found.

Deprecations are correct; ripgrep shows only the enum declarations in lib/enums.dart and no internal call sites to migrate.

test/fetch_products_all_test.dart (1)

58-76: Test covers mixed product types; API usage looks correct.

Nice split via whereType and direct params (skus/type).

lib/modules/android.dart (2)

16-23: Correct: throw on non-Android usage.

This avoids silent failures. Confirm callers gate by platform.


26-37: Good: non-nullable return with VoidResult and null-response handling.

Channel call parsing and error path returning success:false are appropriate.

example/lib/src/screens/purchase_flow_screen.dart (1)

334-341: Nice: fetchProducts with direct params and ProductType.InApp.

Matches the new direct-params style.

lib/flutter_inapp_purchase.dart (8)

1009-1061: Guard clauses in _parseOfferDetails look good

Early returns for null/invalid inputs improve readability and safety. LGTM.


1063-1109: _parsePricingPhases: robust parsing

Null handling and mixed-structure support are sound. LGTM.


2094-2111: Product type parsing: resilient and consistent

Normalization (INAPP → IN_APP) and fallback logic are solid. LGTM.


1763-1767: Explicit unsupported path for Android receipt validation

Clear error path is fine. Consider documenting this in README/API docs.

Would you like a short docs snippet explaining Android server-side validation flow?


2196-2253: Result wrappers look good

PurchaseResult/ConnectionResult factories and extractResult() are straightforward and consistent with event payloads.


933-949: Incorrect — assignments are type-correct.

lib/types.dart declares oneTimePurchaseOfferDetailsAndroid and subscriptionOfferDetailsAndroid on both ProductAndroid and ProductSubscriptionAndroid (oneTime nullable; subscriptionOfferDetails nullable on ProductAndroid and non‑nullable on ProductSubscriptionAndroid), so the constructor assignments in lib/flutter_inapp_purchase.dart are valid and will not cause analyzer errors. No change required.

Likely an incorrect or invalid review comment.


1111-1144: PurchaseState enum casing and toJson() — verified

lib/types.dart defines PascalCase members (Pending, Purchased, Failed, Restored, …) and toJson() returns the mapped string values; flutter_inapp_purchase's parser accepts case-insensitive strings and numeric codes — no changes required.


90-93: Incorrect: proposed change references non-existent orderIdAndroid field

lib/types.dart’s PurchaseAndroid defines developerPayloadAndroid and id but no orderIdAndroid; changing the getter to .orderIdAndroid will break compilation. Either return the existing id (e.g. (this as iap_types.PurchaseAndroid).id) or add/parses an orderId/orderIdAndroid field in lib/types.dart and its fromJson/toJson. Location: lib/flutter_inapp_purchase.dart:90–93; lib/types.dart:1237–1260.

Likely an incorrect or invalid review comment.

example/test/available_purchases_screen_test.dart (1)

6-9: LGTM: test bootstrap and channel name

Binding init and the flutter_inapp channel setup look correct.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
example/lib/src/screens/builder_demo_screen.dart (1)

89-95: Thanks for removing the unsafe fallback to the first purchase.

Safe loop with explicit match avoids replacing the wrong subscription.

🧹 Nitpick comments (8)
example/lib/src/screens/builder_demo_screen.dart (3)

111-114: Drop deprecated type: when using props:.

requestPurchase ignores the deprecated type when props is supplied; remove it to avoid warnings.

-        await _iap.requestPurchase(
-          props: subBuilder.build(),
-          type: ProductType.Subs,
-        );
+        await _iap.requestPurchase(
+          props: subBuilder.build(),
+        );

122-124: Same here: remove deprecated type: with props:.

-        await _iap.requestPurchase(
-          props: newSub.build(),
-          type: ProductType.Subs,
-        );
+        await _iap.requestPurchase(
+          props: newSub.build(),
+        );

201-204: Update the embedded code sample to reflect the modern props API.

Examples should avoid deprecated request:/type: parameters.

-    ..type = ProductType.InApp
+    ..type = ProductType.InApp
-    ..withIOS((RequestPurchaseIosBuilder i) => i
+    ..withIOS((RequestPurchaseIosBuilder i) => i
@@
-    ..type = ProductType.Subs
+    ..type = ProductType.Subs
-    ..withIOS((RequestPurchaseIosBuilder i) => i..sku = 'dev.hyo.martie.premium')
+    ..withIOS((RequestPurchaseIosBuilder i) => i..sku = 'dev.hyo.martie.premium')
@@
-await iap.requestPurchase(request: b.build(), type: ProductType.Subs);""",
+await iap.requestPurchase(props: b.build());""",

Also applies to: 212-215, 223-223

example/lib/src/screens/purchase_flow_screen.dart (3)

380-392: Migrate to the builder‑based API (avoids deprecated request:).

Use requestPurchaseWithBuilder for clarity and future‑proofing.

-      await _iap.requestPurchase(
-        request: RequestPurchase(
-          ios: RequestPurchaseIOS(
-            sku: productId,
-            quantity: 1,
-          ),
-          android: RequestPurchaseAndroid(
-            skus: [productId],
-          ),
-          type: ProductType.InApp,
-        ),
-      );
+      await _iap.requestPurchaseWithBuilder(
+        build: (r) => r
+          ..type = ProductType.InApp
+          ..withIOS((i) => i
+            ..sku = productId
+            ..quantity = 1)
+          ..withAndroid((a) => a..skus = [productId]),
+      );

214-231: Avoid ! on potentially null receipt/token; compute safe previews.

-    setState(() {
+    final receipt = purchase.transactionReceipt;
+    final token = purchase.purchaseToken;
+    setState(() {
@@
-Receipt: ${purchase.transactionReceipt?.substring(0, purchase.transactionReceipt!.length > 50 ? 50 : purchase.transactionReceipt!.length)}...
-Purchase Token: ${purchase.purchaseToken?.substring(0, 30)}...
+Receipt: ${receipt == null ? 'N/A' : (receipt.length > 50 ? receipt.substring(0, 50) : receipt)}...
+Purchase Token: ${token == null ? 'N/A' : (token.length > 30 ? token.substring(0, 30) : token)}...

269-324: Minor: prefer getCurrentPlatform() for error platform display.

error.platform may be null in legacy compat; use getCurrentPlatform() if available when formatting UI messages.

Would you like a small helper to inject getCurrentPlatform() into error rendering?

lib/flutter_inapp_purchase.dart (2)

210-212: Expose error platform via getCurrentPlatform() in legacy compat.

Helps examples display platform without nulls.

-extension PurchaseErrorLegacyCompat on iap_types.PurchaseError {
-  iap_types.IapPlatform? get platform => null;
-  int? get responseCode => null;
-}
+extension PurchaseErrorLegacyCompat on iap_types.PurchaseError {
+  iap_types.IapPlatform? get platform => iap_err.getCurrentPlatform();
+  int? get responseCode => null;
+}

90-90: Consider surfacing Android order ID if available.

If generated PurchaseAndroid exposes orderIdAndroid, return it here; otherwise keep null.

Do you want me to scan types.dart for an orderIdAndroid field and wire it up if present?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28b2286 and 222d3b4.

📒 Files selected for processing (8)
  • .github/workflows/ci.yml (1 hunks)
  • example/lib/src/screens/builder_demo_screen.dart (8 hunks)
  • example/lib/src/screens/purchase_flow_screen.dart (4 hunks)
  • example/lib/src/screens/subscription_flow_screen.dart (7 hunks)
  • example/test/available_purchases_screen_test.dart (1 hunks)
  • lib/builders.dart (5 hunks)
  • lib/flutter_inapp_purchase.dart (35 hunks)
  • test/fetch_products_all_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • test/fetch_products_all_test.dart
  • example/test/available_purchases_screen_test.dart
  • example/lib/src/screens/subscription_flow_screen.dart
  • .github/workflows/ci.yml
🧰 Additional context used
📓 Path-based instructions (5)
**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

**/*.dart: Code must be formatted with dart format
Code should pass flutter analyze

**/*.dart: Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed
Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)
Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)
When using generic functions like showModalBottomSheet, specify explicit type arguments (e.g., showModalBottomSheet) to avoid type inference errors
iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)
For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)
Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)
Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)
Exception: standalone iOS fields that end with ID should use all-caps ID (e.g., transactionID, webOrderLineItemID in iOS-only contexts)
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Do not use generic prefixes like get or find for APIs; follow OpenIAP terminology

Files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}: Add appropriate error handling for edge cases
Add comments only for complex logic that isn't self-explanatory

Files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
example/**

📄 CodeRabbit inference engine (GEMINI.md)

Update the example app when needed for new features

Files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
lib/**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

Follow the simplified API design where methods use direct parameters instead of parameter objects

Files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
{lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
🧠 Learnings (20)
📓 Common learnings
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.
📚 Learning: 2025-09-09T06:04:46.743Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-08-21T02:01:17.634Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T02:25:46.402Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/index.md:65-69
Timestamp: 2025-08-21T02:25:46.402Z
Learning: When reviewing flutter_inapp_purchase documentation, `purchaseUpdatedListener` and `purchaseErrorListener` are the correct listener names in the public API, not `purchaseUpdated` and `purchaseError`.

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Exception: standalone iOS fields that end with ID should use all-caps ID (e.g., transactionID, webOrderLineItemID in iOS-only contexts)

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • lib/builders.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification for parameter naming. According to the specification at https://www.openiap.dev/docs/types#request-types, the parameter name should be "skus" (not "productIds") when requesting products, as confirmed by the project maintainer hyochan.

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification at https://www.openiap.dev/docs/types#request-types for parameter naming conventions. According to hyochan, "skus" is the correct parameter name based on this specification, not "productIds".

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: According to the OpenIAP specification at https://www.openiap.dev/docs/types#request-types, the parameter name for requesting products should be "skus" rather than "productIds" to maintain compliance with the OpenIAP standard.

Applied to files:

  • example/lib/src/screens/purchase_flow_screen.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed

Applied to files:

  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)

Applied to files:

  • example/lib/src/screens/builder_demo_screen.dart
  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-08-21T02:09:07.921Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/classes/flutter-inapp-purchase.md:97-123
Timestamp: 2025-08-21T02:09:07.921Z
Learning: For flutter_inapp_purchase PRs, focus on the current version being developed (e.g., 6.4) and ignore versioned documentation files (e.g., version-6.3 folders) as those are historical snapshots that should not be modified.

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to {lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}} : Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • lib/builders.dart
📚 Learning: 2025-08-20T23:53:15.912Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/methods/finish-transaction.md:66-77
Timestamp: 2025-08-20T23:53:15.912Z
Learning: In flutter_inapp_purchase, the correct way to listen to purchase updates is `FlutterInappPurchase.instance.purchaseUpdatedListener.listen`, not `FlutterInappPurchase.purchaseUpdated.listen`.

Applied to files:

  • lib/flutter_inapp_purchase.dart
🔇 Additional comments (16)
example/lib/src/screens/builder_demo_screen.dart (2)

42-47: LGTM: enum/type/builder migration looks correct.

ProductType.InApp, RequestPurchaseIosBuilder, and Android skus usage align with the new API and naming rules.


66-71: LGTM: subscription path migration is consistent.

ProductType.Subs and per‑platform builders are used correctly.

example/lib/src/screens/purchase_flow_screen.dart (3)

328-337: LGTM: fetchProducts<Product>(..., type: ProductType.InApp) usage is correct.


115-133: iOS field logs are gated properly.

Conditional prints on IapPlatform.IOS look good.


151-158: Fix enum member casing: use PurchaseState.Purchased.

The generated enums are PascalCase; lower‑case will not compile.

-      final bool condition1 = purchase.purchaseState == PurchaseState.purchased;
+      final bool condition1 = purchase.purchaseState == PurchaseState.Purchased;
⛔ Skipped due to learnings
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/index.md:65-69
Timestamp: 2025-08-21T02:25:46.402Z
Learning: When reviewing flutter_inapp_purchase documentation, `purchaseUpdatedListener` and `purchaseErrorListener` are the correct listener names in the public API, not `purchaseUpdated` and `purchaseError`.
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)
lib/flutter_inapp_purchase.dart (5)

771-795: LGTM: _resolveProductType fixes the “all” enum/string mismatch.

Resolves prior bug where enum All was compared to 'all'.


1007-1107: LGTM: robust OfferDetails/PricingPhases parsing with safe defaults.

Handles Map/List/String and bad input gracefully.


1169-1174: Good: reject purchases lacking identifiers.

Throwing FormatException avoids non‑unique IDs and matches previous guidance.


1442-1466: LGTM: consumePurchaseAndroid returns VoidResult with graceful fallback.


1961-2019: LGTM: active subscriptions logic no longer treats iOS Deferred as active.

lib/builders.dart (6)

3-23: LGTM: iOS builder rename to “Ios” and props mapping are correct.


25-45: LGTM: RequestSubscriptionIosBuilder mirrors purchase builder consistently.


66-90: LGTM: Android subscription builder supports offer inputs and null‑coalescing.


98-127: Rejecting ProductQueryType.All in type setter is the right call.

Prevents impossible builds.


131-180: Explicit error for unsupported All in build() is appropriate.

Informs callers to choose InApp/Subs or use RequestSubscriptionBuilder.


221-231: LGTM: RequestSubscriptionBuilder.build() returns subs payload as expected.

Comment on lines +600 to +607
final sku =
androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';

if (androidRequest.replacementModeAndroid != null &&
androidRequest.replacementModeAndroid != -1 &&
(androidRequest.purchaseTokenAndroid == null ||
androidRequest.purchaseTokenAndroid!.isEmpty)) {
throw const iap_types.PurchaseError(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Validate Android SKU before invoking platform channels.

Avoid sending empty SKUs; throw a clear developer error.

-          final sku =
-              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          final sku =
+              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          if (sku.isEmpty) {
+            throw const iap_types.PurchaseError(
+              code: iap_types.ErrorCode.DeveloperError,
+              message: 'Android subscription sku cannot be empty',
+            );
+          }
@@
-          final sku =
-              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          final sku =
+              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          if (sku.isEmpty) {
+            throw const iap_types.PurchaseError(
+              code: iap_types.ErrorCode.DeveloperError,
+              message: 'Android in‑app sku cannot be empty',
+            );
+          }

Also applies to: 637-641

🤖 Prompt for AI Agents
In lib/flutter_inapp_purchase.dart around lines 600-607 (and similarly at
637-641), the code may send an empty Android SKU to platform channels; validate
the resolved sku string before making any platform calls and if it is empty
throw a clear iap_types.PurchaseError (developer-oriented) with an explanatory
message and appropriate code so callers know this is a developer input error;
apply the same non-empty SKU check and error throw at the other referenced block
(637-641) to prevent empty-SKU invocations.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (7)
test/flutter_inapp_purchase_active_subscriptions_test.dart (7)

35-56: Mock both getAvailableItems and getAvailablePurchases to reduce coupling.

Depending on plugin/native side, the method may be named either way. Support both in the handler to avoid brittle tests.

Apply this diff:

       .setMockMethodCallHandler(channel, (MethodCall call) async {
         switch (call.method) {
           case 'initConnection':
             return true;
-          case 'getAvailableItems':
+          case 'getAvailableItems':
+          case 'getAvailablePurchases':
             return <Map<String, dynamic>>[
               <String, dynamic>{
                 'platform': 'android',
                 'productId': 'sub.android',
                 'transactionId': 'txn_android',
                 'purchaseToken': 'token-123',
                 'purchaseStateAndroid': 1,
                 'isAutoRenewing': true,
                 'autoRenewingAndroid': true,
                 'quantity': 1,
                 'transactionDate': 1700000000000,
               },
+              // Non-purchased entry to assert filtering behavior.
+              <String, dynamic>{
+                'platform': 'android',
+                'productId': 'sub.android.pending',
+                'transactionId': 'txn_android_pending',
+                'purchaseToken': 'token-456',
+                'purchaseStateAndroid': 2, // PENDING
+                'autoRenewingAndroid': false,
+                'transactionDate': 1700000000001,
+              },
             ];
         }
         return null;
       });

58-67: End the IAP connection to avoid cross-test leakage.

Close the channel connection after the test finishes.

Apply this diff:

       final iap = FlutterInappPurchase.private(
         FakePlatform(operatingSystem: 'android'),
       );
 
       await iap.initConnection();
       final subs = await iap.getActiveSubscriptions();
 
       expect(subs, hasLength(1));
       expect(subs.single.productId, 'sub.android');
       expect(subs.single.autoRenewingAndroid, isTrue);
+      await iap.endConnection();

71-90: Mirror the dual-name mocking for iOS too.

Support both getAvailableItems and getAvailablePurchases for parity with Android test.

Apply this diff:

       .setMockMethodCallHandler(channel, (MethodCall call) async {
         switch (call.method) {
           case 'initConnection':
             return true;
-          case 'getAvailableItems':
+          case 'getAvailableItems':
+          case 'getAvailablePurchases':
             return <Map<String, dynamic>>[
               <String, dynamic>{
                 'platform': 'ios',
                 'productId': 'sub.ios',
                 'transactionId': 'txn_ios',
                 'purchaseToken': 'receipt-data',
                 'purchaseState': 'DEFERRED',
                 'transactionDate': 1700000000000,
               },
             ];
         }
         return null;
       });

91-99: Also close the iOS connection.

Apply this diff:

       await iap.initConnection();
       final subs = await iap.getActiveSubscriptions();
 
       expect(subs, isEmpty);
+      await iap.endConnection();

102-121: Mirror dual-name mocking for iOS (purchased) case as well.

Apply this diff:

       .setMockMethodCallHandler(channel, (MethodCall call) async {
         switch (call.method) {
           case 'initConnection':
             return true;
-          case 'getAvailableItems':
+          case 'getAvailableItems':
+          case 'getAvailablePurchases':
             return <Map<String, dynamic>>[
               <String, dynamic>{
                 'platform': 'ios',
                 'productId': 'sub.ios',
                 'transactionId': 'txn_ios',
                 'purchaseToken': 'receipt-data',
                 'purchaseState': 'PURCHASED',
                 'transactionDate': 1700000000000,
               },
             ];
         }
         return null;
       });

126-132: Close the connection after assertions.

Apply this diff:

       await iap.initConnection();
       final subs = await iap.getActiveSubscriptions();
 
       expect(subs, hasLength(1));
       expect(subs.single.productId, 'sub.ios');
       expect(subs.single.environmentIOS, isNull);
+      await iap.endConnection();

22-26: Nit: Drop the synthetic 'platform' field in the input map.

extractPurchases already receives the fake platform via FlutterInappPurchase.private(FakePlatform(...)); the extra 'platform' key is noise.

Apply this diff:

     final result = iap.extractPurchases(
       <Map<String, dynamic>>[
         <String, dynamic>{
-          'platform': 'android',
           'purchaseStateAndroid': 1,
         },
       ],
     );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 222d3b4 and 19e638a.

📒 Files selected for processing (1)
  • test/flutter_inapp_purchase_active_subscriptions_test.dart (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

**/*.dart: Code must be formatted with dart format
Code should pass flutter analyze

**/*.dart: Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed
Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)
Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)
When using generic functions like showModalBottomSheet, specify explicit type arguments (e.g., showModalBottomSheet) to avoid type inference errors
iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)
For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)
Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)
Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)
Exception: standalone iOS fields that end with ID should use all-caps ID (e.g., transactionID, webOrderLineItemID in iOS-only contexts)
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Do not use generic prefixes like get or find for APIs; follow OpenIAP terminology

Files:

  • test/flutter_inapp_purchase_active_subscriptions_test.dart
test/**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

test/**/*.dart: Add tests for new functionality or bug fixes
Ensure new code is covered by tests
For new features, add comprehensive tests
For bug fixes, write a failing test that demonstrates the bug

Files:

  • test/flutter_inapp_purchase_active_subscriptions_test.dart
{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}: Add appropriate error handling for edge cases
Add comments only for complex logic that isn't self-explanatory

Files:

  • test/flutter_inapp_purchase_active_subscriptions_test.dart
🧠 Learnings (6)
📓 Common learnings
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: All changes must pass `flutter test`

Applied to files:

  • test/flutter_inapp_purchase_active_subscriptions_test.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)

Applied to files:

  • test/flutter_inapp_purchase_active_subscriptions_test.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to test/**/*.dart : Add tests for new functionality or bug fixes

Applied to files:

  • test/flutter_inapp_purchase_active_subscriptions_test.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to test/**/*.dart : For new features, add comprehensive tests

Applied to files:

  • test/flutter_inapp_purchase_active_subscriptions_test.dart
📚 Learning: 2025-09-09T06:04:46.743Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.

Applied to files:

  • test/flutter_inapp_purchase_active_subscriptions_test.dart
🔇 Additional comments (2)
test/flutter_inapp_purchase_active_subscriptions_test.dart (2)

10-13: Good teardown hygiene.

Mock channel is reliably reset after each test. LGTM.


79-85: Confirm purchaseState JSON casing (uppercase vs lowercase)

Tests pass 'DEFERRED' / 'PURCHASED' strings, while the Dart side deserializes with PurchaseState.fromJson(json['purchaseState'] as String) and iOS mapping uses _parsePurchaseStateIOS(...).toJson(). Verify PurchaseState.fromJson accepts the uppercase tokens ('DEFERRED','PURCHASED'); if it expects different casing/values, update the serializer or the test fixtures.
Locations: test/flutter_inapp_purchase_active_subscriptions_test.dart (lines ~80, 111), lib/types.dart (PurchaseState.fromJson ~1290), lib/flutter_inapp_purchase.dart (_parsePurchaseStateIOS ~1225).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 (3)
ios/Classes/FlutterInappPurchasePlugin.swift (2)

45-49: Implement real canMakePayments check (don’t return true unconditionally).

Users with parental/enterprise restrictions will get false positives. Use StoreKit (or an OpenIAP helper if available).

-import OpenIAP
+import OpenIAP
+import StoreKit
@@
-        case "canMakePayments":
-            print("\(FlutterInappPurchasePlugin.TAG) canMakePayments called (OpenIAP)")
-            // OpenIAP abstraction: assume payments can be made once initialized
-            result(true)
+        case "canMakePayments":
+            print("\(FlutterInappPurchasePlugin.TAG) canMakePayments called")
+            // Prefer OpenIAP helper if it exists; fallback to StoreKit.
+            #if compiler(>=5.5)
+            if let can = (try? await? (OpenIapModule.shared as AnyObject)
+                .perform?(Selector(("canMakePayments")))) as? Bool {
+                result(can)
+            } else {
+                result(SKPaymentQueue.canMakePayments())
+            }
+            #else
+            result(SKPaymentQueue.canMakePayments())
+            #endif

171-178: Call FlutterResult on the main thread in endConnection.

Currently result(nil) may run off-main. Align with the rest of the file.

-    private func endConnection(result: @escaping FlutterResult) {
+    private func endConnection(result: @escaping FlutterResult) {
         print("\(FlutterInappPurchasePlugin.TAG) endConnection called")
         removeOpenIapListeners()
-        Task {
-            _ = try? await OpenIapModule.shared.endConnection()
-            result(nil)
-        }
+        Task { @MainActor in
+            _ = try? await OpenIapModule.shared.endConnection()
+            result(nil)
+        }
     }
example/lib/src/screens/subscription_flow_screen.dart (1)

315-315: Prevent RangeError from substring on short/null tokens.

Current substring(0, N) will throw if the token is shorter than N.

Apply these diffs:

-            '  - ${p.productId}: token=${p.purchaseToken?.substring(0, 20)}...');
+            '  - ${p.productId}: token=${_ellipsize(p.purchaseToken, 20)}');
-          _purchaseResult =
-              'Active: ${_currentSubscription!.productId}\nToken: ${_currentSubscription!.purchaseToken?.substring(0, 30)}...';
+          _purchaseResult =
+              'Active: ${_currentSubscription!.productId}\nToken: ${_ellipsize(_currentSubscription!.purchaseToken, 30)}';
-                  'Token: ${_currentSubscription!.purchaseToken?.substring(0, 10)}...',
+                  'Token: ${_ellipsize(_currentSubscription!.purchaseToken, 10)}',

Add this helper inside _SubscriptionFlowScreenState (anywhere in the class):

String _ellipsize(String? s, [int max = 10]) {
  if (s == null || s.isEmpty) return '';
  return s.length <= max ? s : '${s.substring(0, max)}...';
}

Also applies to: 335-335, 809-811

🧹 Nitpick comments (13)
ios/Classes/FlutterInappPurchasePlugin.swift (9)

124-129: Verify minimum iOS version for code redemption.

Apple’s code redemption sheet was introduced earlier than iOS 16 in StoreKit; confirm OpenIAP’s requirement and consider lowering the gate if supported.

Would you confirm whether OpenIapModule.presentCodeRedemptionSheetIOS requires iOS 16.0, or can we gate at 14.0?


387-398: Preserve underlying error details when emitting purchase-error and FlutterError.

You’re discarding the thrown error context; include it in the event and in FlutterError.details.

-                let errorData: [String: Any] = [
-                    "code": OpenIapError.PurchaseError,
-                    "message": defaultMessage(for: OpenIapError.PurchaseError),
-                    "productId": sku
-                ]
+                let nsErr = error as NSError
+                let errorData: [String: Any] = [
+                    "code": OpenIapError.PurchaseError,
+                    "message": nsErr.localizedDescription,
+                    "productId": sku,
+                    "details": ["domain": nsErr.domain, "code": nsErr.code]
+                ]
@@
-                let code = OpenIapError.PurchaseError
-                result(FlutterError(code: code, message: defaultMessage(for: code), details: nil))
+                let code = OpenIapError.PurchaseError
+                result(FlutterError(code: code,
+                                    message: defaultMessage(for: code),
+                                    details: ["domain": nsErr.domain, "code": nsErr.code, "description": nsErr.localizedDescription]))

160-167: Include error details in initConnection failure.

Consistency with other paths and better diagnostics.

-                    let code = OpenIapError.InitConnection
-                    result(FlutterError(code: code, message: defaultMessage(for: code), details: nil))
+                    let code = OpenIapError.InitConnection
+                    result(FlutterError(code: code,
+                                        message: defaultMessage(for: code),
+                                        details: (error as NSError).localizedDescription))

341-346: Drop redundant MainActor.run inside MainActor contexts.

You’re already in Task { @mainactor … }. The nested await MainActor.run adds overhead with no benefit.

Also applies to: 412-415, 432-434, 448-450, 462-464, 491-492, 505-506, 521-522, 563-564


306-308: Default onlyIncludeActiveItemsIOS=true may hide expired iOS subs.

If previous behavior returned expired purchases by default, this is a breaking change. Confirm desired default.


248-252: NSNumber→String coercions: centralize and cover id/transactionId.

Looks good; consider extracting a helper to reduce duplication and ensure all numeric IDs are normalized consistently.

Also applies to: 312-340, 336-338


239-246: Listener lifecycle: add deinit safety.

Remove listeners in deinit to avoid leaks if the plugin instance is ever torn down.

 public class FlutterInappPurchasePlugin: NSObject, FlutterPlugin {
@@
     private func removeOpenIapListeners() {
         ...
     }
+
+    deinit {
+        removeOpenIapListeners()
+    }

10-16: Remove dead code: updateListenerTask, processedTransactionIds, StoreError, cleanupExistingState().

These are unused.

-    private var updateListenerTask: Task<Void, Never>?
@@
-    private var processedTransactionIds: Set<String> = []
@@
-    private func cleanupExistingState() {
-        updateListenerTask?.cancel()
-        updateListenerTask = nil
-        processedTransactionIds.removeAll()
-        removeOpenIapListeners()
-    }
@@
-    enum StoreError: Error {
-        case verificationFailed
-        case productNotFound
-        case purchaseFailed
-    }

Also applies to: 180-186, 258-263


578-590: Confirm Legacy registration path actually runs on < iOS 15.

Unless the app’s GeneratedPluginRegistrant calls this Legacy.register, it won’t be used. Consider making a single registrar entry point that branches on availability.

-@available(iOS 15.0, *)
-public class FlutterInappPurchasePlugin: NSObject, FlutterPlugin {
+public class FlutterInappPurchasePlugin: NSObject, FlutterPlugin {
@@
-    public static func register(with registrar: FlutterPluginRegistrar) {
-        print("\(TAG) Swift register called")
-        let channel = FlutterMethodChannel(name: "flutter_inapp", binaryMessenger: registrar.messenger())
-        let instance = FlutterInappPurchasePlugin()
-        registrar.addMethodCallDelegate(instance, channel: channel)
-        instance.channel = channel
-        instance.setupOpenIapListeners()
-    }
+    public static func register(with registrar: FlutterPluginRegistrar) {
+        if #available(iOS 15.0, *) {
+            print("\(TAG) Swift register (>= iOS 15)")
+            let channel = FlutterMethodChannel(name: "flutter_inapp", binaryMessenger: registrar.messenger())
+            let instance = FlutterInappPurchasePlugin()
+            registrar.addMethodCallDelegate(instance, channel: channel)
+            instance.channel = channel
+            instance.setupOpenIapListeners()
+        } else {
+            FlutterInappPurchasePluginLegacy.register(with: registrar)
+        }
+    }
example/lib/src/screens/subscription_flow_screen.dart (4)

175-178: Handle Android Pending explicitly in the pending/unknown branch.

Covers cases where only the Android state is populated.

Apply this diff:

-        } else if (purchase.purchaseState == PurchaseState.Pending ||
-            purchase.purchaseStateAndroid ==
-                AndroidPurchaseState.Unknown.value) {
+        } else if (purchase.purchaseState == PurchaseState.Pending ||
+            purchase.purchaseStateAndroid == AndroidPurchaseState.Pending.value ||
+            purchase.purchaseStateAndroid == AndroidPurchaseState.Unknown.value) {

43-50: Replace magic numbers with AndroidReplacementMode constants.

Keeps UI options in sync with SDK values and improves readability.

Apply this diff:

   final Map<String, int> _prorationModes = {
-    'Immediate with Time Proration': 1,
-    'Immediate and Charge Prorated Price': 2,
-    'Immediate without Proration': 3,
-    'Deferred': 4,
-    'Immediate and Charge Full Price': 5,
+    'Immediate with Time Proration':
+        AndroidReplacementMode.immediateWithTimeProration.value,
+    'Immediate and Charge Prorated Price':
+        AndroidReplacementMode.immediateAndChargeProratedPrice.value,
+    'Immediate without Proration':
+        AndroidReplacementMode.immediateWithoutProration.value,
+    'Deferred': AndroidReplacementMode.deferred.value,
+    'Immediate and Charge Full Price':
+        AndroidReplacementMode.immediateAndChargeFullPrice.value,
   };

78-81: Mask sensitive purchase tokens in logs.

Avoid printing full tokens; truncate consistently.

Apply this diff:

-        debugPrint('  Purchase token: ${purchase.purchaseToken}');
+        debugPrint('  Purchase token: ${_ellipsize(purchase.purchaseToken, 10)}');
-          debugPrint('Purchase token: ${_currentSubscription!.purchaseToken}');
+          debugPrint('Purchase token: ${_ellipsize(_currentSubscription!.purchaseToken, 10)}');

Also applies to: 333-334


503-512: UI text mismatches behavior: “Empty Token” test actually sends a non-empty token.

Avoid confusing testers; rename tooltip/label to “Test token”.

Apply this diff:

-                        child: const Tooltip(
-                          message: 'Test proration with empty token',
-                          child: Text('Empty Token',
-                              style: TextStyle(fontSize: 11)),
+                        child: const Tooltip(
+                          message: 'Test proration with test token',
+                          child: Text('Test Token',
+                              style: TextStyle(fontSize: 11)),

Optionally rename the method to _testTokenProration for clarity.

Also applies to: 772-775

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19e638a and 7e4f117.

📒 Files selected for processing (3)
  • example/lib/src/screens/purchase_flow_screen.dart (4 hunks)
  • example/lib/src/screens/subscription_flow_screen.dart (7 hunks)
  • ios/Classes/FlutterInappPurchasePlugin.swift (22 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • example/lib/src/screens/purchase_flow_screen.dart
🧰 Additional context used
📓 Path-based instructions (5)
**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

**/*.dart: Code must be formatted with dart format
Code should pass flutter analyze

**/*.dart: Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed
Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)
Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)
When using generic functions like showModalBottomSheet, specify explicit type arguments (e.g., showModalBottomSheet) to avoid type inference errors
iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)
For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)
Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)
Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)
Exception: standalone iOS fields that end with ID should use all-caps ID (e.g., transactionID, webOrderLineItemID in iOS-only contexts)
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Do not use generic prefixes like get or find for APIs; follow OpenIAP terminology

Files:

  • example/lib/src/screens/subscription_flow_screen.dart
{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}: Add appropriate error handling for edge cases
Add comments only for complex logic that isn't self-explanatory

Files:

  • example/lib/src/screens/subscription_flow_screen.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
example/**

📄 CodeRabbit inference engine (GEMINI.md)

Update the example app when needed for new features

Files:

  • example/lib/src/screens/subscription_flow_screen.dart
{lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Files:

  • ios/Classes/FlutterInappPurchasePlugin.swift
{android/**,ios/**}

📄 CodeRabbit inference engine (GEMINI.md)

For new features, implement for both iOS and Android when applicable

Files:

  • ios/Classes/FlutterInappPurchasePlugin.swift
🧠 Learnings (9)
📓 Common learnings
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)

Applied to files:

  • example/lib/src/screens/subscription_flow_screen.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)

Applied to files:

  • example/lib/src/screens/subscription_flow_screen.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)

Applied to files:

  • example/lib/src/screens/subscription_flow_screen.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)

Applied to files:

  • example/lib/src/screens/subscription_flow_screen.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification for parameter naming. According to the specification at https://www.openiap.dev/docs/types#request-types, the parameter name should be "skus" (not "productIds") when requesting products, as confirmed by the project maintainer hyochan.

Applied to files:

  • example/lib/src/screens/subscription_flow_screen.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-09-09T06:04:46.743Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.

Applied to files:

  • ios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification at https://www.openiap.dev/docs/types#request-types for parameter naming conventions. According to hyochan, "skus" is the correct parameter name based on this specification, not "productIds".

Applied to files:

  • ios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: According to the OpenIAP specification at https://www.openiap.dev/docs/types#request-types, the parameter name for requesting products should be "skus" rather than "productIds" to maintain compliance with the OpenIAP standard.

Applied to files:

  • ios/Classes/FlutterInappPurchasePlugin.swift
🔇 Additional comments (2)
example/lib/src/screens/subscription_flow_screen.dart (2)

274-277: Product type migration and request wiring look correct.

  • Using skus: param with fetchProducts<ProductSubscription> is aligned with OpenIAP.
  • Supplying type: ProductType.Subs inside RequestPurchase matches the new API.

If you need expired iOS subscriptions during restore, switch to:

await _iap.getAvailablePurchases(options: const PurchaseOptions(includeExpiredIOS: true));

Otherwise current usage is fine.

Also applies to: 398-402, 406-415, 418-426, 468-472, 513-517


102-110: Android purchase-state checks — add explicit Pending guard & confirm enum names.

Add an explicit guard for Android Pending (AndroidPurchaseState.Pending) and defensively handle null/desynced purchase.purchaseState so Pending purchases are not treated as completed.
Location: example/lib/src/screens/subscription_flow_screen.dart — lines ~102–110 (also apply same change at 114–118).
Run flutter analyze locally and confirm the enum names/values/casing used in this branch (AndroidPurchaseState.Purchased | AndroidPurchaseState.Pending | AndroidPurchaseState.Unknown.value) before merging.

@hyochan hyochan force-pushed the fix/lint-curly-braces branch from 9cd23bc to e4bbc29 Compare September 17, 2025 10:36
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 (2)
example/ios/Podfile (1)

41-47: Don’t force SWIFT_VERSION=5.0 for all pods; it can conflict with pods declaring 5.5+.

The plugin podspec sets Swift 5.5. Overriding to 5.0 globally risks build failures. Restrict the override to Runner (or remove).

Apply:

-      config.build_settings['ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES'] = 'YES'
-      config.build_settings['SWIFT_VERSION'] = '5.0'
+      config.build_settings['ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES'] = 'YES'
+      # Only set Swift version for the app target to avoid clobbering pod-declared versions.
+      if target.name == 'Runner'
+        config.build_settings['SWIFT_VERSION'] = '5.5'
+      end
lib/flutter_inapp_purchase.dart (1)

451-472: Use ErrorCode.InitConnection for initConnection failures.

ErrorCode.InitConnection is defined in lib/types.dart — replace iap_types.ErrorCode.NotPrepared with iap_types.ErrorCode.InitConnection in the initConnection catch block (lib/flutter_inapp_purchase.dart, NotPrepared at line ~468).

♻️ Duplicate comments (1)
lib/flutter_inapp_purchase.dart (1)

600-607: Validate Android SKU before platform call.

Empty SKU can reach platform channels and cause hard-to-debug errors.

Apply:

-          final sku =
-              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          final sku =
+              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          if (sku.isEmpty) {
+            throw const iap_types.PurchaseError(
+              code: iap_types.ErrorCode.DeveloperError,
+              message: 'Android subscription sku cannot be empty',
+            );
+          }

And for in‑app:

-          final sku =
-              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          final sku =
+              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          if (sku.isEmpty) {
+            throw const iap_types.PurchaseError(
+              code: iap_types.ErrorCode.DeveloperError,
+              message: 'Android in‑app sku cannot be empty',
+            );
+          }

Also applies to: 647-649

🧹 Nitpick comments (10)
example/ios/Podfile (1)

35-36: Update stale comments and plan to drop git pin once trunk has 1.1.12.

Comments still reference 1.1.9 while the pin is 1.1.12. Also consider switching back to the released spec (no :git) once trunk catches up to avoid source-based installs.

Apply:

-# CocoaPods trunk CDN (ensures latest specs like openiap 1.1.9)
+# CocoaPods trunk CDN (ensures latest specs like openiap 1.1.12)
@@
-  # Ensure openiap resolves to 1.1.9 even if trunk index is lagging
+  # Ensure openiap resolves to 1.1.12 even if trunk index is lagging
   pod 'openiap', :git => 'https://github.com/hyodotdev/openiap-apple.git', :tag => '1.1.12'
+  # Prefer released spec once trunk has 1.1.12:
+  # pod 'openiap', '1.1.12'
CONTRIBUTING.md (3)

130-133: Local‑debugging snippet: good; add syntax highlighting and keep release line in sync.

Use gradle fenced blocks for readability and ensure the release line mirrors build.gradle on bumps.

Apply:

-   ```
+   ```gradle
    // implementation "io.github.hyochan.openiap:openiap-google:1.1.11"
    debugImplementation project(":openiap")
-   releaseImplementation "io.github.hyochan.openiap:openiap-google:1.1.11"
+   releaseImplementation "io.github.hyochan.openiap:openiap-google:1.1.11"
    ```

141-141: Revert instruction: good; also add a note to update this when versions change.

Tiny guard against future drift.

Apply:

-   `implementation "io.github.hyochan.openiap:openiap-google:1.1.11"` line in `android/build.gradle`.
+   `implementation "io.github.hyochan.openiap:openiap-google:<current-version>"` line in `android/build.gradle` (keep this in sync with build.gradle).

105-105: Docs version updated to 1.1.11 — verified match in android/build.gradle and CONTRIBUTING.md.

Both files contain openiap-google:1.1.11; add a CI check to validate the version in android/build.gradle against CONTRIBUTING.md to prevent drift.

lib/flutter_inapp_purchase.dart (4)

1296-1301: Prefer runtime platform helper in error mapping.

Aligns with project guidance to avoid hardcoding platform checks.

Apply:

-        _platform.isIOS
-            ? iap_types.IapPlatform.IOS
-            : iap_types.IapPlatform.Android,
+        iap_err.getCurrentPlatform(),

1488-1497: Avoid unchecked cast; add type guard for safety.

Prevents rare runtime cast errors if a wrong object leaks through.

Apply:

-      final androidPurchase = purchase as iap_types.PurchaseAndroid;
+      if (purchase is! iap_types.PurchaseAndroid) {
+        throw const iap_types.PurchaseError(
+          code: iap_types.ErrorCode.DeveloperError,
+          message: 'finishTransaction: expected Android purchase object',
+        );
+      }
+      final androidPurchase = purchase;

2105-2115: Broaden normalization to accept “in-app”/“in_app”.

Prevents misclassification when native returns hyphen/underscore variants.

Apply:

-    final rawUpper = value?.toString().toUpperCase() ?? 'IN_APP';
-    final normalized = rawUpper == 'INAPP' ? 'IN_APP' : rawUpper;
+    final rawUpper = value?.toString().toUpperCase() ?? 'IN_APP';
+    final normalized = rawUpper
+        .replaceAll('-', '_')
+        .replaceAll('INAPP', 'IN_APP');

2009-2015: iOS: Make subscription detection more robust than “productId contains 'sub'”.

Prefer structural hints to avoid false positives/negatives.

Apply:

-          final receipt = purchase.purchaseToken;
-          final bool isSubscription =
-              receipt != null || purchase.productId.contains('sub');
+          final receipt = purchase.purchaseToken;
+          final bool isSubscription =
+              receipt != null ||
+              purchase.subscriptionGroupIdIOS != null ||
+              purchase.expirationDateIOS != null;
ios/Classes/FlutterInappPurchasePlugin.swift (2)

46-49: Don't always return true for canMakePayments.

If OpenIAP exposes capability or initialization state, use it; else gate on successful init.

Example:

// Pseudocode
let isReady = (try? await OpenIapModule.shared.isReady()) ?? false
result(isReady)

534-535: Unify error messaging style.

Other paths use defaultMessage(for: …); consider same here for consistency.

- result(FlutterError(code: OpenIapError.ServiceError, message: error.localizedDescription, details: nil))
+ let code = OpenIapError.ServiceError
+ result(FlutterError(code: code, message: defaultMessage(for: code), details: nil))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7e4f117 and 9e84c5c.

⛔ Files ignored due to path filters (1)
  • example/ios/Podfile.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • CONTRIBUTING.md (2 hunks)
  • GEMINI.md (0 hunks)
  • android/build.gradle (1 hunks)
  • example/ios/Podfile (1 hunks)
  • example/lib/src/screens/purchase_flow_screen.dart (4 hunks)
  • example/lib/src/screens/subscription_flow_screen.dart (7 hunks)
  • ios/Classes/FlutterInappPurchasePlugin.swift (22 hunks)
  • ios/flutter_inapp_purchase.podspec (1 hunks)
  • lib/flutter_inapp_purchase.dart (36 hunks)
💤 Files with no reviewable changes (1)
  • GEMINI.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • example/lib/src/screens/purchase_flow_screen.dart
  • example/lib/src/screens/subscription_flow_screen.dart
🧰 Additional context used
📓 Path-based instructions (7)
{android/**,ios/**}

📄 CodeRabbit inference engine (GEMINI.md)

For new features, implement for both iOS and Android when applicable

Files:

  • android/build.gradle
  • ios/flutter_inapp_purchase.podspec
  • ios/Classes/FlutterInappPurchasePlugin.swift
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using emojis in documentation, especially in headings

Files:

  • CONTRIBUTING.md
example/**

📄 CodeRabbit inference engine (GEMINI.md)

Update the example app when needed for new features

Files:

  • example/ios/Podfile
lib/**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

Follow the simplified API design where methods use direct parameters instead of parameter objects

Files:

  • lib/flutter_inapp_purchase.dart
{lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Files:

  • lib/flutter_inapp_purchase.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

**/*.dart: Code must be formatted with dart format
Code should pass flutter analyze

**/*.dart: Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed
Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)
Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)
When using generic functions like showModalBottomSheet, specify explicit type arguments (e.g., showModalBottomSheet) to avoid type inference errors
iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)
For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)
Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)
Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)
Exception: standalone iOS fields that end with ID should use all-caps ID (e.g., transactionID, webOrderLineItemID in iOS-only contexts)
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Do not use generic prefixes like get or find for APIs; follow OpenIAP terminology

Files:

  • lib/flutter_inapp_purchase.dart
{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}: Add appropriate error handling for edge cases
Add comments only for complex logic that isn't self-explanatory

Files:

  • lib/flutter_inapp_purchase.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
🧠 Learnings (18)
📓 Common learnings
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Follow the OpenIAP specification for implementations

Applied to files:

  • android/build.gradle
  • CONTRIBUTING.md
📚 Learning: 2025-09-09T06:04:46.743Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-08-21T02:25:46.402Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/index.md:65-69
Timestamp: 2025-08-21T02:25:46.402Z
Learning: When reviewing flutter_inapp_purchase documentation, `purchaseUpdatedListener` and `purchaseErrorListener` are the correct listener names in the public API, not `purchaseUpdated` and `purchaseError`.

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification for parameter naming. According to the specification at https://www.openiap.dev/docs/types#request-types, the parameter name should be "skus" (not "productIds") when requesting products, as confirmed by the project maintainer hyochan.

Applied to files:

  • lib/flutter_inapp_purchase.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to {lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}} : Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-20T23:53:15.912Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/methods/finish-transaction.md:66-77
Timestamp: 2025-08-20T23:53:15.912Z
Learning: In flutter_inapp_purchase, the correct way to listen to purchase updates is `FlutterInappPurchase.instance.purchaseUpdatedListener.listen`, not `FlutterInappPurchase.purchaseUpdated.listen`.

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification at https://www.openiap.dev/docs/types#request-types for parameter naming conventions. According to hyochan, "skus" is the correct parameter name based on this specification, not "productIds".

Applied to files:

  • ios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: According to the OpenIAP specification at https://www.openiap.dev/docs/types#request-types, the parameter name for requesting products should be "skus" rather than "productIds" to maintain compliance with the OpenIAP standard.

Applied to files:

  • ios/Classes/FlutterInappPurchasePlugin.swift
🔇 Additional comments (12)
ios/flutter_inapp_purchase.podspec (1)

18-19: Confirm OpenIAP 1.1.12 is published on CocoaPods trunk and prefer trunk resolution

Podspec pin matches Podfile; verification environment here reports "pod: command not found" — run locally to confirm 1.1.12 is published on CocoaPods trunk and ensure Podfile/CI resolve from trunk for reproducibility.

Run locally:

pod repo update
pod trunk info openiap
pod spec which openiap | xargs basename
pod spec cat openiap | grep -A1 -n "spec.version" | sed -n '1,3p'

Optionally drop deprecated VALID_ARCHS/i386 exclusions in a follow-up:

 s.pod_target_xcconfig = {
   'DEFINES_MODULE' => 'YES',
   'SWIFT_COMPILATION_MODE' => 'wholemodule',
-  'VALID_ARCHS' => 'arm64 x86_64',
-  'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386'
 }
android/build.gradle (1)

54-56: Version bump to 1.1.11 — update comment and verify artifact host & iOS parity

Apply the diff below (android/build.gradle, ~lines 54–56). Confirm where the Android artifact is published (Maven Central vs JitPack/GitHub Packages) — I couldn't find io.github.hyochan.openiap:openiap-google on Maven Central — and whether Android should match iOS (1.1.12). If the version skew is intentional, document it.

-    // OpenIAP Google billing wrapper (1.1.0) includes Google Play Billing
+    // OpenIAP Google billing wrapper (1.1.11) includes Google Play Billing
     implementation "io.github.hyochan.openiap:openiap-google:1.1.11"
lib/flutter_inapp_purchase.dart (4)

1017-1070: LGTM: Guarded parsing in _parseOfferDetails.

Early returns and typed fallbacks reduce crash risk on malformed payloads.


781-804: LGTM: _resolveProductType handles enums/strings uniformly.

This fixes prior “All” enum vs string mismatches in fetchProducts paths.


1017-1041: Confirmed: referenced generated types exist in lib/types.dart.
ProductSubscriptionAndroidOfferDetails (line 1090), PricingPhasesAndroid (line 822), and PricingPhaseAndroid (line 781) are defined in lib/types.dart.


90-91: Expose Android orderId when available.

If lib/types.dart's PurchaseAndroid exposes orderIdAndroid (or orderId/purchaseToken), return it from the getter; otherwise keep null. Ensure the file imports the generated types with the same alias used below (iap_types) or adjust the cast.

Location: lib/flutter_inapp_purchase.dart (lines 90–91)

-  String? get orderIdAndroid => null;
+  String? get orderIdAndroid => (this is iap_types.PurchaseAndroid)
+      ? (this as iap_types.PurchaseAndroid).orderIdAndroid
+      : null;
ios/Classes/FlutterInappPurchasePlugin.swift (6)

34-38: LGTM: Main-thread dispatch centralized.

Using Task { @mainactor … } keeps channel interactions on main without blocking.


279-299: LGTM: fetchProducts validates empty SKU list and maps type.

Good use of QueryProduct error and .inApp/.subs mapping.


413-415: Consistent error semantics for finishTransaction.

ServiceError aligns with Dart side mapping.


124-130: LGTM: iOS 16+ gating for code redemption.

Graceful FeatureNotSupported on older OS versions.


23-31: LGTM: Early listener setup with idempotent guard.

Prevents missed events before initConnection and avoids double-subscription.


588-590: LGTM: Legacy plugin returns FeatureNotSupported on < iOS 15.

Clear and consistent behavior.

Comment on lines +1120 to 1152
if (value is iap_types.PurchaseState) return value;
if (value is String) {
switch (value.toLowerCase()) {
case 'purchasing':
case 'pending':
return iap_types.PurchaseState.Pending;
case 'purchased':
case 'restored':
return iap_types.PurchaseState.Purchased;
case 'failed':
return iap_types.PurchaseState.Failed;
case 'deferred':
return iap_types.PurchaseState.Deferred;
default:
return iap_types.PurchaseState.Unknown;
}
}
if (value is num) {
switch (value.toInt()) {
case 0:
return iap_types.PurchaseState.Pending;
case 1:
return iap_types.PurchaseState.Purchased;
case 2:
return iap_types.PurchaseState.Failed;
case 3:
return iap_types.PurchaseState.Purchased;
case 4:
return iap_types.PurchaseState.Deferred;
}
}
return iap_types.PurchaseState.Unknown;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix iOS purchase state mapping: “restored” must map to Restored (not Purchased).

This collapses semantics and can mis-mark restorations as purchases.

Apply:

-        case 'restored':
-          return iap_types.PurchaseState.Purchased;
+        case 'restored':
+          return iap_types.PurchaseState.Restored;
@@
-        case 3:
-          return iap_types.PurchaseState.Purchased;
+        case 3:
+          return iap_types.PurchaseState.Restored;
📝 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.

Suggested change
if (value is iap_types.PurchaseState) return value;
if (value is String) {
switch (value.toLowerCase()) {
case 'purchasing':
case 'pending':
return iap_types.PurchaseState.Pending;
case 'purchased':
case 'restored':
return iap_types.PurchaseState.Purchased;
case 'failed':
return iap_types.PurchaseState.Failed;
case 'deferred':
return iap_types.PurchaseState.Deferred;
default:
return iap_types.PurchaseState.Unknown;
}
}
if (value is num) {
switch (value.toInt()) {
case 0:
return iap_types.PurchaseState.Pending;
case 1:
return iap_types.PurchaseState.Purchased;
case 2:
return iap_types.PurchaseState.Failed;
case 3:
return iap_types.PurchaseState.Purchased;
case 4:
return iap_types.PurchaseState.Deferred;
}
}
return iap_types.PurchaseState.Unknown;
}
if (value is iap_types.PurchaseState) return value;
if (value is String) {
switch (value.toLowerCase()) {
case 'purchasing':
case 'pending':
return iap_types.PurchaseState.Pending;
case 'purchased':
return iap_types.PurchaseState.Purchased;
case 'restored':
return iap_types.PurchaseState.Restored;
case 'failed':
return iap_types.PurchaseState.Failed;
case 'deferred':
return iap_types.PurchaseState.Deferred;
default:
return iap_types.PurchaseState.Unknown;
}
}
if (value is num) {
switch (value.toInt()) {
case 0:
return iap_types.PurchaseState.Pending;
case 1:
return iap_types.PurchaseState.Purchased;
case 2:
return iap_types.PurchaseState.Failed;
case 3:
return iap_types.PurchaseState.Restored;
case 4:
return iap_types.PurchaseState.Deferred;
}
}
return iap_types.PurchaseState.Unknown;
}
🤖 Prompt for AI Agents
In lib/flutter_inapp_purchase.dart around lines 1120-1152 the iOS purchase state
mapping incorrectly maps the string 'restored' and numeric case 3 to
PurchaseState.Purchased; update these to map to PurchaseState.Restored instead,
leaving all other string and numeric cases unchanged and preserving the final
Unknown fallback.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (2)

322-384: requestPurchase: return@launch used outside launch; move the whole execution into the coroutine.

After the withLock block, the code leaves the coroutine and still uses return@launch, which won’t compile. Also normalize error argument order in this block.

Apply this refactor:

                 scope.launch {
-                    // Ensure connection and listeners under mutex
-                    connectionMutex.withLock {
-                        try {
-                            attachListenersIfNeeded()
-                            openIap?.setActivity(activity)
-                            if (!connectionReady) {
-                                val ok = openIap?.initConnection() ?: false
-                                connectionReady = ok
-                                val item = JSONObject().apply { put("connected", ok) }
-                                channel?.invokeMethod("connection-updated", item.toString())
-                                if (!ok) {
-                                val err = legacyErrorJson(OpenIapErrorCode.InitConnection, "Failed to initialize connection")
-                                channel?.invokeMethod("purchase-error", err.toString())
-                                safe.error(call.method, OpenIapErrorCode.InitConnection, "Failed to initialize connection")
-                                return@withLock
-                            }
-                        }
-                    } catch (e: Exception) {
-                        val err = legacyErrorJson(OpenIapErrorCode.ServiceError, e.message)
-                        channel?.invokeMethod("purchase-error", err.toString())
-                        safe.error(call.method, OpenIapErrorCode.ServiceError, e.message)
-                        return@withLock
-                    }
-                }
-
-                try {
-                    val iap = openIap
-                    if (iap == null) {
-                        safe.error(call.method, OpenIapErrorCode.NotPrepared, "IAP module not initialized.")
-                        return@launch
-                    }
-                    val offers = (params["subscriptionOffers"] as? List<*>)?.mapNotNull { entry ->
+                    // Ensure connection and listeners under mutex
+                    connectionMutex.withLock {
+                        try {
+                            attachListenersIfNeeded()
+                            openIap?.setActivity(activity)
+                            if (!connectionReady) {
+                                val ok = openIap?.initConnection() ?: false
+                                connectionReady = ok
+                                val item = JSONObject().apply { put("connected", ok) }
+                                channel?.invokeMethod("connection-updated", item.toString())
+                                if (!ok) {
+                                    val err = legacyErrorJson(OpenIapErrorCode.InitConnection, "Failed to initialize connection")
+                                    channel?.invokeMethod("purchase-error", err.toString())
+                                    safe.error(OpenIapErrorCode.InitConnection, "Failed to initialize connection", null)
+                                    return@launch
+                                }
+                            }
+                        } catch (e: Exception) {
+                            val err = legacyErrorJson(OpenIapErrorCode.ServiceError, e.message)
+                            channel?.invokeMethod("purchase-error", err.toString())
+                            safe.error(OpenIapErrorCode.ServiceError, e.message, null)
+                            return@launch
+                        }
+                    }
+                    try {
+                        val iap = openIap ?: run {
+                            safe.error(OpenIapErrorCode.NotPrepared, "IAP module not initialized.", null)
+                            return@launch
+                        }
+                        val offers = (params["subscriptionOffers"] as? List<*>)?.mapNotNull { entry ->
                         val map = entry as? Map<*, *> ?: return@mapNotNull null
                         val sku = map["sku"] as? String ?: return@mapNotNull null
                         val offerToken = map["offerToken"] as? String ?: return@mapNotNull null
                         SubscriptionOffer(sku = sku, offerToken = offerToken)
                     }
 
                     val offerList = offers ?: emptyList()
 
                     val requestParams = RequestPurchaseParams(
                         skus = skusNormalized,
                         obfuscatedAccountIdAndroid = obfuscatedAccountId,
                         obfuscatedProfileIdAndroid = obfuscatedProfileId,
                         isOfferPersonalized = isOfferPersonalized,
                         subscriptionOffers = offerList
                     )
 
-                    iap.requestPurchase(
+                    iap.requestPurchase(
                         requestParams,
                         ProductRequest.ProductRequestType.fromString(typeStr)
                     )
                     // Success signaled by purchase-updated event
-                    safe.success(null)
-                } catch (e: Exception) {
-                    channel?.invokeMethod(
-                        "purchase-error",
-                        legacyErrorJson(OpenIapErrorCode.PurchaseError, e.message).toString()
-                    )
-                    safe.error(call.method, OpenIapErrorCode.PurchaseError, e.message)
-                }
+                    safe.success(null)
+                    } catch (e: Exception) {
+                        channel?.invokeMethod(
+                            "purchase-error",
+                            legacyErrorJson(OpenIapErrorCode.PurchaseError, e.message).toString()
+                        )
+                        safe.error(OpenIapErrorCode.PurchaseError, e.message, null)
+                    }
                 }

815-829: Listeners may never attach if openIap is null when first called.

listenersAttached = true is set before checking openIap. If openIap is null, the flag blocks future attachment and you lose events.

Apply this fix:

-    private fun attachListenersIfNeeded() {
-        if (listenersAttached) return
-        listenersAttached = true
-        openIap?.addPurchaseUpdateListener(OpenIapPurchaseUpdateListener { p ->
+    private fun attachListenersIfNeeded() {
+        if (listenersAttached) return
+        val module = openIap ?: return
+        module.addPurchaseUpdateListener(OpenIapPurchaseUpdateListener { p ->
             ...
-        openIap?.addPurchaseErrorListener(OpenIapPurchaseErrorListener { e ->
+        module.addPurchaseErrorListener(OpenIapPurchaseErrorListener { e ->
             ...
         })
+        listenersAttached = true
     }
🧹 Nitpick comments (5)
example/android/app/build.gradle (1)

28-28: Don’t hard‑code NDK; fall back to Flutter’s managed version with an override knob.

Hard‑pinning NDK can break local/CI setups. Prefer Flutter’s flutter.ndkVersion with an opt‑in override via local.properties (e.g., ndk.version=27.0.12077973), and keep the fixed value only as a last‑resort default.

Apply:

-    ndkVersion "27.0.12077973"
+    // Uses local override if present, else Flutter-managed, else a safe default.
+    ndkVersion (localProperties.getProperty('ndk.version') ?: flutter.ndkVersion ?: "27.0.12077973")
CHANGELOG.md (1)

3-9: Fix MD034 (no-bare-urls) and keep changelog lint‑clean.

Wrap the bare URL in a Markdown link.

Apply this diff:

- - refactor: align openiap-gql@1.0.2 https://github.com/hyodotdev/openiap-gql/releases/tag/1.0.2
+ - refactor: align openiap-gql@1.0.2 ([release notes](https://github.com/hyodotdev/openiap-gql/releases/tag/1.0.2))
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (3)

99-103: Safer Application cast when unregistering lifecycle callbacks.

Guard against non-Application contexts to avoid ClassCastException.

Apply this diff:

-            (context as Application).unregisterActivityLifecycleCallbacks(this)
+            (context?.applicationContext as? Application)
+                ?.unregisterActivityLifecycleCallbacks(this)

847-851: Unused constant PLAY_STORE_URL.

Remove if unused to keep the file tidy.

Apply this diff:

-        private const val PLAY_STORE_URL = "https://play.google.com/store/account/subscriptions"

187-205: Duplicate “ensure connection” code across branches; consider an ensureConnected() helper.

Reduces repetition and keeps error semantics centralized.

I can draft a small suspend fun ensureConnected(): Boolean that handles attach, setActivity, init, event emit, and standardized errors, then reuse it in all paths. Want me to open a follow‑up PR with the refactor?

Also applies to: 218-236, 260-278, 485-503, 531-549, 579-597, 618-636

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e84c5c and 7589c1b.

📒 Files selected for processing (5)
  • CHANGELOG.md (1 hunks)
  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (23 hunks)
  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/OpenIapErrorCode.kt (1 hunks)
  • example/android/app/build.gradle (1 hunks)
  • pubspec.yaml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • pubspec.yaml
🧰 Additional context used
📓 Path-based instructions (6)
{lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/OpenIapErrorCode.kt
  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}: Add appropriate error handling for edge cases
Add comments only for complex logic that isn't self-explanatory

Files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/OpenIapErrorCode.kt
  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
{android/**,ios/**}

📄 CodeRabbit inference engine (GEMINI.md)

For new features, implement for both iOS and Android when applicable

Files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/OpenIapErrorCode.kt
  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
CHANGELOG.md

📄 CodeRabbit inference engine (GEMINI.md)

Update CHANGELOG.md after dependency changes

Files:

  • CHANGELOG.md
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using emojis in documentation, especially in headings

Files:

  • CHANGELOG.md
example/**

📄 CodeRabbit inference engine (GEMINI.md)

Update the example app when needed for new features

Files:

  • example/android/app/build.gradle
🧠 Learnings (8)
📓 Common learnings
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.
📚 Learning: 2025-09-09T06:04:46.743Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/OpenIapErrorCode.kt
  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification for parameter naming. According to the specification at https://www.openiap.dev/docs/types#request-types, the parameter name should be "skus" (not "productIds") when requesting products, as confirmed by the project maintainer hyochan.

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification at https://www.openiap.dev/docs/types#request-types for parameter naming conventions. According to hyochan, "skus" is the correct parameter name based on this specification, not "productIds".

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-08-21T02:25:46.402Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/index.md:65-69
Timestamp: 2025-08-21T02:25:46.402Z
Learning: When reviewing flutter_inapp_purchase documentation, `purchaseUpdatedListener` and `purchaseErrorListener` are the correct listener names in the public API, not `purchaseUpdated` and `purchaseError`.

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Follow the OpenIAP specification for implementations

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
🪛 markdownlint-cli2 (0.17.2)
CHANGELOG.md

6-6: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (2)
example/android/app/build.gradle (1)

25-33: Confirm NDK consistency across the repo and CI.

example/android/app/build.gradle pins ndkVersion "27.0.12077973" (lines 25–33). Ensure all Gradle modules, native build files (CMake/ndk-build), third‑party AARs, and CI workflows use this NDK or document/override it (root gradle.properties or README).

The verification script returned "No files were searched" — run these checks and paste outputs:

  • git grep -n -e 'ndkVersion' -e 'ndk.version' -e 'flutter.ndkVersion' -e 'NDK_VERSION' || true
  • grep -RIn --exclude-dir=build --exclude-dir=.git -e 'CMakeLists.txt' -e 'ndk-build' -e 'ndkBuild' -e 'abiFilters' -e 'sdkmanager.*ndk' . || true
  • rg -n --hidden --no-ignore -S 'sdkmanager.*ndk|ANDROID_NDK|NDK_VERSION|actions/setup-android' .github .circleci .gitlab-ci.yml || true
android/src/main/kotlin/dev/hyo/flutterinapppurchase/OpenIapErrorCode.kt (1)

3-14: LGTM: centralized, constant error codes.

The internal object with const vals is fine and keeps codes consistent.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 (1)
lib/flutter_inapp_purchase.dart (1)

1963-2024: Active subscription detection: handle Android “auto‑renew off” and iOS expiry.

  • Android: users who turn off auto‑renew are still active until expiry; don’t key “isSubscription” on boolean value being true.
  • iOS: filter out expired subscriptions when expirationDateIOS is present.

Apply:

@@
-        if (purchase is iap_types.PurchaseAndroid) {
-          final bool isSubscription = purchase.autoRenewingAndroid ?? false;
-          final bool isActive = isSubscription &&
-              purchase.purchaseState == iap_types.PurchaseState.Purchased;
+        if (purchase is iap_types.PurchaseAndroid) {
+          // Presence of the field indicates a sub; value may be false when auto‑renew is off.
+          final bool isSubscription = purchase.autoRenewingAndroid != null;
+          final bool isActive =
+              purchase.purchaseState == iap_types.PurchaseState.Purchased;
@@
-        } else if (purchase is iap_types.PurchaseIOS) {
-          final receipt = purchase.purchaseToken;
-          final bool isSubscription =
-              receipt != null || purchase.productId.contains('sub');
-          final bool isActive = (purchase.purchaseState ==
-                      iap_types.PurchaseState.Purchased ||
-                  purchase.purchaseState == iap_types.PurchaseState.Restored) &&
-              isSubscription;
+        } else if (purchase is iap_types.PurchaseIOS) {
+          // More reliable than heuristics: group ID implies subscription.
+          final bool isSubscription = purchase.subscriptionGroupIdIOS != null;
+          final bool isPurchasedOrRestored =
+              purchase.purchaseState == iap_types.PurchaseState.Purchased ||
+              purchase.purchaseState == iap_types.PurchaseState.Restored;
+          final double nowMs =
+              DateTime.now().millisecondsSinceEpoch.toDouble();
+          final bool notExpired = purchase.expirationDateIOS == null ||
+              (purchase.expirationDateIOS! > nowMs);
+          final bool isActive = isSubscription && isPurchasedOrRestored && notExpired;
♻️ Duplicate comments (3)
lib/flutter_inapp_purchase.dart (3)

1849-1908: LGTM: using resolvedType fixes the enum/string mismatch for “All”.

This addresses the prior bug where enum callers caused misclassification.


1114-1147: Fix iOS purchase state mapping: “restored” must map to Restored (not Purchased).

Incorrect mapping collapses semantics and will mis-mark restorations as purchases.

Apply:

-        case 'restored':
-          return iap_types.PurchaseState.Purchased;
+        case 'restored':
+          return iap_types.PurchaseState.Restored;
@@
-        case 3:
-          return iap_types.PurchaseState.Purchased;
+        case 3:
+          return iap_types.PurchaseState.Restored;

595-607: Validate non‑empty Android SKU before invoking platform channels (subs + in‑app).

Prevent empty productId calls to the native layer.

Apply:

@@
-          final sku =
-              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          final sku =
+              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          if (sku.isEmpty) {
+            throw const iap_types.PurchaseError(
+              code: iap_types.ErrorCode.DeveloperError,
+              message: 'Android subscription sku cannot be empty',
+            );
+          }
@@
-          final sku =
-              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          final sku =
+              androidRequest.skus.isNotEmpty ? androidRequest.skus.first : '';
+          if (sku.isEmpty) {
+            throw const iap_types.PurchaseError(
+              code: iap_types.ErrorCode.DeveloperError,
+              message: 'Android in‑app sku cannot be empty',
+            );
+          }

Also applies to: 642-649

🧹 Nitpick comments (9)
.gitignore (2)

8-8: Confirm intent for .build/ and document it

Looks fine, but please confirm .build/ only holds generated artifacts (e.g., codegen/temp outputs) and is safe to ignore across all packages. Add a short comment for future contributors.

Proposed tweak:

+# Generated artifacts
 .build/

If lib/types.dart is not meant to be committed, consider ignoring it too; otherwise, ensure CI verifies it’s up‑to‑date.


42-42: Scope NDK .cxx ignore more clearly (and dedupe)

Using *.cxx/ works, but the recursive form improves intent and avoids accidental matches; then the specific path on Line 41 becomes redundant.

Proposed tweak:

-example/android/app/.cxx/
-*.cxx/
+**/.cxx/
tool/lints/core.yaml (1)

1-3: Local copy attribution looks good; add an upgrade note.

Add a brief note on how/when to sync with upstream (e.g., target flutter_lints tag, manual diff steps) to keep rules current.

tool/lints/flutter.yaml (1)

8-20: Flutter rules selection makes sense; consider documenting noisy rules.

Optionally note in analysis_options.yaml how to locally suppress known-noisy findings (e.g., use_build_context_synchronously) to guide contributors.

tool/lints/recommended.yaml (1)

8-62: Rule list LGTM; confirm generated-code exclusions upstream.

Ensure analysis_options.yaml excludes generated files (e.g., **/.g.dart, **/.freezed.dart) so these rules don’t flag generated code.

If not present, add:

analyzer:
  exclude:
    - "**/*.g.dart"
    - "**/*.freezed.dart"
lib/flutter_inapp_purchase.dart (4)

1181-1190: Parse numeric string transactionDate from legacy JSON.

iOS often supplies milliseconds as a string; current path leaves it at 0.

Apply:

-    } else if (transactionDateValue is String) {
-      final parsedDate = DateTime.tryParse(transactionDateValue);
-      if (parsedDate != null) {
-        transactionDate = parsedDate.millisecondsSinceEpoch.toDouble();
-      }
-    }
+    } else if (transactionDateValue is String) {
+      final asNum = num.tryParse(transactionDateValue);
+      if (asNum != null) {
+        transactionDate = asNum.toDouble();
+      } else {
+        final parsedDate = DateTime.tryParse(transactionDateValue);
+        if (parsedDate != null) {
+          transactionDate = parsedDate.millisecondsSinceEpoch.toDouble();
+        }
+      }
+    }

1905-1907: Default detectedType to 'inapp' (not 'in-app').

Avoid relying on fallback parsing; use canonical enum name.

Apply:

-          final detectedType = (resolvedType == 'all')
-              ? (itemMap['type']?.toString() ?? 'in-app')
+          final detectedType = (resolvedType == 'all')
+              ? (itemMap['type']?.toString() ?? TypeInApp.inapp.name)
               : resolvedType;

1462-1471: Don’t silently swallow consume errors.

Either rethrow as PurchaseError or return a structured failure with reason.

Apply (option A — rethrow):

-    } catch (error) {
-      debugPrint('Error consuming purchase: $error');
-      return const iap_types.VoidResult(success: false);
-    }
+    } catch (error) {
+      throw iap_types.PurchaseError(
+        code: iap_types.ErrorCode.ServiceError,
+        message: 'consumePurchaseAndroid failed: $error',
+      );
+    }

1482-1484: Guard the Android cast in finishTransaction.

Be defensive in mixed/test contexts to avoid a runtime cast error.

Apply:

-      final androidPurchase = purchase as iap_types.PurchaseAndroid;
+      if (purchase is! iap_types.PurchaseAndroid) {
+        throw const iap_types.PurchaseError(
+          code: iap_types.ErrorCode.DeveloperError,
+          message: 'finishTransaction: expected Android purchase object',
+        );
+      }
+      final androidPurchase = purchase;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7589c1b and ab9706c.

📒 Files selected for processing (7)
  • .gitignore (2 hunks)
  • analysis_options.yaml (2 hunks)
  • example/lib/src/screens/purchase_flow_screen.dart (4 hunks)
  • lib/flutter_inapp_purchase.dart (35 hunks)
  • tool/lints/core.yaml (1 hunks)
  • tool/lints/flutter.yaml (1 hunks)
  • tool/lints/recommended.yaml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • analysis_options.yaml
  • example/lib/src/screens/purchase_flow_screen.dart
🧰 Additional context used
📓 Path-based instructions (4)
lib/**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

Follow the simplified API design where methods use direct parameters instead of parameter objects

Files:

  • lib/flutter_inapp_purchase.dart
{lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Files:

  • lib/flutter_inapp_purchase.dart
**/*.dart

📄 CodeRabbit inference engine (GEMINI.md)

**/*.dart: Code must be formatted with dart format
Code should pass flutter analyze

**/*.dart: Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed
Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)
Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)
When using generic functions like showModalBottomSheet, specify explicit type arguments (e.g., showModalBottomSheet) to avoid type inference errors
iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)
For field names with iOS in the middle, use Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)
Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)
Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)
Exception: standalone iOS fields that end with ID should use all-caps ID (e.g., transactionID, webOrderLineItemID in iOS-only contexts)
Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)
Do not use generic prefixes like get or find for APIs; follow OpenIAP terminology

Files:

  • lib/flutter_inapp_purchase.dart
{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}: Add appropriate error handling for edge cases
Add comments only for complex logic that isn't self-explanatory

Files:

  • lib/flutter_inapp_purchase.dart
🧠 Learnings (16)
📓 Common learnings
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/faq.md:164-172
Timestamp: 2025-08-21T02:01:17.634Z
Learning: When reviewing PRs for flutter_inapp_purchase, focus on the current version being developed rather than versioned documentation files, as those are historical snapshots.
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to **/*.dart : Code should pass `flutter analyze`

Applied to files:

  • tool/lints/flutter.yaml
  • tool/lints/core.yaml
  • tool/lints/recommended.yaml
📚 Learning: 2025-09-09T06:04:46.743Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T02:25:46.402Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/index.md:65-69
Timestamp: 2025-08-21T02:25:46.402Z
Learning: When reviewing flutter_inapp_purchase documentation, `purchaseUpdatedListener` and `purchaseErrorListener` are the correct listener names in the public API, not `purchaseUpdated` and `purchaseError`.

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Android-related symbols should use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : iOS-related symbols should use IOS suffix; when not final suffix, use Ios (e.g., PurchaseIOS, SubscriptionOfferIOS, IosManager, IosHelper)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Functions that depend on event results should use the request prefix (e.g., requestPurchase, requestSubscription)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Replace RequestProductsParams with direct parameters in requestProducts; the RequestProductsParams class is removed

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use Id (camel-case) consistently (e.g., productId, transactionId, offerId); when combined with platform suffixes, place Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Exception: standalone iOS fields that end with ID should use all-caps ID (e.g., transactionID, webOrderLineItemID in iOS-only contexts)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification for parameter naming. According to the specification at https://www.openiap.dev/docs/types#request-types, the parameter name should be "skus" (not "productIds") when requesting products, as confirmed by the project maintainer hyochan.

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to {lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}} : Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-08-20T23:53:15.912Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/methods/finish-transaction.md:66-77
Timestamp: 2025-08-20T23:53:15.912Z
Learning: In flutter_inapp_purchase, the correct way to listen to purchase updates is `FlutterInappPurchase.instance.purchaseUpdatedListener.listen`, not `FlutterInappPurchase.purchaseUpdated.listen`.

Applied to files:

  • lib/flutter_inapp_purchase.dart
⏰ 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 (4)
tool/lints/flutter.yaml (1)

1-5: Include layering is correct for local lint stacks.

Local include of recommended.yaml keeps CI independent of external package resolution.

tool/lints/recommended.yaml (1)

4-4: Chaining to core.yaml is correct.

This mirrors flutter_lints’ structure and keeps duplication minimal.

tool/lints/core.yaml (1)

4-37: Rule set LGTM — verify analyzer/SDK in CI

Include chain is correct (analysis_options.yaml → tool/lints/flutter.yaml → tool/lints/recommended.yaml → tool/lints/core.yaml). Pubspec environment: sdk: ">=3.0.0 <4.0.0", flutter: ">=3.10.0". Ensure CI uses a Dart/Flutter SDK (and analyzer) that recognizes these lint rules to avoid "unrecognized lint rule" failures.

lib/flutter_inapp_purchase.dart (1)

90-90: Expose Android order ID (currently always null).

  • Replace the getter so it returns the Android order id when the runtime type is iap_types.PurchaseAndroid:
-  String? get orderIdAndroid => null;
+  String? get orderIdAndroid => this is iap_types.PurchaseAndroid
+      ? (this as iap_types.PurchaseAndroid).orderIdAndroid
+      : null;
  • Propagate the field in legacy → model Android conversion:
@@
         'signatureAndroid': itemJson['signatureAndroid']?.toString(),
+        'orderIdAndroid': itemJson['orderIdAndroid']?.toString() ??
+            itemJson['orderId']?.toString(),

Location: lib/flutter_inapp_purchase.dart:90 — current repo search shows no other occurrences of orderIdAndroid; confirm iap_types.PurchaseAndroid exposes orderIdAndroid and update the legacy mapping location before merging.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 (1)
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (1)

92-99: Fix cancellation race in onDetachedFromActivity — endConnection may never run

job.cancel() immediately cancels the launched coroutine; endConnection() can be skipped, leaking BillingClient/listeners.

Apply:

 fun onDetachedFromActivity() {
-        scope.launch {
-            kotlin.runCatching { openIap?.endConnection() }
-            connectionReady = false
-        }
-        // Cancel coroutine job to avoid leaks
-        job.cancel()
+        try {
+            openIap?.endConnection()
+        } catch (e: Exception) {
+            Log.w(TAG, "endConnection failed on detach", e)
+        } finally {
+            connectionReady = false
+            // Cancel coroutine job to avoid leaks
+            job.cancel()
+        }
 }
♻️ Duplicate comments (1)
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (1)

119-121: Error argument order fixed (code, message, details) — thanks

This resolves the prior C/R about flipped arguments.

🧹 Nitpick comments (6)
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (6)

14-16: Remove unused import

RequestSubscriptionAndroidProps (Line 15) isn’t used; keep only SubscriptionOffer.

-import dev.hyo.openiap.models.RequestSubscriptionAndroidProps
 import dev.hyo.openiap.models.RequestSubscriptionAndroidProps.SubscriptionOffer

319-319: Deduplicate SKUs before request

Prevent accidental duplicates being sent downstream.

-                val skusNormalized = skus.filter { it.isNotBlank() }
+                val skusNormalized = skus.filter { it.isNotBlank() }.distinct()

368-389: Guard subscriptionOffers to subscriptions only

Passing subscriptionOffers for non-subs purchases can cause confusing errors downstream. Validate early.

-                    val offerList = offers ?: emptyList()
+                    val offerList = offers ?: emptyList()
+                    val reqTypeForPurchase = ProductRequest.ProductRequestType.fromString(typeStr)
+                    if (offerList.isNotEmpty() && reqTypeForPurchase != ProductRequest.ProductRequestType.Subs) {
+                        val msg = "subscriptionOffers is only valid when type=subs"
+                        channel?.invokeMethod("purchase-error", legacyErrorJson(OpenIapError.DeveloperError.CODE, OpenIapError.DeveloperError.MESSAGE, msg).toString())
+                        safe.error(OpenIapError.DeveloperError.CODE, OpenIapError.DeveloperError.MESSAGE, msg)
+                        return@launch
+                    }
...
-                    iap.requestPurchase(
-                        requestParams,
-                        ProductRequest.ProductRequestType.fromString(typeStr)
-                    )
+                    iap.requestPurchase(requestParams, reqTypeForPurchase)

829-861: Unify purchase-error fallback payload with helper and keep platform

Use legacyErrorJson for consistent shape; append platform to preserve current field.

-                        else -> JSONObject(
-                            mapOf(
-                                "code" to OpenIapError.PurchaseFailed.CODE,
-                                "message" to (e.message ?: "Purchase error"),
-                                "platform" to "android"
-                            )
-                        )
+                        else -> legacyErrorJson(
+                            OpenIapError.PurchaseFailed.CODE,
+                            OpenIapError.PurchaseFailed.MESSAGE,
+                            e.message
+                        ).apply { put("platform", "android") }

231-250: DRY up “ensure connection + listeners” logic

The same pattern repeats across endpoints. Extract a small helper (returns boolean) to reduce risk of divergence and cut code size.

Example helper (add near other privates):

private suspend fun ensureConnected(safe: MethodResultWrapper, emitPurchaseError: Boolean = false): Boolean {
  return try {
    attachListenersIfNeeded()
    openIap?.setActivity(activity)
    if (!connectionReady) {
      val ok = openIap?.initConnection() ?: false
      connectionReady = ok
      channel?.invokeMethod("connection-updated", JSONObject().put("connected", ok).toString())
      if (!ok) {
        val msg = "Failed to initialize connection"
        if (emitPurchaseError) channel?.invokeMethod("purchase-error", legacyErrorJson(OpenIapError.InitConnection.CODE, OpenIapError.InitConnection.MESSAGE, msg).toString())
        safe.error(OpenIapError.InitConnection.CODE, OpenIapError.InitConnection.MESSAGE, msg)
      }
      ok
    } else true
  } catch (e: Exception) {
    if (emitPurchaseError) channel?.invokeMethod("purchase-error", legacyErrorJson(OpenIapError.BillingError.CODE, OpenIapError.BillingError.MESSAGE, e.message).toString())
    safe.error(OpenIapError.BillingError.CODE, OpenIapError.BillingError.MESSAGE, e.message)
    false
  }
}

Then replace the repeated with:

connectionMutex.withLock {
  if (!ensureConnected(safe, emitPurchaseError = /* true for purchase flows */ false)) return@launch
}

Want me to push a full patch across all sites?

Also applies to: 273-294, 499-517, 545-563, 593-612, 631-650, 689-711


864-866: Tidy constants: correct TAG, drop unused URL

Nit-level cleanup.

-        private const val TAG = "InappPurchasePlugin"
-        private const val PLAY_STORE_URL = "https://play.google.com/store/account/subscriptions"
+        private const val TAG = "AndroidInappPurchasePlugin"
+        // PLAY_STORE_URL unused; remove if not needed
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab9706c and 76c35a6.

📒 Files selected for processing (1)
  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (26 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
{lib/**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

Use consistent platform suffixes in names (IOS for iOS, Android for Android)

Files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}

📄 CodeRabbit inference engine (GEMINI.md)

{**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}}: Add appropriate error handling for edge cases
Add comments only for complex logic that isn't self-explanatory

Files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
{android/**,ios/**}

📄 CodeRabbit inference engine (GEMINI.md)

For new features, implement for both iOS and Android when applicable

Files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Implementations must follow the OpenIAP specification for APIs, types, events, and errors
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: When adding a new feature, verify it aligns with the OpenIAP specification
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions (v6.4.6+) when needed (e.g., include expired iOS subscriptions); do not use getPurchaseHistories (deprecated)

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification for parameter naming. According to the specification at https://www.openiap.dev/docs/types#request-types, the parameter name should be "skus" (not "productIds") when requesting products, as confirmed by the project maintainer hyochan.

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-09-09T07:22:04.861Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-09T07:22:04.861Z
Learning: Applies to **/*.dart : Calls to requestProducts should pass skus and optional type directly (type defaults to PurchaseType.inapp)

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-08-21T01:20:28.525Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/overview.md:128-131
Timestamp: 2025-08-21T01:20:28.525Z
Learning: The flutter_inapp_purchase library follows the OpenIAP specification at https://www.openiap.dev/docs/types#request-types for parameter naming conventions. According to hyochan, "skus" is the correct parameter name based on this specification, not "productIds".

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-09-09T06:04:46.743Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#545
File: lib/flutter_inapp_purchase.dart:1665-1665
Timestamp: 2025-09-09T06:04:46.743Z
Learning: In flutter_inapp_purchase project, prefer using iap_err.getCurrentPlatform() in error handling over hardcoded platform constants to ensure accurate runtime platform reporting in error messages and debugging.

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Applies to {**/*.dart,android/**/*.{kt,kts,java},ios/**/*.{swift,m,mm,h}} : Add appropriate error handling for edge cases

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-08-21T02:25:46.402Z
Learnt from: hyochan
PR: hyochan/flutter_inapp_purchase#531
File: docs/versioned_docs/version-6.3/api/index.md:65-69
Timestamp: 2025-08-21T02:25:46.402Z
Learning: When reviewing flutter_inapp_purchase documentation, `purchaseUpdatedListener` and `purchaseErrorListener` are the correct listener names in the public API, not `purchaseUpdated` and `purchaseError`.

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
📚 Learning: 2025-08-31T19:59:33.928Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: GEMINI.md:0-0
Timestamp: 2025-08-31T19:59:33.928Z
Learning: Follow the OpenIAP specification for implementations

Applied to files:

  • android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt
🔇 Additional comments (1)
android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt (1)

51-75: Good: centralized error helpers

The legacyErrorJson and MethodResultWrapper.error helpers reduce duplication and fix error argument ordering.

@hyochan hyochan changed the title migration: openiap-gql integration migration: consolidate OpenIAP packages (gql, google, apple) Sep 18, 2025
@hyochan hyochan merged commit 9edcb5c into main Sep 18, 2025
2 of 4 checks passed
@hyochan hyochan deleted the fix/lint-curly-braces branch September 18, 2025 02:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant