Skip to content

Commit 8c095f9

Browse files
committed
code review updates
1 parent c2903a5 commit 8c095f9

3 files changed

Lines changed: 53 additions & 4 deletions

File tree

lib/core/services/stripe_service.dart

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:convert';
2+
13
import 'package:flutter/foundation.dart';
24
import 'package:flutter/material.dart';
35
import '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+
3450
class 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
);

lib/features/auth/choose_payment_method.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,15 @@ class ChoosePaymentMethod extends HookConsumerWidget {
265265
return StripeIntentMode.payment;
266266
}
267267

268-
final userData = ref.read(homeProvider).value?.legacyUserData;
268+
final home = ref.read(homeProvider);
269+
final userData = home.value?.legacyUserData;
270+
if (userData == null) {
271+
appLogger.error(
272+
'Stripe: renewal intent mode selected without user data '
273+
'(homeProvider loading: ${home.isLoading}, '
274+
'error: ${home.hasError}); defaulting to PaymentIntent',
275+
);
276+
}
269277
return stripeIntentModeForRenewal(userData);
270278
}
271279

test/core/services/stripe_service_test.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,21 @@ void main() {
293293
StripeIntentMode.payment,
294294
);
295295
});
296+
297+
test('treats other empty purchase encodings as no history', () {
298+
for (final encoded in ['', ' ', 'null', '[ ]', '[\n]', '{}']) {
299+
final user = UserDataModel(
300+
userLevel: 'pro',
301+
expiration: 2000,
302+
purchases: encoded,
303+
);
304+
expect(
305+
stripeIntentModeForRenewal(user, currentTimeSeconds: 1000),
306+
StripeIntentMode.payment,
307+
reason: 'purchases: "$encoded"',
308+
);
309+
}
310+
});
296311
});
297312
}
298313

0 commit comments

Comments
 (0)