refactor: migrate Stripe checkout to external browser with lifecycle-based subscription refresh

This commit is contained in:
pradeepkumar
2026-04-12 13:08:36 +05:30
parent 1a08730b83
commit 09cee9931b

View File

@@ -1,12 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart'; import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart';
import 'package:real_estate_mobile/features/profile/presentation/screens/stripe_checkout_screen.dart';
class BillingTab extends ConsumerStatefulWidget { class BillingTab extends ConsumerStatefulWidget {
const BillingTab({super.key}); const BillingTab({super.key});
@@ -15,16 +13,33 @@ class BillingTab extends ConsumerStatefulWidget {
ConsumerState<BillingTab> createState() => _BillingTabState(); ConsumerState<BillingTab> createState() => _BillingTabState();
} }
class _BillingTabState extends ConsumerState<BillingTab> { class _BillingTabState extends ConsumerState<BillingTab>
with WidgetsBindingObserver {
Map<String, dynamic>? _subscription; Map<String, dynamic>? _subscription;
bool _isLoadingSubscription = false; bool _isLoadingSubscription = false;
bool _openedExternalCheckout = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
_loadSubscription(); _loadSubscription();
} }
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed && _openedExternalCheckout) {
_openedExternalCheckout = false;
_loadSubscription();
}
}
Future<void> _loadSubscription() async { Future<void> _loadSubscription() async {
if (_isLoadingSubscription) return; if (_isLoadingSubscription) return;
setState(() => _isLoadingSubscription = true); setState(() => _isLoadingSubscription = true);
@@ -51,7 +66,10 @@ class _BillingTabState extends ConsumerState<BillingTab> {
_subscription!['status'] == 'ACTIVE'; _subscription!['status'] == 'ACTIVE';
final plan = _subscription?['plan'] as Map<String, dynamic>?; final plan = _subscription?['plan'] as Map<String, dynamic>?;
return ListView( return RefreshIndicator(
color: AppColors.accentOrange,
onRefresh: _loadSubscription,
child: ListView(
padding: const EdgeInsets.fromLTRB(24, 10, 24, 30), padding: const EdgeInsets.fromLTRB(24, 10, 24, 30),
children: [ children: [
const Center( const Center(
@@ -109,6 +127,7 @@ class _BillingTabState extends ConsumerState<BillingTab> {
const SizedBox(height: 24), const SizedBox(height: 24),
_buildNeedHelpSection(), _buildNeedHelpSection(),
], ],
),
); );
} }
@@ -371,6 +390,15 @@ class _BillingTabState extends ConsumerState<BillingTab> {
), ),
], ],
), ),
const SizedBox(height: 12),
Text(
'By clicking this button you\'ll be taken to our website to complete your purchase.',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 12,
color: AppColors.primaryDark.withValues(alpha: 0.5),
),
),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( const Text(
'Include Features', 'Include Features',
@@ -555,18 +583,13 @@ class _BillingTabState extends ConsumerState<BillingTab> {
final repo = ref.read(profileRepositoryProvider); final repo = ref.read(profileRepositoryProvider);
final url = await repo.createPortalSession(); final url = await repo.createPortalSession();
if (!mounted) return; if (!mounted) return;
await Navigator.of(context, rootNavigator: true).push<Map<String, dynamic>?>( _openedExternalCheckout = true;
MaterialPageRoute( await launchUrl(
builder: (_) => StripeCheckoutScreen( Uri.parse(url),
url: url, mode: LaunchMode.externalApplication,
title: 'Manage Subscription',
),
),
); );
if (mounted) {
_loadSubscription();
}
} catch (e) { } catch (e) {
_openedExternalCheckout = false;
if (mounted) _showSnackBar('Failed to open billing portal: $e', isError: true); if (mounted) _showSnackBar('Failed to open billing portal: $e', isError: true);
} }
} }
@@ -582,22 +605,13 @@ class _BillingTabState extends ConsumerState<BillingTab> {
final planId = plans.first['id'] as String; final planId = plans.first['id'] as String;
final url = await repo.createCheckoutSession(planId); final url = await repo.createCheckoutSession(planId);
if (!mounted) return; if (!mounted) return;
final result = await Navigator.of(context, rootNavigator: true).push<Map<String, dynamic>?>( _openedExternalCheckout = true;
MaterialPageRoute( await launchUrl(
builder: (_) => StripeCheckoutScreen( Uri.parse(url),
url: url, mode: LaunchMode.externalApplication,
title: 'Checkout',
successUrlPattern: 'confirmation',
),
),
); );
if (result != null && result['success'] == true && mounted) {
final sessionId = result['sessionId'] as String?;
context.go('/payment-success${sessionId != null ? '?session_id=$sessionId' : ''}');
} else if (mounted) {
_loadSubscription();
}
} catch (e) { } catch (e) {
_openedExternalCheckout = false;
if (mounted) _showSnackBar('Failed to start checkout: $e', isError: true); if (mounted) _showSnackBar('Failed to start checkout: $e', isError: true);
} }
} }