Skip to content

refactor: modernize iOS Plugin and Improve Purchase Flow#560

Merged
hyochan merged 5 commits intomainfrom
feat/refactor-purchase-flow
Sep 21, 2025
Merged

refactor: modernize iOS Plugin and Improve Purchase Flow#560
hyochan merged 5 commits intomainfrom
feat/refactor-purchase-flow

Conversation

@hyochan
Copy link
Copy Markdown
Owner

@hyochan hyochan commented Sep 21, 2025

Refactors the iOS plugin, improves purchase flow, and updates the example app.

Summary by CodeRabbit

  • New Features

    • iOS: Purchase History section with reload, loading, and error states.
    • Subscription screen shows detailed status (active/inactive, auto‑renew, expiration, expiring soon, environment) with human‑readable dates.
  • Improvements

    • Unified available‑purchases retrieval across platforms and more reliable subscription detection.
    • Better iOS diagnostics/logging and safer, consistent purchase/finish flows.
    • Robust input normalization for varied payload shapes.
  • Bug Fixes

    • Prevents UI updates after screens are closed; fixes loading vs empty-state handling.
  • Tests

    • Added unit test for iOS subscription parsing.
  • Chores

    • Added iOS example launch configuration; more resilient build file discovery.

Revamps the iOS native layer for better stability and maintainability.

Introduces `FlutterIapHelper` for data sanitization and parsing, and `FlutterIapLog` for structured logging. The main plugin class is refactored to delegate logic to these new components, resulting in cleaner, more robust code aligned with the underlying OpenIAP library.
Enhances the core logic for handling subscriptions and purchases.

This commit refactors the `requestPurchase`, `getAvailableItems`, and `finishTransaction` methods for improved reliability. It also introduces helper functions for data normalization and adds tests for parsing subscription information, ensuring more robust data handling.
Improves the example apps UI for managing subscriptions and viewing purchase history.

This commit refactors the subscription and purchase screens to provide a clearer and more interactive user experience. Key changes include separating purchase history loading, adding explicit buttons to load or reload history, and displaying more detailed subscription information like expiration dates and auto-renewal status.
Updates project dependencies and build-related configurations.

This commit updates the `openiap` dependency to version `1.2.2`. It also includes improvements to the Android build script for locating version files and adds a new VS Code launch configuration for easier debugging on iOS devices.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Sep 21, 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

Adds iOS helper and logging utilities, refactors iOS plugin to use OpenIAP decoding and standardized errors, normalizes Dart parsing and purchase flows, enhances example screens (iOS history/subscriptions), adds normalization helpers and tests, makes Android Gradle locate openiap-versions.json upward, and adds a VSCode launch config.

Changes

Cohort / File(s) Summary
Editor config
/.vscode/launch.json
Adds a Flutter iOS device launch configuration for the example app.
Android build tooling
/android/build.gradle
Adds locateOpeniapVersionsFile(startDir) to search upward for openiap-versions.json; replaces fixed-path lookup and throws GradleException if not found.
Example app – purchases UI/flow
/example/lib/src/screens/available_purchases_screen.dart
Adds iOS-only purchase history loading with separate loading/error state, guarded setState, and conditional history UI (load button, loading/error/list).
Example app – purchase flow
/example/lib/src/screens/purchase_flow_screen.dart
Simplifies product extraction by using result.inAppProducts() directly.
Example app – subscription flow
/example/lib/src/screens/subscription_flow_screen.dart
Adds active-subscription resolution combining summaries and purchases, tracks active-subscription info, updates UI/cards, formatting, and restore flow.
iOS helpers
/ios/Classes/FlutterIapHelper.swift
New utility enum providing sanitization, JSON helpers, payload normalization/decoding (product requests, purchase options, request props, receipt validation), and deterministic fallback inputs.
iOS logging
/ios/Classes/FlutterIapLog.swift
New structured logging utility with levels, enable/handler APIs, payload/result/failure helpers, and OSLogger/NSLog fallback.
iOS plugin refactor
/ios/Classes/FlutterInappPurchasePlugin.swift
Refactors to use FlutterIapHelper/FlutterIapLog, standardizes error code/messages, updates fetch/getAvailable/request/finish flows and result sanitization.
Dart core – plugin internals
/lib/flutter_inapp_purchase.dart
Adds _buildIosPurchasePayload, unifies getAvailablePurchases via normalized PurchaseOptions and resolvePurchases(), changes finishTransaction payload shape, normalizes fetchProducts items, and inlines Android acknowledge success checks.
Dart utilities
/lib/helpers.dart
Adds normalizeDynamicMap / normalizeDynamicValue and uses normalization when parsing SubscriptionInfoIOS.
Tests
/test/subscription_info_parse_test.dart
Adds unit test asserting parsing of an iOS subscription product and SubscriptionInfoIOS fields.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App as Flutter App (Dart)
  participant Dart as flutter_inapp_purchase.dart
  participant MethodCh as MethodChannel
  participant iOS as FlutterInappPurchasePlugin (iOS)
  participant Helper as FlutterIapHelper
  participant Log as FlutterIapLog

  rect rgba(200,230,255,0.25)
  App->>Dart: getAvailablePurchases(options)
  Dart->>Dart: normalize options (PurchaseOptions)
  Dart->>MethodCh: invokeMethod("getAvailableItems", normalized)
  MethodCh->>iOS: getAvailableItems(params)
  iOS->>Helper: decodePurchaseOptions(params)
  Helper-->>iOS: PurchaseOptions
  iOS->>Log: payload("getAvailableItems", params)
  iOS-->>MethodCh: sanitized items
  MethodCh-->>Dart: items
  Dart->>Dart: resolve/aggregate results
  Dart-->>App: purchases list
  end
