Skip to content

Stripe ghost subscription fix - #8941

Merged
jigar-f merged 10 commits into
mainfrom
jigar/stripe-ghost-sub-fix
Aug 6, 2026
Merged

Stripe ghost subscription fix#8941
jigar-f merged 10 commits into
mainfrom
jigar/stripe-ghost-sub-fix

Conversation

@jigar-f

@jigar-f jigar-f commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

This pull request refactors and improves the Stripe payment flow, particularly for Android, by switching to a deferred-intent approach. It ensures that the Stripe publishable key is dynamically sourced from the backend, centralizes environment selection logic, and improves error handling and user messaging for Stripe failures. Additionally, it cleans up and modernizes how provider data is modeled and accessed.

Stripe Payment Flow Improvements:

  • Replaces upfront Stripe SDK initialization with a deferred-intent flow, where the payment sheet is presented immediately and the backend subscription is only created after the user taps Pay. This prevents abandoned subscriptions and ensures the Stripe publishable key matches the backend environment. (lib/core/services/stripe_service.dart, lib/features/auth/choose_payment_method.dart) [1] [2]
  • Refactors the payment flow to dynamically update the Stripe publishable key from the backend's plans response, removing reliance on hardcoded or environment-based keys. (lib/core/services/stripe_service.dart, lib/core/models/plan_data.dart, lib/core/services/injection_container.dart) [1] [2] [3]

Provider and Plan Modeling:

  • Refactors the provider data model to a strongly-typed ProviderData class, making it easier and safer to access provider-specific fields like Stripe's pubKey. (lib/core/models/plan_data.dart)
  • Adds convenience methods to PlansData for retrieving the publishable key for the current platform and for sorting providers by subscription support. (lib/core/models/plan_data.dart)

Error Handling and User Messaging:

  • Improves error handling for Stripe failures by filtering out developer-facing error messages and only showing safe, user-facing messages in the UI. (lib/core/extensions/error.dart, lib/core/services/stripe_service.dart) [1] [2]
  • Adds a method to the plan model for retrieving the expected monthly price in USD cents, ensuring correct amounts are quoted to Stripe. (lib/core/extensions/plan.dart)

Environment Handling:

  • Centralizes and standardizes the environment selection logic with a new IsStaging method on Opts, ensuring consistent environment checks across the codebase. (lantern-core/utils/common.go, lantern-core/core.go, lantern-core/mobile/mobile.go) [1] [2] [3]

Other Cleanups:

  • Removes unused Stripe key accessors from AppSecrets and updates imports accordingly. (lib/core/common/app_secrets.dart)
  • Updates dependency injection and service initialization to match the new Stripe flow. (lib/core/services/injection_container.dart)

Summary by CodeRabbit

  • New Features
    • Added platform-specific payment provider support and dynamic Stripe configuration.
    • Stripe subscriptions are now created only after tapping Pay.
    • Added clearer, user-facing Stripe error messages.
    • Added a visible “STAGING” banner when the app runs in staging mode.
  • Bug Fixes
    • Improved handling of canceled, retried, or failed Stripe payment flows.
    • Improved compatibility with staging environment settings and payment provider data.

@jigar-f jigar-f self-assigned this Jul 31, 2026
Copilot AI lite review requested due to automatic review settings July 31, 2026 11:55
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change centralizes staging detection, supplies Stripe configuration from plan data, separates service initialization, and defers subscription creation until payment confirmation. It adds payment and setup intent handling, filtered Stripe errors, renewal selection, and Stripe flow tests.

Changes

Staging runtime configuration

Layer / File(s) Summary
Centralized staging detection
lib/core/models/app_setting.dart, lib/features/developer/developer_mode.dart, lib/lantern_app.dart
AppSetting.isStaging recognizes current and legacy staging values. Developer mode and routed application rendering use this getter. Staging displays a red STAGING banner.

Deferred Stripe payment flow

