refactor: migrate Stripe checkout to external browser with lifecycle-based subscription refresh
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user