Loading
sequenceDiagram
  autonumber
  actor App as Flutter App (Dart)
  participant Dart as flutter_inapp_purchase.dart
  participant MethodCh as MethodChannel
  participant iOS as FlutterInappPurchasePlugin (iOS)
  participant Helper as FlutterIapHelper
  participant Log as FlutterIapLog

  rect rgba(220,255,220,0.25)
  App->>Dart: requestPurchase(props)
  Dart->>Dart: _buildIosPurchasePayload(...)
  Dart->>MethodCh: invokeMethod("requestPurchase", payload)
  MethodCh->>iOS: requestPurchase(payload)
  iOS->>Helper: decodeRequestPurchaseProps(payload)
  Helper-->>iOS: RequestPurchaseProps
  iOS->>Log: payload("requestPurchase", normalized)
  iOS-->>MethodCh: result / error (standardized)
  MethodCh-->>Dart: response
  end

  rect rgba(255,230,200,0.25)
  App->>Dart: finishTransaction(purchase, isConsumable)
  Dart->>MethodCh: invokeMethod("finishTransaction", {transactionId,purchase,isConsumable})
  MethodCh->>iOS: finishTransaction(payload)
  iOS->>Log: payload("finishTransaction", payload)
  iOS-->>MethodCh: status
  MethodCh-->>Dart: status
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit hops through JSON bright,
Sanitizes keys by morning light.
Dart tidies maps and iOS logs sing,
Subscriptions sorted, histories ring.
Version hunts, tests pass — carrot dream takes flight. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% 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 "refactor: modernize iOS Plugin and Improve Purchase Flow" accurately and succinctly captures the primary work in this changeset—substantial iOS plugin refactors (new Swift helpers, logging, and plugin updates) alongside cross-cutting purchase-flow improvements in the library and example app as shown in the raw_summary and PR objectives. It is concise, relevant, and follows a conventional commit style.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/refactor-purchase-flow

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.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

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 focuses on modernizing the iOS plugin and refining the in-app purchase experience within the example application. The changes introduce a more structured approach to iOS native code, improve the reliability of data exchange between Flutter and native platforms, and provide a clearer user interface for managing purchases and subscriptions. The primary goal is to create a more stable and debuggable in-app purchase system.

Highlights

  • iOS Plugin Refactoring: The iOS plugin has undergone significant refactoring, introducing dedicated helper and logging utilities to streamline code, improve maintainability, and enhance error handling and debugging capabilities.
  • Improved Purchase Flow Logic: The purchase flow within the example application has been enhanced, particularly for iOS, with better handling of available purchases, purchase history, and subscription states, including loading indicators and error displays.
  • Robust Data Serialization and Deserialization: New utility functions have been added to handle dynamic data types more robustly across the Flutter-native bridge, ensuring consistent and reliable parsing of product and purchase information.
  • Dependency Update: The openiap dependency for iOS has been updated to version 1.2.2, bringing potential improvements or fixes from the underlying in-app purchase library.
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

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.

@codecov
Copy link
Copy Markdown

codecov bot commented Sep 21, 2025

Codecov Report

❌ Patch coverage is 37.93103% with 54 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.24%. Comparing base (e92614e) to head (4caee69).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/flutter_inapp_purchase.dart 28.35% 48 Missing ⚠️
lib/helpers.dart 70.00% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #560      +/-   ##
==========================================
+ Coverage   30.45%   32.24%   +1.79%     
==========================================
  Files           8        8              
  Lines        2243     2267      +24     
==========================================
+ Hits          683      731      +48     
+ Misses       1560     1536      -24     

☔ 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

@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 is a significant and well-executed refactoring of the iOS plugin and example app. It modernizes the Swift code by introducing structured logging, better error handling, and cleaner payload management through helper classes, aligning with the updated OpenIAP library. The changes in the example app, particularly for handling purchase history and active subscriptions, greatly improve the user experience and provide a more robust demonstration. My review focuses on a few key areas to ensure the stability of this large refactor: I've identified a potential data leak in the new logging sanitization logic, a risk of type mismatch from removed data coercion that could affect Dart-side parsing, and a minor configuration issue with a hardcoded device ID. Addressing these points will help solidify this excellent modernization effort.

Comment on lines +35 to +37
if let optionalDictionary = value as? [String: Any?] {
return sanitizeDictionary(optionalDictionary)
}
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.

high

The sanitizeValue function has inconsistent logic for handling dictionaries. A dictionary of type [String: Any] is sanitized recursively, which includes hiding tokens. However, a dictionary of type [String: Any?] is only passed to sanitizeDictionary, which just removes nil values but does not perform recursive sanitization or token hiding. This can lead to sensitive tokens being logged if they are nested inside a dictionary with optional values.

You can refactor this function to handle both [String: Any] and [String: Any?] with a single, recursive logic block by casting to [String: Any?]. Similarly, array handling can be simplified.