Layer / File(s) Summary
Plan provider and Stripe key contract
lib/core/models/plan_data.dart
PlansData selects platform providers and exposes the Stripe publishable key. ProviderData preserves provider payloads and provides typed Stripe key access.
Dynamic Stripe service registration
lib/core/services/injection_container.dart, lib/features/plans/provider/plans_notifier.dart
Android registers StripeService without upfront initialization. Plans state updates its publishable key. Notification initialization runs independently.
Deferred payment-sheet confirmation
lib/core/services/stripe_service.dart, lib/features/auth/choose_payment_method.dart
The payment sheet receives the amount, email, and intent mode. Subscription creation runs when the user taps Pay. The returned intent secret is then confirmed.
Stripe error handling and validation
lib/core/services/stripe_service.dart, lib/features/auth/choose_payment_method.dart, test/core/services/stripe_service_test.dart
Cancellation handling, filtered user-facing messages, retry behavior, intent selection, and deferred subscription creation are covered by tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChoosePaymentMethod
  participant StripeService
  participant StripePaymentSheet
  participant SubscriptionBackend
  ChoosePaymentMethod->>StripeService: startStripeSDK(amount, email, intentMode)
  StripeService->>StripePaymentSheet: present payment sheet
  StripePaymentSheet->>StripeService: request deferred subscription
  StripeService->>ChoosePaymentMethod: invoke onCreateSubscription
  ChoosePaymentMethod->>SubscriptionBackend: create subscription
  SubscriptionBackend-->>StripeService: return intent secret
  StripeService->>StripePaymentSheet: confirm selected intent
Loading

Possibly related PRs

  • getlantern/lantern#8933: The payment-method flow overlaps with this PR, although the underlying logic changes differ.

Suggested reviewers: copilot, atavism

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: preventing unintended Stripe subscriptions through a payment-flow fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jigar/stripe-ghost-sub-fix

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.

Copilot AI left a comment

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.

🟡 Not ready to approve

The updated Stripe flow is missing an applySettings() propagation step and has inconsistent Stripe error filtering that can surface developer-facing messages to users.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This pull request refactors the Stripe subscription flow to a deferred-intent approach (to avoid creating abandoned “ghost” subscriptions), sources the Stripe publishable key dynamically from the backend plans response, and centralizes staging-environment detection in lantern-core.

Changes:

  • Moves Android Stripe subscriptions to a deferred-intent PaymentSheet flow where the backend subscription is created only after the user taps Pay.
  • Refactors plans/provider modeling to expose a typed Stripe publishable key (PlansData.stripePubKey) and keeps the Stripe SDK key in sync when plans change.
  • Centralizes environment selection via Opts.IsStaging() and ensures staging is set early enough on Android IPC startup.
File summaries
File Description
lib/features/plans/provider/plans_notifier.dart Adds a state listener to sync Stripe publishable key from fetched/cached plans.
lib/features/auth/choose_payment_method.dart Switches Android Stripe flow to deferred-intent callbacks; improves Stripe error handling in UI.
lib/core/services/stripe_service.dart Reworks Stripe service to deferred-intent PaymentSheet initialization/confirmation and adds user-facing error filtering.
lib/core/services/injection_container.dart Registers StripeService on Android without upfront initialization; simplifies notification init path.
lib/core/models/plan_data.dart Adds platform provider accessors and typed provider data (incl. Stripe pubKey).
lib/core/extensions/plan.dart Adds monthlyUsdCents helper for Stripe amount quoting.
lib/core/extensions/error.dart Adds StripeException-specific localized description filtering to avoid developer-facing messages.
lib/core/common/app_secrets.dart Removes Stripe publishable key accessors from env-based secrets.
lantern-core/utils/common.go Adds Opts.IsStaging() helper to centralize staging environment checks.
lantern-core/mobile/mobile.go Ensures staging env is set before backend construction on Android IPC startup.
lantern-core/core.go Replaces ad-hoc staging checks with opts.IsStaging().
Review details
  • Files reviewed: 11/11 changed files
  • Comments generated: 4
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread lib/core/services/stripe_service.dart
Comment thread lib/core/services/stripe_service.dart
Comment thread lib/features/plans/provider/plans_notifier.dart
Comment thread lib/core/services/injection_container.dart Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
lantern-core/utils/common.go (1)

