734 lines
23 KiB
Dart
734 lines
23 KiB
Dart
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||
|
||
import '../../core/adjust/adjust_events.dart';
|
||
import '../../core/api/api_config.dart';
|
||
import '../../core/api/services/payment_api.dart';
|
||
import '../../core/auth/auth_service.dart';
|
||
import '../../core/log/app_logger.dart';
|
||
import '../../core/theme/app_colors.dart';
|
||
import '../../core/user/user_state.dart';
|
||
import '../../core/theme/app_spacing.dart';
|
||
import '../../core/theme/app_typography.dart';
|
||
import '../../shared/widgets/top_nav_bar.dart';
|
||
|
||
import 'google_play_purchase_service.dart';
|
||
import 'models/activity_item.dart';
|
||
import 'models/payment_method_item.dart';
|
||
import 'payment_webview_screen.dart';
|
||
|
||
/// Recharge screen - matches Pencil tPjdN
|
||
/// Tier cards (DC6NS, YRaOG, QlNz6) 对接 getGooglePayActivities / getApplePayActivities
|
||
class RechargeScreen extends StatefulWidget {
|
||
static final _log = AppLogger('Recharge');
|
||
const RechargeScreen({super.key});
|
||
|
||
@override
|
||
State<RechargeScreen> createState() => _RechargeScreenState();
|
||
}
|
||
|
||
class _RechargeScreenState extends State<RechargeScreen> with WidgetsBindingObserver {
|
||
List<ActivityItem> _activities = [];
|
||
bool _loadingTiers = true;
|
||
String? _tierError;
|
||
/// 当前正在支付的商品 code,仅该 item 的 Buy 显示 loading
|
||
String? _loadingProductId;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addObserver(this);
|
||
_fetchActivities();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
if (state == AppLifecycleState.resumed && _loadingProductId != null && mounted) {
|
||
setState(() => _loadingProductId = null);
|
||
}
|
||
}
|
||
|
||
Future<void> _fetchActivities() async {
|
||
setState(() {
|
||
_loadingTiers = true;
|
||
_tierError = null;
|
||
});
|
||
try {
|
||
await AuthService.loginComplete;
|
||
final country = UserState.navigate.value ?? '';
|
||
|
||
final res = defaultTargetPlatform == TargetPlatform.iOS
|
||
? await PaymentApi.getApplePayActivities(
|
||
vambrace: country.isEmpty ? null : country,
|
||
)
|
||
: await PaymentApi.getGooglePayActivities(
|
||
vambrace: country.isEmpty ? null : country,
|
||
);
|
||
if (!mounted) return;
|
||
if (res.isSuccess && res.data != null) {
|
||
final data = res.data as Map<String, dynamic>?;
|
||
final summon = data?['summon'] as List<dynamic>? ?? [];
|
||
final list = summon
|
||
.whereType<Map<String, dynamic>>()
|
||
.map((e) => ActivityItem.fromJson(e))
|
||
.where((e) => e.code.isNotEmpty)
|
||
.toList();
|
||
setState(() {
|
||
_activities = list;
|
||
_loadingTiers = false;
|
||
});
|
||
} else {
|
||
setState(() {
|
||
_activities = [];
|
||
_loadingTiers = false;
|
||
_tierError = res.msg.isNotEmpty ? res.msg : 'Failed to load';
|
||
});
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_activities = [];
|
||
_loadingTiers = false;
|
||
_tierError = e.toString();
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _onBuy(ActivityItem item) async {
|
||
if (_loadingProductId != null) return;
|
||
setState(() => _loadingProductId = item.code);
|
||
|
||
final price = AdjustEvents.parsePrice(item.actualAmount);
|
||
if (price != null) {
|
||
final tierToken = AdjustEvents.tierTokenFromPrice(price);
|
||
if (tierToken != null) {
|
||
AdjustEvents.trackTier(tierToken);
|
||
}
|
||
}
|
||
final titleLower = item.title.toLowerCase();
|
||
if (titleLower.contains('monthly vip')) {
|
||
AdjustEvents.trackMonthlyVip();
|
||
} else if (titleLower.contains('weekly vip')) {
|
||
AdjustEvents.trackWeeklyVip();
|
||
}
|
||
|
||
final useThirdParty =
|
||
UserState.enableThirdPartyPayment.value == true;
|
||
final uid = UserState.userId.value;
|
||
|
||
if (useThirdParty && uid != null && uid.isNotEmpty) {
|
||
await _runThirdPartyPayment(item, uid);
|
||
} else {
|
||
await _runGooglePay(item);
|
||
}
|
||
}
|
||
|
||
/// 第三方支付流程:获取支付方式 -> 弹窗选择 -> 创建订单 -> 打开支付页
|
||
Future<void> _runThirdPartyPayment(ActivityItem item, String userId) async {
|
||
final activityId = item.activityId;
|
||
if (activityId == null || activityId.isEmpty) {
|
||
if (mounted) {
|
||
_showSnackBar(context, 'Invalid product', isError: true);
|
||
}
|
||
AdjustEvents.trackPaymentFailed();
|
||
return;
|
||
}
|
||
|
||
if (!mounted) return;
|
||
|
||
try {
|
||
final country = UserState.navigate.value;
|
||
|
||
final methodsRes = await PaymentApi.getPaymentMethods(
|
||
warrior: activityId,
|
||
vambrace: country?.isEmpty == true ? null : country,
|
||
);
|
||
if (!mounted) return;
|
||
if (!methodsRes.isSuccess || methodsRes.data == null) {
|
||
_showSnackBar(
|
||
context,
|
||
methodsRes.msg.isNotEmpty ? methodsRes.msg : 'Failed to load payment methods',
|
||
isError: true,
|
||
);
|
||
AdjustEvents.trackPaymentFailed();
|
||
return;
|
||
}
|
||
|
||
final methodsList = methodsRes.data is Map<String, dynamic>
|
||
? (methodsRes.data! as Map<String, dynamic>)['renew'] as List<dynamic>?
|
||
: null;
|
||
if (methodsList == null || methodsList.isEmpty) {
|
||
_showSnackBar(context, 'No payment methods available', isError: true);
|
||
AdjustEvents.trackPaymentFailed();
|
||
return;
|
||
}
|
||
|
||
final methods = methodsList
|
||
.whereType<Map<String, dynamic>>()
|
||
.map((e) => PaymentMethodItem.fromJson(e))
|
||
.where((m) => m.paymentMethod.isNotEmpty)
|
||
.toList();
|
||
if (methods.isEmpty) {
|
||
_showSnackBar(context, 'No payment methods available', isError: true);
|
||
AdjustEvents.trackPaymentFailed();
|
||
return;
|
||
}
|
||
|
||
if (!mounted) return;
|
||
final selected = await showDialog<PaymentMethodItem>(
|
||
context: context,
|
||
builder: (ctx) => _PaymentMethodDialog(methods: methods),
|
||
);
|
||
|
||
if (!mounted) return;
|
||
if (selected == null) {
|
||
setState(() => _loadingProductId = null);
|
||
return;
|
||
}
|
||
|
||
await _createOrderAndOpenUrl(
|
||
userId: userId,
|
||
activityId: activityId,
|
||
productId: item.code,
|
||
paymentMethod: selected.paymentMethod,
|
||
subPaymentMethod: selected.subPaymentMethod?.isEmpty == true ? null : selected.subPaymentMethod,
|
||
);
|
||
} catch (e) {
|
||
if (mounted) {
|
||
_showSnackBar(context, 'Payment error: ${e.toString()}', isError: true);
|
||
}
|
||
AdjustEvents.trackPaymentFailed();
|
||
} finally {
|
||
if (mounted) {
|
||
setState(() => _loadingProductId = null);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 创建订单;若为 Google Pay 则调起内购并上报凭据,否则打开支付链接
|
||
Future<void> _createOrderAndOpenUrl({
|
||
required String userId,
|
||
required String activityId,
|
||
required String productId,
|
||
required String paymentMethod,
|
||
String? subPaymentMethod,
|
||
}) async {
|
||
if (!mounted) return;
|
||
|
||
try {
|
||
final createRes = await PaymentApi.createPayment(
|
||
sentinel: ApiConfig.appId,
|
||
asset: userId,
|
||
warrior: activityId,
|
||
resource: paymentMethod,
|
||
ceremony: subPaymentMethod,
|
||
);
|
||
if (!mounted) return;
|
||
if (!createRes.isSuccess) {
|
||
_showSnackBar(
|
||
context,
|
||
createRes.msg.isNotEmpty ? createRes.msg : 'Failed to create order',
|
||
isError: true,
|
||
);
|
||
AdjustEvents.trackPaymentFailed();
|
||
return;
|
||
}
|
||
|
||
final data = createRes.data is Map<String, dynamic>
|
||
? createRes.data as Map<String, dynamic>
|
||
: null;
|
||
final orderId = data?['federation']?.toString();
|
||
|
||
if (_isGooglePay(paymentMethod, subPaymentMethod)) {
|
||
if (defaultTargetPlatform != TargetPlatform.android) {
|
||
_showSnackBar(context, 'Google Pay is only available on Android.', isError: true);
|
||
AdjustEvents.trackPaymentFailed();
|
||
return;
|
||
}
|
||
final purchaseData = await GooglePlayPurchaseService.launchPurchaseAndReturnData(productId);
|
||
if (!mounted) return;
|
||
if (purchaseData != null && purchaseData.isNotEmpty && orderId != null && orderId.isNotEmpty) {
|
||
RechargeScreen._log.d('googlepay 入参: federation=$orderId, asset=$userId');
|
||
RechargeScreen._log.d('googlepay 入参: merchant(length=${purchaseData.length}) ${purchaseData.length > 400 ? "${purchaseData.substring(0, 400)}..." : purchaseData}');
|
||
final googlepayRes = await PaymentApi.googlepay(
|
||
merchant: purchaseData,
|
||
federation: orderId,
|
||
asset: userId,
|
||
);
|
||
if (!mounted) return;
|
||
if (googlepayRes.isSuccess) {
|
||
_showSnackBar(context, 'Purchase completed.');
|
||
AdjustEvents.trackPurchaseSuccess();
|
||
} else {
|
||
_showSnackBar(context, googlepayRes.msg.isNotEmpty ? googlepayRes.msg : 'Payment verification failed.', isError: true);
|
||
AdjustEvents.trackPaymentFailed();
|
||
}
|
||
} else {
|
||
_showSnackBar(context, 'Purchase was cancelled or failed.', isError: true);
|
||
AdjustEvents.trackPaymentFailed();
|
||
}
|
||
return;
|
||
}
|
||
|
||
final payUrl = data?['convert']?.toString();
|
||
if (payUrl != null && payUrl.isNotEmpty) {
|
||
if (mounted) {
|
||
await Navigator.of(context).push(
|
||
MaterialPageRoute<void>(
|
||
builder: (_) => PaymentWebViewScreen(paymentUrl: payUrl),
|
||
),
|
||
);
|
||
_showSnackBar(context, 'Order created. Complete payment in the page.');
|
||
AdjustEvents.trackPurchaseSuccess();
|
||
}
|
||
} else {
|
||
if (mounted) {
|
||
_showSnackBar(context, 'Order created. Awaiting payment confirmation.');
|
||
}
|
||
AdjustEvents.trackPurchaseSuccess();
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
_showSnackBar(context, 'Payment error: ${e.toString()}', isError: true);
|
||
}
|
||
AdjustEvents.trackPaymentFailed();
|
||
} finally {
|
||
if (mounted) {
|
||
setState(() => _loadingProductId = null);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// ceremony==GooglePay 或 resource==GOOGLEPAY 时走谷歌内购并上报
|
||
static bool _isGooglePay(String paymentMethod, String? subPaymentMethod) {
|
||
final r = paymentMethod.trim().toLowerCase();
|
||
final c = (subPaymentMethod ?? '').trim().toLowerCase();
|
||
return r == 'googlepay' || c == 'googlepay';
|
||
}
|
||
|
||
/// 三方为 false 时走谷歌应用内支付
|
||
Future<void> _runGooglePay(ActivityItem item) async {
|
||
if (defaultTargetPlatform != TargetPlatform.android) {
|
||
_showSnackBar(context, 'Google Pay is only available on Android.', isError: true);
|
||
AdjustEvents.trackPaymentFailed();
|
||
return;
|
||
}
|
||
await _launchGooglePlayPurchase(item);
|
||
}
|
||
|
||
/// 调起 Google Play 内购(商品 ID = item.code / helm)
|
||
Future<void> _launchGooglePlayPurchase(ActivityItem item) async {
|
||
if (!mounted) return;
|
||
|
||
try {
|
||
final success = await GooglePlayPurchaseService.launchPurchase(item.code);
|
||
if (!mounted) return;
|
||
if (success) {
|
||
_showSnackBar(context, 'Purchase completed.');
|
||
AdjustEvents.trackPurchaseSuccess();
|
||
} else {
|
||
_showSnackBar(context, 'Purchase was cancelled or failed.', isError: true);
|
||
AdjustEvents.trackPaymentFailed();
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
_showSnackBar(context, 'Google Pay error: ${e.toString()}', isError: true);
|
||
}
|
||
AdjustEvents.trackPaymentFailed();
|
||
} finally {
|
||
if (mounted) {
|
||
setState(() => _loadingProductId = null);
|
||
}
|
||
}
|
||
}
|
||
|
||
void _showSnackBar(BuildContext context, String message, {bool isError = false}) {
|
||
ScaffoldMessenger.of(context).clearSnackBars();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(message),
|
||
backgroundColor: isError ? Colors.red.shade700 : null,
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: AppColors.background,
|
||
appBar: PreferredSize(
|
||
preferredSize: const Size.fromHeight(56),
|
||
child: TopNavBar(
|
||
title: 'Recharge',
|
||
credits: UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||
showBackButton: true,
|
||
onBack: () => Navigator.of(context).pop(),
|
||
onCreditsTap: null,
|
||
),
|
||
),
|
||
body: SingleChildScrollView(
|
||
padding: const EdgeInsets.only(bottom: AppSpacing.screenPaddingLarge),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_CreditsSection(
|
||
currentCredits:
|
||
UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.screenPaddingLarge,
|
||
vertical: AppSpacing.xxl,
|
||
),
|
||
child: Text(
|
||
'Select Tier',
|
||
style: AppTypography.bodyMedium.copyWith(
|
||
color: AppColors.textPrimary,
|
||
),
|
||
),
|
||
),
|
||
if (_loadingTiers)
|
||
const Padding(
|
||
padding: EdgeInsets.all(AppSpacing.xxl),
|
||
child: Center(
|
||
child: SizedBox(
|
||
width: 24,
|
||
height: 24,
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
),
|
||
),
|
||
)
|
||
else if (_tierError != null)
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.screenPadding),
|
||
child: Column(
|
||
children: [
|
||
Text(
|
||
_tierError!,
|
||
style: AppTypography.bodyRegular.copyWith(
|
||
color: AppColors.textSecondary,
|
||
),
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
TextButton(
|
||
onPressed: _fetchActivities,
|
||
child: const Text('Retry'),
|
||
),
|
||
],
|
||
),
|
||
)
|
||
else if (_activities.isEmpty)
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.screenPadding),
|
||
child: Text(
|
||
'No packages available',
|
||
style: AppTypography.bodyRegular.copyWith(
|
||
color: AppColors.textSecondary,
|
||
),
|
||
),
|
||
)
|
||
else
|
||
...List.generate(_activities.length, (i) {
|
||
final item = _activities[i];
|
||
final isRecommended = i == 1;
|
||
final isPopular = i == 2;
|
||
return Padding(
|
||
padding: EdgeInsets.only(
|
||
bottom: i < _activities.length - 1
|
||
? AppSpacing.xl
|
||
: 0,
|
||
),
|
||
child: _TierCardFromActivity(
|
||
item: item,
|
||
badge: isRecommended
|
||
? _TierBadge.recommended
|
||
: isPopular
|
||
? _TierBadge.popular
|
||
: _TierBadge.none,
|
||
loading: _loadingProductId == item.code,
|
||
onBuy: () => _onBuy(item),
|
||
),
|
||
);
|
||
}),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 支付方式选择弹窗(三方支付列表)
|
||
class _PaymentMethodDialog extends StatelessWidget {
|
||
const _PaymentMethodDialog({required this.methods});
|
||
|
||
final List<PaymentMethodItem> methods;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return AlertDialog(
|
||
title: const Text('Select payment method'),
|
||
content: SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: methods
|
||
.map(
|
||
(m) => ListTile(
|
||
title: Text(m.displayName),
|
||
subtitle: m.subPaymentMethod != null && m.subPaymentMethod!.isNotEmpty
|
||
? Text(m.subPaymentMethod!, style: AppTypography.caption)
|
||
: null,
|
||
trailing: m.recommend
|
||
? Text('Recommended', style: AppTypography.caption.copyWith(color: AppColors.primary))
|
||
: null,
|
||
onTap: () => Navigator.of(context).pop(m),
|
||
),
|
||
)
|
||
.toList(),
|
||
),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('Cancel'),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
enum _TierBadge { none, recommended, popular }
|
||
|
||
class _TierCardFromActivity extends StatelessWidget {
|
||
const _TierCardFromActivity({
|
||
required this.item,
|
||
required this.badge,
|
||
required this.loading,
|
||
required this.onBuy,
|
||
});
|
||
|
||
final ActivityItem item;
|
||
final _TierBadge badge;
|
||
final bool loading;
|
||
final VoidCallback onBuy;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final content = Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.screenPadding),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.xxl,
|
||
vertical: AppSpacing.xl,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.surface,
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(color: AppColors.border),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: AppColors.shadowLight,
|
||
blurRadius: 6,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
item.creditsDisplay,
|
||
style: AppTypography.bodyLarge.copyWith(
|
||
color: AppColors.textPrimary,
|
||
),
|
||
),
|
||
SizedBox(height: badge == _TierBadge.none ? AppSpacing.xs : AppSpacing.sm),
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||
textBaseline: TextBaseline.alphabetic,
|
||
children: [
|
||
Text(
|
||
item.priceDisplayWithDollar,
|
||
style: AppTypography.bodyRegular.copyWith(
|
||
color: AppColors.textSecondary,
|
||
),
|
||
),
|
||
if (item.originAmount != null &&
|
||
item.originAmount!.isNotEmpty &&
|
||
item.originAmount != item.actualAmount) ...[
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Text(
|
||
item.originAmount!,
|
||
style: AppTypography.bodyRegular.copyWith(
|
||
color: AppColors.textSecondary,
|
||
decoration: TextDecoration.lineThrough,
|
||
decorationColor: AppColors.textSecondary,
|
||
),
|
||
),
|
||
],
|
||
if (item.bonus > 0) ...[
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Text(
|
||
'Bonus: ${item.bonus} credits',
|
||
style: AppTypography.caption.copyWith(
|
||
color: AppColors.primary,
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
_BuyButton(onTap: onBuy, loading: loading),
|
||
],
|
||
),
|
||
);
|
||
|
||
if (badge == _TierBadge.none) {
|
||
return content;
|
||
}
|
||
|
||
return Stack(
|
||
clipBehavior: Clip.none,
|
||
children: [
|
||
content,
|
||
Positioned(
|
||
top: 0,
|
||
right: AppSpacing.screenPadding,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.md,
|
||
vertical: AppSpacing.xs,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: badge == _TierBadge.recommended
|
||
? AppColors.primaryLight
|
||
: AppColors.accentOrangeLight,
|
||
borderRadius: const BorderRadius.only(
|
||
topRight: Radius.circular(16),
|
||
bottomLeft: Radius.circular(16),
|
||
),
|
||
),
|
||
child: Text(
|
||
badge == _TierBadge.recommended
|
||
? 'Recommended'
|
||
: 'Most Popular',
|
||
style: AppTypography.label.copyWith(
|
||
color: badge == _TierBadge.recommended
|
||
? AppColors.primary
|
||
: AppColors.accentOrange,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _CreditsSection extends StatelessWidget {
|
||
const _CreditsSection({required this.currentCredits});
|
||
|
||
final String currentCredits;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
margin: const EdgeInsets.all(AppSpacing.screenPadding),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.xxl,
|
||
vertical: AppSpacing.xl,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.primary,
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(
|
||
color: AppColors.primary.withValues(alpha: 0.5),
|
||
),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: AppColors.primaryShadow.withValues(alpha: 0.25),
|
||
blurRadius: 8,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(LucideIcons.sparkles, size: 28, color: AppColors.surface),
|
||
const SizedBox(width: AppSpacing.md),
|
||
Text(
|
||
currentCredits,
|
||
style: AppTypography.bodyLarge.copyWith(
|
||
fontSize: 32,
|
||
fontWeight: FontWeight.w700,
|
||
color: AppColors.surface,
|
||
),
|
||
),
|
||
const Spacer(),
|
||
Text(
|
||
'Current Credits',
|
||
style: AppTypography.caption.copyWith(
|
||
color: AppColors.surface.withValues(alpha: 0.8),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _BuyButton extends StatelessWidget {
|
||
const _BuyButton({required this.onTap, this.loading = false});
|
||
|
||
final VoidCallback onTap;
|
||
final bool loading;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return GestureDetector(
|
||
onTap: loading ? null : onTap,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.xl,
|
||
vertical: AppSpacing.md,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: loading ? AppColors.primary.withValues(alpha: 0.7) : AppColors.primary,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: loading
|
||
? const SizedBox(
|
||
width: 20,
|
||
height: 20,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.surface),
|
||
),
|
||
)
|
||
: Text(
|
||
'Buy',
|
||
style: AppTypography.bodyRegular.copyWith(
|
||
color: AppColors.surface,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|