Example refactor:

static func sanitizeValue(_ value: Any?) -> Any? {
    guard let value else { return nil }

    if let dictionary = value as? [String: Any?] {
        return dictionary.reduce(into: [String: Any]()) { result, element in
            let (key, rawValue) = element
            if key.lowercased().contains("token") {
                result[key] = "hidden"
            } else if let sanitized = sanitizeValue(rawValue) {
                result[key] = sanitized
            }
        }
    }

    if let array = value as? [Any?] {
        return array.compactMap { sanitizeValue($0) }
    }

    return value
}

return dict
}
await MainActor.run { result(sanitized) }
let serialized = FlutterIapHelper.sanitizeArray(OpenIapSerialization.purchases(purchases))
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.

high

The previous implementation of getAvailableItems included logic to coerce NSNumber fields (like transactionId, webOrderLineItemIdIOS, etc.) to String before sending them to the Dart side. This refactoring has removed that coercion logic. If the underlying OpenIapModule still returns these identifiers as NSNumber, this change could cause parsing errors or crashes on the Dart side, which may expect these fields to be strings. Please verify that OpenIapSerialization.purchases now correctly returns all identifier fields as strings, or consider re-introducing the type coercion to prevent potential runtime issues.

"request": "launch",
"program": "lib/main.dart",
"cwd": "${workspaceFolder}/example",
"deviceId": "00008101-0006455036D8001E",
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.

medium

A hardcoded deviceId has been added to this shared launch configuration. This value is specific to a developer's machine and will likely cause this configuration to fail for other contributors. It's best to avoid committing machine-specific settings. Please consider removing this line or using an environment variable (e.g., ${env:IOS_DEVICE_ID}) to make it configurable for everyone.

}

_subscriptions = products;
_subscriptions = List<ProductCommon>.from(products, growable: false);
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.

medium

While creating a new list from products is safe, you can avoid creating an intermediate list by using List.of with growable: false for a more direct and slightly more efficient approach, especially if products is already a list.

Suggested change
_subscriptions = List<ProductCommon>.from(products, growable: false);
_subscriptions = List.of(products, growable: false);

@hyochan hyochan changed the title Refactor: Modernize iOS Plugin and Improve Purchase Flow refactor: modernize iOS Plugin and Improve Purchase Flow Sep 21, 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: 2

Caution

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

⚠️ Outside diff range comments (6)
example/lib/src/screens/purchase_flow_screen.dart (2)

90-91: Avoid logging full purchase tokens/objects (PII).

Mask or truncate tokens and avoid dumping full objects to logs.

Apply this diff:

-        debugPrint('PurchaseToken: ${purchase.purchaseToken}');
-        debugPrint('Full purchase data: $purchase');
+        final masked = _mask(purchase.purchaseToken);
+        debugPrint('PurchaseToken: $masked');
+        debugPrint('Full purchase data: <redacted>');

Add this helper (anywhere in the State class):

String _mask(String? v, {int showLast = 4}) {
  if (v == null || v.isEmpty) return 'N/A';
  final keep = v.length <= showLast ? v.length : showLast;
  return '*' * (v.length - keep) + v.substring(v.length - keep);
}

210-214: Mask tokens in success logs as well.

Same concern on success path.

Apply this diff:

-    debugPrint('Purchase token: ${purchase.purchaseToken}');
+    debugPrint('Purchase token: ${_mask(purchase.purchaseToken)}');
lib/helpers.dart (1)

717-746: Bug: defaulting unknown Android purchase state to Purchased.

This can grant entitlements erroneously. Default to Unknown instead.

Apply this diff:

 int _coerceAndroidPurchaseState(dynamic value) {
   if (value == null) {
-    return AndroidPurchaseState.Purchased.value;
+    return AndroidPurchaseState.Unknown.value;
   }
@@
   if (value is String) {
@@
   }
-  return AndroidPurchaseState.Purchased.value;
+  return AndroidPurchaseState.Unknown.value;
 }
lib/flutter_inapp_purchase.dart (1)

1372-1411: Heuristic for iOS subscription detection is too weak.

receipt != null || productId.contains('sub') can misclassify. Use iOS fields instead and check expiry.

Apply this diff:

-            } else if (purchase is gentype.PurchaseIOS) {
-              final receipt = purchase.purchaseToken;
-              final bool isSubscription =
-                  receipt != null || purchase.productId.contains('sub');
-              final bool isActive =
-                  (purchase.purchaseState == gentype.PurchaseState.Purchased ||
-                          purchase.purchaseState ==
-                              gentype.PurchaseState.Restored) &&
-                      isSubscription;
+            } else if (purchase is gentype.PurchaseIOS) {
+              final nowMs = DateTime.now().millisecondsSinceEpoch.toDouble();
+              final bool isSubscription =
+                  (purchase.subscriptionGroupIdIOS?.isNotEmpty ?? false) ||
+                  (purchase.originalTransactionIdentifierIOS?.isNotEmpty ??
+                      false);
+              final bool notExpired =
+                  purchase.expirationDateIOS == null ||
+                  (purchase.expirationDateIOS! > 0 &&
+                      purchase.expirationDateIOS! > nowMs);
+              final bool isActive =
+                  isSubscription &&
+                  (purchase.purchaseState == gentype.PurchaseState.Purchased ||
+                      purchase.purchaseState ==
+                          gentype.PurchaseState.Restored) &&
+                  notExpired;
example/lib/src/screens/subscription_flow_screen.dart (2)

1082-1086: Potential RangeError: substring(0, 10) without length guard

If the token is shorter than 10 chars, this throws. Use a safe preview helper.

-                  'Token: ${_currentSubscription!.purchaseToken?.substring(0, 10)}...',
+                  'Token: ${_safeTokenPreview(_currentSubscription!.purchaseToken, 10)}',

And add:

+  String _safeTokenPreview(String? token, int maxLen) {
+    if (token == null) return 'null';
+    return token.length <= maxLen ? token : '${token.substring(0, maxLen)}...';
+  }

620-624: Same substring issue here

Guard length when previewing testToken.

-      debugPrint('Using test token: ${testToken.substring(0, 20)}...');
+      debugPrint('Using test token: ${_safeTokenPreview(testToken, 20)}');
🧹 Nitpick comments (18)
.vscode/launch.json (2)

88-96: Avoid committing a machine‑specific iOS deviceId (UDID).

Hard-coding "deviceId": "00008101-0006455036D8001E" will fail for other contributors and causes churn when devices change. Make this config portable by removing the deviceId and letting Flutter/Dart select the active iOS device/simulator at launch.

Apply this diff:

-    },
-    {
-      "name": "Example (iOS Device)",
+    },
+    {
+      "name": "Flutter: Run Example App (iOS)",
       "type": "dart",
       "request": "launch",
       "program": "lib/main.dart",
       "cwd": "${workspaceFolder}/example",
-      "deviceId": "00008101-0006455036D8001E",
       "args": []
     }

Optional: if you want a pinned-device config for personal use, keep it in an ignored file (e.g., .vscode/launch.local.json) and add it to .gitignore.


88-96: Naming consistency with existing Flutter entries.

Rename “Example (iOS Device)” to follow the existing “Flutter: Run Example App (…)” pattern for quick palette filtering.

android/build.gradle (2)

3-15: Helper is solid; consider dropping static for Gradle/Groovy script safety

Top‑level static methods in Groovy scripts can be finicky across Gradle versions. A plain script method is sufficient and avoids edge compatibility issues.

Apply this small tweak:

-static File locateOpeniapVersionsFile(File startDir) {
+File locateOpeniapVersionsFile(File startDir) {
     File current = startDir
     while (current != null) {
         File candidate = new File(current, 'openiap-versions.json')
         if (candidate.exists()) {
             return candidate
         }
         current = current.getParentFile()
     }
     throw new GradleException(
         "Unable to locate openiap-versions.json starting from ${startDir.absolutePath}"
     )
 }

Please run a local Gradle sync/build to confirm no configuration‑time warnings/regressions after this change.


17-17: Start search from projectDir for broader coverage

Starting at rootDir skips the module’s own directory; beginning at projectDir finds files placed beside this module and still walks up to the repo root or beyond.

-def openiapVersionsFile = locateOpeniapVersionsFile(rootDir)
+def openiapVersionsFile = locateOpeniapVersionsFile(projectDir)

sanity check: ensure the intended openiap-versions.json is still resolved in your CI and local dev layouts (monorepo, nested includes).

test/subscription_info_parse_test.dart (1)

31-45: Nice focused test; consider asserting product type too.

Add a quick check to guard against accidental type regressions.

Apply this diff:

     final subscription = product as types.ProductSubscriptionIOS;
+    expect(subscription.type, types.ProductType.Subs);
lib/helpers.dart (1)

478-488: Guard against non-list discounts payloads.

Casting directly to List can throw when servers send objects.

Apply this diff:

gentype.DiscountIOS? _asDiscount(dynamic e) {
  if (e is Map<String, dynamic>) return gentype.DiscountIOS.fromJson(e);
  if (e is Map) return gentype.DiscountIOS.fromJson(Map<String, dynamic>.from(e));
  return null;
}
 
 List<gentype.DiscountIOS>? _parseDiscountsIOS(dynamic json) {
-  if (json == null) return null;
-  final list = json as List<dynamic>;
-  return list
-      .map(
-        (e) => gentype.DiscountIOS.fromJson(
-          e is Map<String, dynamic> ? e : Map<String, dynamic>.from(e as Map),
-        ),
-      )
-      .toList();
+  if (json == null) return null;
+  if (json is! List) return null;
+  return (json as List)
+      .map<gentype.DiscountIOS?>(_asDiscount)
+      .whereType<gentype.DiscountIOS>()
+      .toList(growable: false);
 }
example/lib/src/screens/available_purchases_screen.dart (2)

200-204: Deduplicate history list too (optional).

Keeps UI consistent with active list behavior.

Apply this diff:

-      setState(() {
-        _purchaseHistory = purchaseHistory;
+      setState(() {
+        _purchaseHistory = _deduplicatePurchases(purchaseHistory);
         _historyError = null;
       });

136-147: Mounted early-return is fine; minor: ensure loading reset in non-mounted cases.

Not harmful since widget is gone, but can trap spinners in tests. Optional to set local flags before return.

lib/flutter_inapp_purchase.dart (1)

1011-1070: Acknowledge response parsing is robust; consider extraction.

Minor: extract didAcknowledgeSucceed to utils for reuse/testing.

ios/Classes/FlutterIapLog.swift (2)

22-35: Make access to handler/isEnabled thread-safe

These statics can be mutated/read from different threads. Consider a lightweight synchronization (e.g., serial queue) to avoid data races.

Also applies to: 49-55


75-93: stringify: consider Encodable support for richer payloads

If sanitized values conform to Encodable, encode via JSONEncoder before falling back to String(describing:).

ios/Classes/FlutterInappPurchasePlugin.swift (1)

507-521: Unify error messaging with the rest of the file

Here you surface error.localizedDescription directly; elsewhere you use defaultMessage(for:). Align for consistency.

-                    result(FlutterError(code: code.rawValue, message: error.localizedDescription, details: nil))
+                    result(FlutterError(code: code.rawValue, message: defaultMessage(for: code), details: error.localizedDescription))
example/lib/src/screens/subscription_flow_screen.dart (3)

330-369: Active subscription resolution is thoughtful; keep time unit consistent

You compare dates with a 2s tolerance assuming milliseconds. If either side is seconds, matching may fail. Consider normalizing to ms or guarding by magnitude.

-      const double matchToleranceMs = 2000; // 2 seconds tolerance
+      const double matchToleranceMs = 2000; // ms
+      num norm(num t) => t > 1e12 ? t : t * 1000; // seconds -> ms
...
-          final matchesDate =
-              (purchase.transactionDate - summary.transactionDate).abs() <=
-                  matchToleranceMs;
+          final matchesDate =
+              (norm(purchase.transactionDate) - norm(summary.transactionDate))
+                  .abs() <= matchToleranceMs;

Also applies to: 372-420, 423-461


1254-1262: UI shows full token in multiple places; mask it in production

Even in examples, prefer token previews. You already added a helper—reuse it here when printing/showing tokens.

Also applies to: 1265-1324


835-841: Date formatting: handle seconds vs ms

If an iOS field arrives in seconds, this will render 1970 dates. Normalize as in the earlier suggestion.

-  final date = DateTime.fromMillisecondsSinceEpoch(timestamp.round());
+  final millis = timestamp >= 1e12 ? timestamp : timestamp * 1000;
+  final date = DateTime.fromMillisecondsSinceEpoch(millis.round());
ios/Classes/FlutterIapHelper.swift (3)

147-160: Set a realistic transactionDate in fallback purchase input

0 can break ordering and matching heuristics. Use current time in ms.

-            "transactionDate": 0
+            "transactionDate": Int(Date().timeIntervalSince1970 * 1000)

50-55: jsonString helper: optionally pretty‑print in DEBUG

Minor: in DEBUG, consider .prettyPrinted for easier dev inspection.


58-72: parseProductQueryType: accept common aliases

Optional: accept "inapp", "in-app", "subs", "subscriptions" for resilience.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e92614e and 34a40f1.

⛔ Files ignored due to path filters (1)
  • example/ios/Podfile.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • .vscode/launch.json (1 hunks)
  • android/build.gradle (1 hunks)
  • example/lib/src/screens/available_purchases_screen.dart (5 hunks)
  • example/lib/src/screens/purchase_flow_screen.dart (1 hunks)
  • example/lib/src/screens/subscription_flow_screen.dart (7 hunks)
  • ios/Classes/FlutterIapHelper.swift (1 hunks)
  • ios/Classes/FlutterIapLog.swift (1 hunks)
  • ios/Classes/FlutterInappPurchasePlugin.swift (19 hunks)
  • lib/flutter_inapp_purchase.dart (8 hunks)
  • lib/helpers.dart (2 hunks)
  • test/subscription_info_parse_test.dart (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.dart: Use OpenIAP ProductRequest with iap.fetchProducts and handle the FetchProductsResult union
Use helper extensions from flutter_inapp_purchase.dart (inAppProducts(), subscriptionProducts(), allProducts()) to unwrap FetchProductsResult safely
Use getAvailablePurchases with PurchaseOptions for OpenIAP compliance; prefer defaults unless specific iOS options are needed
Do not use getPurchaseHistories (deprecated); replace with getAvailablePurchases and options as needed
When calling generic functions (e.g., showModalBottomSheet), specify explicit type arguments (e.g., showModalBottomSheet)
iOS-related types/functions use IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
When iOS is not the final suffix, use Ios (e.g., IosManager, IosHelper)
For field names with iOS in the middle, place Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)
Android-related types/functions use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)
Use Id (not ID) in identifiers (e.g., productId, transactionId, offerId); when combined with platform suffixes, use Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)
Exception: iOS-only fields that end with ID use ID (e.g., transactionID, webOrderLineItemID)
Functions that depend on event results must use the request prefix (e.g., requestPurchase, requestPurchaseWithBuilder)
Follow OpenIAP terminology for API method naming; avoid generic prefixes like get or find

Files:

  • test/subscription_info_parse_test.dart
  • example/lib/src/screens/subscription_flow_screen.dart
  • example/lib/src/screens/available_purchases_screen.dart
  • example/lib/src/screens/purchase_flow_screen.dart
  • lib/helpers.dart
  • 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.
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : iOS-related types/functions use IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions for OpenIAP compliance; prefer defaults unless specific iOS options are needed
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Android-related types/functions use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Use OpenIAP ProductRequest with iap.fetchProducts and handle the FetchProductsResult union
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Use helper extensions from flutter_inapp_purchase.dart (inAppProducts(), subscriptionProducts(), allProducts()) to unwrap FetchProductsResult safely
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : For field names with iOS in the middle, place Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)
📚 Learning: 2025-09-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : iOS-related types/functions use IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)

Applied to files:

  • test/subscription_info_parse_test.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
  • lib/helpers.dart
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Use helper extensions from flutter_inapp_purchase.dart (inAppProducts(), subscriptionProducts(), allProducts()) to unwrap FetchProductsResult safely

Applied to files:

  • test/subscription_info_parse_test.dart
  • example/lib/src/screens/subscription_flow_screen.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
  • example/lib/src/screens/purchase_flow_screen.dart
  • ios/Classes/FlutterIapHelper.swift
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Use OpenIAP ProductRequest with iap.fetchProducts and handle the FetchProductsResult union

Applied to files:

  • test/subscription_info_parse_test.dart
  • example/lib/src/screens/subscription_flow_screen.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
  • example/lib/src/screens/purchase_flow_screen.dart
  • ios/Classes/FlutterIapHelper.swift
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions for OpenIAP compliance; prefer defaults unless specific iOS options are needed

Applied to files:

  • test/subscription_info_parse_test.dart
  • ios/Classes/FlutterInappPurchasePlugin.swift
  • example/lib/src/screens/available_purchases_screen.dart
  • example/lib/src/screens/purchase_flow_screen.dart
  • ios/Classes/FlutterIapHelper.swift
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to lib/types.dart : Do not edit lib/types.dart (generated from OpenIAP schema)

Applied to files:

  • test/subscription_info_parse_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:

  • ios/Classes/FlutterInappPurchasePlugin.swift
  • example/lib/src/screens/available_purchases_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 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:

  • 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
📚 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:

  • ios/Classes/FlutterInappPurchasePlugin.swift
📚 Learning: 2025-09-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Do not use getPurchaseHistories (deprecated); replace with getAvailablePurchases and options as needed

Applied to files:

  • example/lib/src/screens/available_purchases_screen.dart
  • example/lib/src/screens/purchase_flow_screen.dart
  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Android-related types/functions use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)

Applied to files:

  • lib/flutter_inapp_purchase.dart
📚 Learning: 2025-09-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
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-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Exception: iOS-only fields that end with ID use ID (e.g., transactionID, webOrderLineItemID)

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:

  • lib/flutter_inapp_purchase.dart
🧬 Code graph analysis (3)
ios/Classes/FlutterInappPurchasePlugin.swift (2)
ios/Classes/FlutterIapLog.swift (6)
  • debug (32-32)
  • result (41-43)
  • payload (37-39)
  • error (35-35)
  • failure (45-47)
  • info (33-33)
ios/Classes/FlutterIapHelper.swift (10)
  • sanitizeDictionary (7-15)
  • jsonString (50-54)
  • decodeProductRequest (74-90)
  • sanitizeArray (17-19)
  • decodePurchaseOptions (92-94)
  • decodePurchaseOptions (96-101)
  • decodeRequestPurchaseProps (103-141)
  • decodePurchaseInput (143-145)
  • fallbackPurchaseInput (147-160)
  • decodeReceiptValidationProps (162-164)
ios/Classes/FlutterIapHelper.swift (1)
ios/Classes/FlutterIapLog.swift (2)
  • result (41-43)
  • payload (37-39)
ios/Classes/FlutterIapLog.swift (1)
ios/Classes/FlutterIapHelper.swift (1)
  • sanitizeValue (21-48)
🔇 Additional comments (19)
example/lib/src/screens/purchase_flow_screen.dart (1)

366-366: Good: switched to result.inAppProducts().

This aligns with the helper extension guidance and simplifies unwrapping.

lib/helpers.dart (2)

593-606: Solid: robust normalization for subscriptionInfoIOS.

This reduces parse flakiness from Map<Object?,Object?> inputs.


618-650: Good utility: normalizeDynamicMap/Value.

Works recursively and preserves string keys.

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

538-590: Nice iOS-only History section and states.

Clear UX with load/reload, progress, and error messaging.


169-222: No action needed — SDK supports unawaited.
pubspec.yaml requires Dart SDK ">=3.0.0 <4.0.0", so using unawaited from dart:async is safe.

lib/flutter_inapp_purchase.dart (6)

239-256: Good: centralize iOS payload construction.

Validates required fields and reduces branching.


411-471: Unified getAvailablePurchases flow with normalized options is solid.

Filters by resolvable identifiers and avoids deprecated histories.


1078-1083: Good: include structured payload when finishing iOS transactions.

Sends transactionId, purchase JSON, and consumable flag.


1259-1291: fetchProducts: correct use of normalizeDynamicMap with defensive logging.

Prevents Map<Object?,Object?> pitfalls.


1000-1010: Confirmed: Android plugin exposes both handlers (acknowledgePurchase is a deprecated alias of acknowledgePurchaseAndroid).
Handlers located in android/src/main/kotlin/dev/hyo/flutterinapppurchase/AndroidInappPurchasePlugin.kt — acknowledgePurchaseAndroid (lines ~529–544); acknowledgePurchase (lines ~734–750) logs deprecation and forwards to acknowledgePurchaseAndroid.


479-529: iOS already preserves and forwards these purchase keys — no Swift change required.

decodeRequestPurchaseProps copies the incoming args into iosPayload, uses sanitizeDictionary (keeps non‑nil entries), nests that under "requestPurchase"/"requestSubscription" and calls OpenIapSerialization.decode; andDangerouslyFinishTransactionAutomatically, withOffer, appAccountToken and quantity are preserved and passed into OpenIAP.
Locations: ios/Classes/FlutterIapHelper.swift (decodeRequestPurchaseProps), ios/Classes/FlutterInappPurchasePlugin.swift (requestPurchase).

ios/Classes/FlutterIapLog.swift (1)

14-21: LGTM: sensible defaults and override points

Debug‑only default enablement and a pluggable handler look good.

Also applies to: 24-35

ios/Classes/FlutterInappPurchasePlugin.swift (4)

260-283: fetchProducts flow is clean and OpenIAP‑compliant

Decoding via FlutterIapHelper.decodeProductRequest, handling PurchaseError, and returning sanitized arrays looks solid.


363-384: finishTransaction fallback is robust

Graceful fallback to a transactionId‑based PurchaseInput covers edge cases. Good logging and error surfacing.

Also applies to: 386-399


200-237: Confirm Dart-side handlers match iOS channel names

iOS emits "purchase-updated", "purchase-error", and "iap-promoted-product" (ios/Classes/FlutterInappPurchasePlugin.swift). Example code uses purchaseUpdatedListener / purchaseErrorListener, but I couldn't find literal handlers for those method-channel event names in lib/ — verify the Dart plugin maps incoming method calls with those exact names to the listeners, or align the names on one side.


49-53: canMakePayments must not return true unconditionally — delegate to OpenIAP or fall back to StoreKit.

Update ios/Classes/FlutterInappPurchasePlugin.swift (case "canMakePayments") to call OpenIapModule.shared.canMakePayments() if that API is available; otherwise return SKPaymentQueue.canMakePayments() gated under #if canImport(StoreKit).

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

295-304: Good: OpenIAP fetch with union unwrapping

Using ProductRequest + result.subscriptionProducts() aligns with guidelines.

Also applies to: 317-318


685-717: Type argument specified — nice

Using showModalBottomSheet<void> matches the guideline for generic calls.

ios/Classes/FlutterIapHelper.swift (1)

74-90: Decoders look solid and aligned with OpenIAP

Handling skus, indexed keys, and normalization to requestPurchase/requestSubscription is clean.

Also applies to: 103-141

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 (2)
example/lib/src/screens/subscription_flow_screen.dart (2)

1082-1086: Fix RangeError risk from substring on purchaseToken

substring assumes length >= N and will throw when shorter. Use a safe preview helper instead.

Apply this diff:

-                label: Text(
-                  'Token: ${_currentSubscription!.purchaseToken?.substring(0, 10)}...',
-                  style: const TextStyle(fontSize: 10),
-                ),
+                label: Text(
+                  'Token: ${_previewToken(_currentSubscription!.purchaseToken, max: 10)}',
+                  style: const TextStyle(fontSize: 10),
+                ),
-      debugPrint('Using test token: ${testToken.substring(0, 20)}...');
+      debugPrint('Using test token: ${_previewToken(testToken, max: 20)}');

Add this helper in the State class (e.g., below _formatReadableDate):

String _previewToken(String? token, {int max = 20}) {
  if (token == null || token.isEmpty) return 'null';
  final end = token.length < max ? token.length : max;
  final preview = token.substring(0, end);
  return token.length > end ? '$preview...' : preview;
}

Also applies to: 623-623


101-103: Redact purchase tokens in logs/UI

Avoid printing full tokens; they’re sensitive. Use the same preview helper consistently.

Apply this diff:

-        debugPrint('  Purchase token: ${purchase.purchaseToken}');
+        debugPrint('  Purchase token: ${_previewToken(purchase.purchaseToken, max: 20)}');
-          debugPrint(
-            '  Purchase token: ${_currentSubscription!.purchaseToken}',
-          );
+          debugPrint('  Purchase token: ${_previewToken(_currentSubscription!.purchaseToken, max: 20)}');
-        debugPrint(
-          'Restored: ${purchase.productId}, Token: ${purchase.purchaseToken}',
-        );
+        debugPrint('Restored: ${purchase.productId}, Token: ${_previewToken(purchase.purchaseToken, max: 20)}');

Also applies to: 439-441, 672-674

🧹 Nitpick comments (2)
example/lib/src/screens/subscription_flow_screen.dart (1)

618-619: Fix misleading “Empty Token” text; code sends a non-empty token

Update copy to reflect behavior (non-empty test token). Keeps developer UX clear.

Apply this diff:

-      debugPrint('Testing proration mode with EMPTY string purchaseToken');
+      debugPrint('Testing proration mode with a non-empty test token');

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

Also applies to: 1046-1049

ios/Classes/FlutterIapHelper.swift (1)

51-74: Optional: tighten/trim sanitizer paths

You can streamline sanitizeValue by casting dictionaries once to [String: Any?] and handling arrays via a single branch; behavior stays identical, code shrinks.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34a40f1 and 4caee69.

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

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.dart: Use OpenIAP ProductRequest with iap.fetchProducts and handle the FetchProductsResult union
Use helper extensions from flutter_inapp_purchase.dart (inAppProducts(), subscriptionProducts(), allProducts()) to unwrap FetchProductsResult safely
Use getAvailablePurchases with PurchaseOptions for OpenIAP compliance; prefer defaults unless specific iOS options are needed
Do not use getPurchaseHistories (deprecated); replace with getAvailablePurchases and options as needed
When calling generic functions (e.g., showModalBottomSheet), specify explicit type arguments (e.g., showModalBottomSheet)
iOS-related types/functions use IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
When iOS is not the final suffix, use Ios (e.g., IosManager, IosHelper)
For field names with iOS in the middle, place Id before IOS (e.g., subscriptionGroupIdIOS, webOrderLineItemIdIOS)
Android-related types/functions use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
When IAP is not the final suffix, use Iap (e.g., IapPurchase, not IAPPurchase)
Use Id (not ID) in identifiers (e.g., productId, transactionId, offerId); when combined with platform suffixes, use Id before the suffix (e.g., subscriptionGroupIdIOS, obfuscatedAccountIdAndroid)
Exception: iOS-only fields that end with ID use ID (e.g., transactionID, webOrderLineItemID)
Functions that depend on event results must use the request prefix (e.g., requestPurchase, requestPurchaseWithBuilder)
Follow OpenIAP terminology for API method naming; avoid generic prefixes like get or find

Files:

  • example/lib/src/screens/subscription_flow_screen.dart
🧠 Learnings (4)
📓 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-20T07:27:44.884Z
Learning: Applies to **/*.dart : iOS-related types/functions use IOS suffix (e.g., PurchaseIOS, SubscriptionOfferIOS)
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions for OpenIAP compliance; prefer defaults unless specific iOS options are needed
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Android-related types/functions use Android suffix (e.g., PurchaseAndroid, SubscriptionOfferAndroid)
📚 Learning: 2025-09-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Use helper extensions from flutter_inapp_purchase.dart (inAppProducts(), subscriptionProducts(), allProducts()) to unwrap FetchProductsResult safely

Applied to files:

  • example/lib/src/screens/subscription_flow_screen.dart
  • ios/Classes/FlutterIapHelper.swift
📚 Learning: 2025-09-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Use OpenIAP ProductRequest with iap.fetchProducts and handle the FetchProductsResult union

Applied to files:

  • example/lib/src/screens/subscription_flow_screen.dart
  • ios/Classes/FlutterIapHelper.swift
📚 Learning: 2025-09-20T07:27:44.884Z
Learnt from: CR
PR: hyochan/flutter_inapp_purchase#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T07:27:44.884Z
Learning: Applies to **/*.dart : Use getAvailablePurchases with PurchaseOptions for OpenIAP compliance; prefer defaults unless specific iOS options are needed

Applied to files:

  • ios/Classes/FlutterIapHelper.swift
🧬 Code graph analysis (2)
ios/Classes/FlutterIapHelper.swift (1)
ios/Classes/FlutterIapLog.swift (2)
  • result (41-43)
  • payload (37-39)
ios/Classes/FlutterIapLog.swift (2)
ios/Classes/FlutterInappPurchasePlugin.swift (2)
  • iOS (404-419)
  • iOS (421-436)
ios/Classes/FlutterIapHelper.swift (1)
  • sanitizeValue (51-74)
🔇 Additional comments (7)
example/lib/src/screens/subscription_flow_screen.dart (3)

303-303: Good: using OpenIAP FetchProductsResult helpers, PurchaseOptions, and typed modal

  • result.subscriptionProducts() used correctly to unwrap the union.
  • getAvailablePurchases with PurchaseOptions aligns with guidance.
  • showModalBottomSheet uses explicit type arg (nice).
  • List.of(..., growable: false) avoids an extra copy later.

Also applies to: 344-347, 685-716, 317-318


200-207: Consider unblocking UI on Pending state

Currently only _purchaseResult is set; _isProcessing remains true, which may freeze actions if the state stays Pending. Confirm intended UX.


835-841: Timestamps are milliseconds — no normalization required

Per the flutter_inapp_purchase changelog and API, Purchase.transactionDate and ActiveSubscription.transactionDate/expirationDateIOS are milliseconds since the Unix epoch; DateTime.fromMillisecondsSinceEpoch is correct — drop the proposed sec→ms diff and keep matchTolerance as-is. Applies to example/lib/src/screens/subscription_flow_screen.dart (lines 835–841 and 389–392).

ios/Classes/FlutterIapHelper.swift (2)

20-25: Sanitization looks solid and comprehensive

Covers tokens/receipts/JWS/signature, recurses through nested maps/arrays, and stringifies identifier numerics—good balance of safety and utility.

Also applies to: 27-45, 51-74


118-127: Nice normalization for request decoding

The normalized shapes before delegating to OpenIapSerialization make the API tolerant to multiple payload forms and improve log traceability with sanitized payloads.

Also applies to: 129-167, 173-186

ios/Classes/FlutterIapLog.swift (2)

57-76: Runtime-guarded os.Logger usage — LGTM

Availability check prevents issues on < iOS 14; fallback to NSLog is correct.


79-97: Good integration with sanitizer for payload/result logging

stringify() leverages FlutterIapHelper.sanitizeValue before serialization—prevents leaking sensitive fields.

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