18-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression tests for Opts.IsStaging.

Opts.IsStaging() controls stage-environment selection, but the test files do not cover "stage", "staging", or a non-staging value. Add focused cases for those inputs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lantern-core/utils/common.go` around lines 18 - 23, Add focused regression
tests for Opts.IsStaging covering both accepted values, "stage" and "staging",
which must return true, plus a non-staging environment value, which must return
false. Keep the tests table-driven or otherwise scoped specifically to this
method.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/core/extensions/plan.dart`:
- Around line 15-20: Update the monthlyUsdCents getter to convert the Stripe
expectedMonthlyPrice amount from major USD units to cents by multiplying the
parsed value by 100 before rounding. Keep _amountOf and _formatPriceMap
unchanged so display currency formatting continues to use the existing
major-unit conversion.

In `@lib/core/models/plan_data.dart`:
- Around line 9-12: Update the platformProviders getter in the plan data model
to use PlatformUtils.isAndroid instead of PlatformUtils.isMobile, returning
providers.android on Android and providers.desktop on iOS or other platforms to
match choose_payment_method.dart and sort the displayed provider list.

In `@lib/core/services/stripe_service.dart`:
- Around line 163-174: Unify Stripe user-facing error filtering by making
StripeErrorMessage.userFacingMessage delegate to
StripeErrorExtension.localizedDescription, or by sharing its allowlist that
permits only card_error, validation_error, and decline-coded errors. Update
lib/core/services/stripe_service.dart:163-174 to remove the inconsistent
_hiddenErrorTypes denylist; retain lib/core/extensions/error.dart:159-174 as the
single implementation if delegation is used, otherwise remove it only if it
becomes redundant.

In `@lib/features/auth/choose_payment_method.dart`:
- Around line 209-246: Guard both the onSuccess and onError callbacks passed to
startStripeSDK with an early context.mounted check before invoking
finishPaymentRedirect, onPurchaseResult, or any context.showSnackBar call.
Preserve the existing success, cancellation, logging, and error-message behavior
when the widget remains mounted.

---

Nitpick comments:
In `@lantern-core/utils/common.go`:
- Around line 18-23: Add focused regression tests for Opts.IsStaging covering
both accepted values, "stage" and "staging", which must return true, plus a
non-staging environment value, which must return false. Keep the tests
table-driven or otherwise scoped specifically to this method.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 44fe5435-fe81-4a4b-9ee0-65f1aeafdae7

📥 Commits

Reviewing files that changed from the base of the PR and between 7e3945d and e07dea8.

📒 Files selected for processing (11)
  • lantern-core/core.go
  • lantern-core/mobile/mobile.go
  • lantern-core/utils/common.go
  • lib/core/common/app_secrets.dart
  • lib/core/extensions/error.dart
  • lib/core/extensions/plan.dart
  • lib/core/models/plan_data.dart
  • lib/core/services/injection_container.dart
  • lib/core/services/stripe_service.dart
  • lib/features/auth/choose_payment_method.dart
  • lib/features/plans/provider/plans_notifier.dart
💤 Files with no reviewable changes (1)
  • lib/core/common/app_secrets.dart

Comment thread lib/core/extensions/plan.dart Outdated
Comment thread lib/core/models/plan_data.dart Outdated
Comment thread lib/core/services/stripe_service.dart
Comment thread lib/features/auth/choose_payment_method.dart
Copilot AI review requested due to automatic review settings July 31, 2026 13:04

Copilot AI left a comment

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.

🟡 Not ready to approve

The Stripe publishable key is updated but Stripe.instance.applySettings() is no longer called anywhere, which can prevent the native SDK from receiving the key before initPaymentSheet runs.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (1)

