1+ import 'dart:convert' ;
2+
13import 'package:flutter/foundation.dart' ;
24import 'package:flutter/material.dart' ;
35import 'package:flutter_stripe/flutter_stripe.dart' ;
@@ -17,8 +19,7 @@ StripeIntentMode stripeIntentModeForRenewal(
1719
1820 final now =
1921 currentTimeSeconds ?? DateTime .now ().millisecondsSinceEpoch ~ / 1000 ;
20- final purchases = userData.purchases.trim ();
21- final hasPurchaseHistory = purchases.isNotEmpty && purchases != '[]' ;
22+ final hasPurchaseHistory = _hasPurchaseHistory (userData.purchases);
2223 final hasActiveOneTimePurchase =
2324 userData.isPro &&
2425 userData.expiration > now &&
@@ -31,6 +32,21 @@ StripeIntentMode stripeIntentModeForRenewal(
3132 : StripeIntentMode .payment;
3233}
3334
35+ /// The backend serializes purchases as JSON, so empty history can arrive as
36+ /// '[] ', 'null', '[ ] ', or blank text — decode rather than compare raw text.
37+ bool _hasPurchaseHistory (String purchases) {
38+ final trimmed = purchases.trim ();
39+ if (trimmed.isEmpty || trimmed == 'null' ) return false ;
40+ try {
41+ final decoded = jsonDecode (trimmed);
42+ if (decoded is List ) return decoded.isNotEmpty;
43+ if (decoded is Map ) return decoded.isNotEmpty;
44+ } catch (_) {
45+ // Not JSON; fall through to the textual check.
46+ }
47+ return trimmed != '[]' ;
48+ }
49+
3450class StripeService {
3551 /// Adopts the publishable key advertised by the plans response so the SDK
3652 /// always confirms intents against the same Stripe account/environment the
@@ -181,7 +197,8 @@ class StripeService {
181197 appLogger.info (
182198 'Stripe: subscription created '
183199 '(subscriptionId: ${options .subscriptionId }, '
184- 'secret type: ${options .clientSecret .isNotEmpty ? 'payment' : 'setup' })' ,
200+ 'payment secret: ${options .clientSecret .isNotEmpty }, '
201+ 'setup secret: ${options .setupIntentClientSecret .isNotEmpty })' ,
185202 );
186203 // The client must return the same kind of intent used to initialize the
187204 // deferred sheet. Active one-time purchases start with a free trial and
@@ -193,6 +210,15 @@ class StripeService {
193210 };
194211
195212 if (secret.isEmpty) {
213+ // The backend chose the other intent type than the sheet was
214+ // initialized with. Don't substitute the other secret — the SDK
215+ // requires the confirmed intent to match the declared
216+ // IntentConfiguration mode.
217+ appLogger.error (
218+ 'Stripe: intent mode mismatch — client selected $intentMode but the '
219+ 'backend returned no matching secret '
220+ '(subscriptionId: ${options .subscriptionId })' ,
221+ );
196222 throw Exception (
197223 'Please try again after some time. If the issue persists, contact support.' ,
198224 );
0 commit comments