lib/core/services/stripe_service.dart:62

  • Stripe.publishableKey is set via updatePublishableKey, but I can't find any call to Stripe.instance.applySettings() in the repo. flutter_stripe's docs/examples require applySettings() after setting the key so the native SDK picks it up; without it, initPaymentSheet may fail due to a missing/stale publishable key.
      // initPaymentSheet applies any pending settings (including the
      // publishable key) to the native SDK itself. If plans never provided
      // a key, this throws StripeConfigException into the catch below.
      await Stripe.instance.initPaymentSheet(
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 31, 2026 13:09

Copilot AI left a comment

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.

🟡 Not ready to approve

The deferred PaymentSheet is initialized with IntentMode.paymentMode but the flow can return a SetupIntent client secret, which can break the PaymentSheet confirm step due to intent-type mismatch.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread lib/core/services/stripe_service.dart Outdated
Copilot AI review requested due to automatic review settings August 3, 2026 13:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/core/extensions/plan.dart`:
- Around line 13-15: Update the yearlyAmountInt getter to return the annual
Stripe amount in USD cents: convert the monthly major-unit price by multiplying
by 12 and 100 before rounding, while preserving the empty-price result of 0.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f22c0539-49ea-427f-826e-7a2cd5a30656

📥 Commits

Reviewing files that changed from the base of the PR and between 4e9b429 and 892561a.

📒 Files selected for processing (6)
  • lib/core/extensions/plan.dart
  • lib/core/models/app_setting.dart
  • lib/core/services/stripe_service.dart
  • lib/features/auth/choose_payment_method.dart
  • lib/features/developer/developer_mode.dart
  • lib/lantern_app.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/features/auth/choose_payment_method.dart
  • lib/core/services/stripe_service.dart

Comment thread lib/core/extensions/plan.dart Outdated

Copilot AI left a comment

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.

🟡 Not ready to approve

The Stripe amount passed to the PaymentSheet is currently derived from expectedMonthlyPrice (not guaranteed to be USD cents) and the Stripe SDK settings aren’t applied before initPaymentSheet, both of which can break or mis-price payments.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (1)

lib/core/services/stripe_service.dart:63

  • After setting Stripe.publishableKey via updatePublishableKey, the SDK settings are never applied before calling initPaymentSheet. flutter_stripe’s recommended setup calls Stripe.instance.applySettings() after updating the publishable key; otherwise the native SDK may still be configured with the old/missing key when the sheet is initialized.
      // initPaymentSheet applies any pending settings (including the
      // publishable key) to the native SDK itself. If plans never provided
      // a key, this throws StripeConfigException into the catch below.
      await Stripe.instance.initPaymentSheet(
        paymentSheetParameters: SetupPaymentSheetParameters(
  • Files reviewed: 10/10 changed files
  • Comments generated: 2
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread lib/core/extensions/plan.dart Outdated
Comment thread lib/features/auth/choose_payment_method.dart Outdated
Copilot AI review requested due to automatic review settings August 3, 2026 13:52

Copilot AI left a comment

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.

🟡 Not ready to approve

There are correctness issues that can break payments (SetupIntent secret ignored) and misquote/charge amounts (Stripe amount derived from monthly-equivalent pricing), plus a potential null-crash in the app builder.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (3)

lib/core/extensions/plan.dart:15

  • yearlyAmountInt is computed from expectedMonthlyPrice (the monthly-equivalent display price), but it’s used as the USD-cents amount passed to Stripe’s payment sheet. This will quote/charge the wrong amount for 1y/2y plans. Use the backend-provided usdPrice (already an int in USD cents) instead.
  int get yearlyAmountInt => expectedMonthlyPrice.isEmpty
      ? 0
      : _amountOf(expectedMonthlyPrice).round();

lib/core/services/stripe_service.dart:130

  • _createSubscriptionAndConfirm always uses options.clientSecret, but the comment/log indicate the backend can return a SetupIntent secret (stored in setupIntentClientSecret) for the trial path. In that case clientSecret will be empty and this will always throw, breaking the flow. Prefer the setup-intent secret when the payment-intent secret is absent.
      // Normal path returns a PaymentIntent secret; the trial path (user
      // still has an unexpired one-time purchase) returns a SetupIntent
      // secret instead.
      final secret = options.clientSecret;

      if (secret.isEmpty) {

lib/lantern_app.dart:248

  • MaterialApp.router's builder can receive a null child; force-unwrapping with child! will crash in that case (both in the early return and in the Banner). Handle the null case and avoid repeated ! by unwrapping once.
                builder: (context, child) {
                  if (!isStaging) return child!;
                  return Banner(
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI left a comment

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.

🟡 Not ready to approve

It introduces a potential runtime crash in MaterialApp.router.builder by force-unwrapping a nullable child (child!) instead of safely handling null.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (1)

lib/lantern_app.dart:254

  • MaterialApp.router.builder receives a nullable child; using child! will throw if Flutter ever calls the builder with child == null (e.g., during certain transitions/tests). Handle null safely and reuse the same non-null widget for both branches.
                builder: (context, child) {
                  if (!isStaging) return child!;
                  return Banner(
                    message: 'STAGING',
                    location: BannerLocation.topEnd,
  • Files reviewed: 9/9 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

* Harden deferred Stripe subscription flow

* code review updates
Copilot AI review requested due to automatic review settings August 6, 2026 12:10

Copilot AI left a comment

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.

🔵 Human review recommended

It changes core payment/subscription behavior and environment/key selection, which is high-impact and warrants careful human validation across platforms and backend integration paths.

Review details

Suppressed comments (3)

test/core/services/stripe_service_test.dart:327

  • The fake Stripe platform records that initialise ran, but it discards the publishableKey argument, so the "publishable key is applied" test can't actually verify which key was used. Capture the key (or include it in the event log) so tests can assert it.
    events.add('initialise:start');
    events.add('initialise:end');
  }

lib/core/services/stripe_service.dart:43

  • The docstring says this method "no-ops on an unchanged key", but the implementation always assigns Stripe.publishableKey. Either update the comment or add an equality guard so repeated plans refreshes don't imply a settings change.
  void updatePublishableKey(String? pubKey) {
    if (pubKey == null || pubKey.isEmpty) return;
    Stripe.publishableKey = pubKey;

test/core/services/stripe_service_test.dart:71

  • This test name implies we assert the publishable key used by StripePlatform.initialise(), but the current expectation only checks call ordering. Once the fake platform records the key, assert it here to make the test meaningful.

This issue also appears on line 325 of the same file.

      'initialise:start',
      'initialise:end',
      'payment-sheet:init',
      'payment-sheet:present',
    ]);
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
test/core/services/stripe_service_test.dart (5)

245-296: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for null userData.

stripeIntentModeForRenewal returns StripeIntentMode.payment when userData is null. That branch is reachable in production, because _stripeIntentMode in lib/features/auth/choose_payment_method.dart passes ref.read(homeProvider).value?.legacyUserData, which is null while the provider loads or after it fails. No test covers it.

♻️ Proposed test
+    test('uses payment mode when user data is unavailable', () {
+      expect(
+        stripeIntentModeForRenewal(null, currentTimeSeconds: 1000),
+        StripeIntentMode.payment,
+      );
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/services/stripe_service_test.dart` around lines 245 - 296, Add a
test in the stripeIntentModeForRenewal group that passes null userData and
currentTimeSeconds, then asserts the result is StripeIntentMode.payment. Keep
the existing non-null purchaser and subscription cases unchanged.

66-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the forwarded publishable key, not only the call order.

The test name states that the key is applied before initPaymentSheet runs. The assertions only check event order. _FakeStripePlatform.initialise discards publishableKey, so a regression that forwards the wrong key still passes.

Capture the key in the fake and assert its value.

♻️ Proposed refactor
     expect(platform.events, [
       'initialise:start',
       'initialise:end',
       'payment-sheet:init',
       'payment-sheet:present',
     ]);
+    expect(platform.initialisedKeys, ['pk_test_deferred_settings']);

In _FakeStripePlatform:

   final events = <String>[];
+  final initialisedKeys = <String>[];
     events.add('initialise:start');
+    initialisedKeys.add(publishableKey);
     events.add('initialise:end');

Also clear initialisedKeys in reset().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/services/stripe_service_test.dart` around lines 66 - 71, Update
_FakeStripePlatform to capture each publishableKey passed to initialise in an
initialisedKeys collection, clear that collection in reset(), and extend the
test assertions to verify the expected key value alongside the existing event
order checks.

24-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reset the global publishable key in setUp.

Stripe.publishableKey is static and persists between tests in this file. Only the first test sets it. The later tests then depend on state that the first test leaked. If a test is skipped, reordered, or run in isolation, that dependency becomes visible.

Set a known key in setUp so each test starts from the same state.

♻️ Proposed refactor
   setUp(() {
     platform.reset();
+    Stripe.publishableKey = 'pk_test_default';
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/services/stripe_service_test.dart` around lines 24 - 26, Update the
setUp block to assign Stripe.publishableKey a known test key after resetting
platform, ensuring every test starts with isolated, deterministic global state.

155-159: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the empty-secret mismatch path.

No test covers the case where the selected intentMode and the returned secret disagree. That path calls intentCreationCallback with an error and leaves a created subscription unconfirmed. A test would lock in the behavior and the message.

Add a case that requests StripeIntentMode.setup and returns only clientSecret, then assert that the recorded callback carries an error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/services/stripe_service_test.dart` around lines 155 - 159, Add a
test in the Stripe service test suite for the setup intent mode receiving only a
clientSecret, using the existing subscription creation callback setup. Capture
the intentCreationCallback invocation and assert it carries an error while the
created subscription remains unconfirmed, including the expected mismatch
message.

344-354: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Call _confirmHandler directly instead of using Function.apply.

_confirmHandler has the concrete ConfirmHandler signature, so a direct call preserves compile-time checking for that callback. Function.apply(_confirmHandler!, [_paymentMethod, false]) hides signature changes behind a runtime failure.

♻️ Proposed refactor
-      final confirmation = Function.apply(_confirmHandler!, [
-        _paymentMethod,
-        false,
-      ]);
-      if (confirmation is Future<void>) {
-        await confirmation;
-      }
+      await _confirmHandler!(_paymentMethod, false);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/services/stripe_service_test.dart` around lines 344 - 354, In the
confirmation loop, replace the Function.apply invocation with a direct call to
the non-null _confirmHandler callback, passing _paymentMethod and false
according to its ConfirmHandler signature. Preserve the existing Future<void>
handling and _intentCallbackCompleter wait.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/core/services/stripe_service.dart`:
- Around line 20-27: Replace the raw `hasPurchaseHistory` string comparison in
the purchase-status logic with a JSON-aware helper such as
`_hasPurchaseHistory`, treating blank text, `null`, empty arrays,
whitespace-only arrays, and empty objects as no history while preserving a
textual fallback for non-JSON values. Update the `hasActiveOneTimePurchase`
calculation to use this helper.

In `@lib/features/auth/choose_payment_method.dart`:
- Around line 268-269: Update _stripeIntentMode in
lib/features/auth/choose_payment_method.dart at lines 268-269 to await or
otherwise obtain the required homeProvider data before selecting the renewal
intent mode, and log the degraded selection when user data is unavailable
instead of treating it as confirmed non-renewal. In
lib/core/services/stripe_service.dart at lines 186-193, update
_createSubscriptionAndConfirm to log an empty secret together with the selected
intentMode and subscriptionId; do not substitute the alternate secret.

---

Nitpick comments:
In `@test/core/services/stripe_service_test.dart`:
- Around line 245-296: Add a test in the stripeIntentModeForRenewal group that
passes null userData and currentTimeSeconds, then asserts the result is
StripeIntentMode.payment. Keep the existing non-null purchaser and subscription
cases unchanged.
- Around line 66-71: Update _FakeStripePlatform to capture each publishableKey
passed to initialise in an initialisedKeys collection, clear that collection in
reset(), and extend the test assertions to verify the expected key value
alongside the existing event order checks.
- Around line 24-26: Update the setUp block to assign Stripe.publishableKey a
known test key after resetting platform, ensuring every test starts with
isolated, deterministic global state.
- Around line 155-159: Add a test in the Stripe service test suite for the setup
intent mode receiving only a clientSecret, using the existing subscription
creation callback setup. Capture the intentCreationCallback invocation and
assert it carries an error while the created subscription remains unconfirmed,
including the expected mismatch message.
- Around line 344-354: In the confirmation loop, replace the Function.apply
invocation with a direct call to the non-null _confirmHandler callback, passing
_paymentMethod and false according to its ConfirmHandler signature. Preserve the
existing Future<void> handling and _intentCallbackCompleter wait.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b04a3f9d-17c8-451c-aa66-3199e28b3369

📥 Commits

Reviewing files that changed from the base of the PR and between 181f12d and c2903a5.

📒 Files selected for processing (4)
  • lib/core/services/stripe_service.dart
  • lib/features/auth/choose_payment_method.dart
  • lib/lantern_app.dart
  • test/core/services/stripe_service_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/lantern_app.dart

Comment thread lib/core/services/stripe_service.dart Outdated
Comment thread lib/features/auth/choose_payment_method.dart Outdated
Copilot AI review requested due to automatic review settings August 6, 2026 12:29

Copilot AI left a comment

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.

🟡 Changes recommended

The Stripe confirm-step error mapping can leak developer/diagnostic details to users, and the new publishable-key test doesn’t currently assert the key passed into Stripe initialisation without the suggested fixes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (4)

test/core/services/stripe_service_test.dart:342

  • initialise() should record the publishableKey it was called with (e.g., lastPublishableKey = publishableKey) so the test can assert the updated key was actually applied before PaymentSheet initialization.
  }) async {
    events.add('initialise:start');
    events.add('initialise:end');
  }

lib/core/services/stripe_service.dart:237

  • _createSubscriptionAndConfirm() builds the PaymentSheet inline error message from e.toString() for all non-StripeException failures. If an unexpected exception occurs (e.g., TypeError/StateError), this can surface developer/diagnostic details to end users. Prefer only surfacing the explicit user-facing Exception message you throw from onCreateSubscription, and otherwise fall back to a generic localizedDescription.
      final message = e is StripeException
          ? e.userFacingMessage
          : e.toString().replaceFirst('Exception: ', '');

test/core/services/stripe_service_test.dart:71

  • This test asserts initialise() happens before initPaymentSheet(), but it never verifies that initialise() was called with the updated publishable key. With the fake recording lastPublishableKey, assert it here so the test matches its name/purpose.
    expect(platform.events, [
      'initialise:start',
      'initialise:end',
      'payment-sheet:init',
      'payment-sheet:present',

test/core/services/stripe_service_test.dart:329

  • _FakeStripePlatform doesn’t capture the publishableKey passed to initialise(), so tests can’t verify which key was applied. Add a field to record it (and reset it between tests).

This issue also appears on line 339 of the same file.

class _FakeStripePlatform extends StripePlatform {
  final events = <String>[];
  final intentCallbacks = <IntentCreationCallbackParams>[];
  SetupPaymentSheetParameters? paymentSheetParameters;
  ConfirmHandler? _confirmHandler;
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings August 6, 2026 12:35

Copilot AI left a comment

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.

🔵 Human review recommended

It changes a critical payment flow (Stripe deferred-intent + dynamic keying) across multiple layers and needs manual end-to-end validation on Android/Stripe to ensure no regressions in real checkout scenarios.

Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@jigar-f
jigar-f merged commit 84bd027 into main Aug 6, 2026
11 checks passed
@jigar-f
jigar-f deleted the jigar/stripe-ghost-sub-fix branch August 6, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants