feat: add payment card grouping and analysis
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../models/payment_card_model.dart';
|
||||
import '../models/subscription_model.dart';
|
||||
import '../providers/payment_card_provider.dart';
|
||||
import '../providers/subscription_provider.dart';
|
||||
import '../providers/locale_provider.dart';
|
||||
import '../utils/payment_card_utils.dart';
|
||||
import '../widgets/native_ad_widget.dart';
|
||||
import '../widgets/analysis/analysis_screen_spacer.dart';
|
||||
import '../widgets/analysis/subscription_pie_chart_card.dart';
|
||||
@@ -9,6 +14,8 @@ import '../widgets/analysis/total_expense_summary_card.dart';
|
||||
import '../widgets/analysis/monthly_expense_chart_card.dart';
|
||||
import '../widgets/analysis/event_analysis_card.dart';
|
||||
|
||||
enum AnalysisCardFilterType { all, unassigned, card }
|
||||
|
||||
class AnalysisScreen extends StatefulWidget {
|
||||
const AnalysisScreen({super.key});
|
||||
|
||||
@@ -25,6 +32,8 @@ class _AnalysisScreenState extends State<AnalysisScreen>
|
||||
List<Map<String, dynamic>> _monthlyData = [];
|
||||
bool _isLoading = true;
|
||||
String _lastDataHash = '';
|
||||
AnalysisCardFilterType _filterType = AnalysisCardFilterType.all;
|
||||
String? _selectedCardId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -42,7 +51,8 @@ class _AnalysisScreenState extends State<AnalysisScreen>
|
||||
super.didChangeDependencies();
|
||||
// Provider 변경 감지
|
||||
final provider = Provider.of<SubscriptionProvider>(context);
|
||||
final currentHash = _calculateDataHash(provider);
|
||||
final filtered = _filterSubscriptions(provider.subscriptions);
|
||||
final currentHash = _calculateDataHash(provider, filtered: filtered);
|
||||
|
||||
debugPrint('[AnalysisScreen] didChangeDependencies: '
|
||||
'현재 해시=$currentHash, 이전 해시=$_lastDataHash, 로딩중=$_isLoading');
|
||||
@@ -64,13 +74,16 @@ class _AnalysisScreenState extends State<AnalysisScreen>
|
||||
}
|
||||
|
||||
/// 구독 데이터의 해시값을 계산하여 변경 감지
|
||||
String _calculateDataHash(SubscriptionProvider provider) {
|
||||
final subscriptions = provider.subscriptions;
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.write(subscriptions.length);
|
||||
buffer.write('_');
|
||||
buffer.write(provider.totalMonthlyExpense.toStringAsFixed(2));
|
||||
String _calculateDataHash(
|
||||
SubscriptionProvider provider, {
|
||||
List<SubscriptionModel>? filtered,
|
||||
}) {
|
||||
final subscriptions =
|
||||
filtered ?? _filterSubscriptions(provider.subscriptions);
|
||||
final buffer = StringBuffer()
|
||||
..write(_filterType.name)
|
||||
..write('_${_selectedCardId ?? 'all'}')
|
||||
..write('_${subscriptions.length}');
|
||||
|
||||
for (final sub in subscriptions) {
|
||||
buffer.write(
|
||||
@@ -80,6 +93,38 @@ class _AnalysisScreenState extends State<AnalysisScreen>
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
List<SubscriptionModel> _filterSubscriptions(
|
||||
List<SubscriptionModel> subscriptions) {
|
||||
switch (_filterType) {
|
||||
case AnalysisCardFilterType.all:
|
||||
return subscriptions;
|
||||
case AnalysisCardFilterType.unassigned:
|
||||
return subscriptions.where((sub) => sub.paymentCardId == null).toList();
|
||||
case AnalysisCardFilterType.card:
|
||||
final cardId = _selectedCardId;
|
||||
if (cardId == null) return subscriptions;
|
||||
return subscriptions
|
||||
.where((sub) => sub.paymentCardId == cardId)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onFilterChanged(AnalysisCardFilterType type,
|
||||
{String? cardId}) async {
|
||||
if (_filterType == type) {
|
||||
if (type != AnalysisCardFilterType.card || _selectedCardId == cardId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_filterType = type;
|
||||
_selectedCardId = type == AnalysisCardFilterType.card ? cardId : null;
|
||||
});
|
||||
|
||||
await _loadData();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
debugPrint('[AnalysisScreen] _loadData 호출됨');
|
||||
setState(() {
|
||||
@@ -89,17 +134,25 @@ class _AnalysisScreenState extends State<AnalysisScreen>
|
||||
final provider = Provider.of<SubscriptionProvider>(context, listen: false);
|
||||
final localeProvider = Provider.of<LocaleProvider>(context, listen: false);
|
||||
final locale = localeProvider.locale.languageCode;
|
||||
final filteredSubscriptions = _filterSubscriptions(provider.subscriptions);
|
||||
|
||||
// 총 지출 계산 (로케일별 기본 통화로 환산)
|
||||
_totalExpense = await provider.calculateTotalExpense(locale: locale);
|
||||
_totalExpense = await provider.calculateTotalExpense(
|
||||
locale: locale,
|
||||
subset: filteredSubscriptions,
|
||||
);
|
||||
debugPrint('[AnalysisScreen] 총 지출 계산 완료: $_totalExpense');
|
||||
|
||||
// 월별 데이터 계산 (로케일별 기본 통화로 환산)
|
||||
_monthlyData = await provider.getMonthlyExpenseData(locale: locale);
|
||||
_monthlyData = await provider.getMonthlyExpenseData(
|
||||
locale: locale,
|
||||
subset: filteredSubscriptions,
|
||||
);
|
||||
debugPrint('[AnalysisScreen] 월별 데이터 계산 완료: ${_monthlyData.length}개월');
|
||||
|
||||
// 현재 데이터 해시값 저장
|
||||
_lastDataHash = _calculateDataHash(provider);
|
||||
_lastDataHash =
|
||||
_calculateDataHash(provider, filtered: filteredSubscriptions);
|
||||
debugPrint('[AnalysisScreen] 데이터 해시값 저장: $_lastDataHash');
|
||||
|
||||
setState(() {
|
||||
@@ -130,6 +183,128 @@ class _AnalysisScreenState extends State<AnalysisScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCardFilterSection(
|
||||
BuildContext context, PaymentCardProvider cardProvider) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
final chips = <Widget>[
|
||||
_buildGenericFilterChip(
|
||||
context: context,
|
||||
label: loc.analysisCardFilterAll,
|
||||
icon: Icons.credit_card,
|
||||
selected: _filterType == AnalysisCardFilterType.all,
|
||||
onTap: () => _onFilterChanged(AnalysisCardFilterType.all),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildGenericFilterChip(
|
||||
context: context,
|
||||
label: loc.paymentCardUnassigned,
|
||||
icon: Icons.credit_card_off_rounded,
|
||||
selected: _filterType == AnalysisCardFilterType.unassigned,
|
||||
onTap: () => _onFilterChanged(AnalysisCardFilterType.unassigned),
|
||||
),
|
||||
];
|
||||
|
||||
for (final card in cardProvider.cards) {
|
||||
chips.add(const SizedBox(width: 8));
|
||||
chips.add(_buildPaymentCardChip(context, card));
|
||||
}
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
loc.analysisCardFilterLabel,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: chips),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGenericFilterChip({
|
||||
required BuildContext context,
|
||||
required String label,
|
||||
required IconData icon,
|
||||
required bool selected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Semantics(
|
||||
selected: selected,
|
||||
button: true,
|
||||
label: label,
|
||||
child: ChoiceChip(
|
||||
label: Text(label),
|
||||
avatar: Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: selected ? cs.onPrimary : cs.onSurfaceVariant,
|
||||
),
|
||||
selected: selected,
|
||||
onSelected: (_) => onTap(),
|
||||
selectedColor: cs.primary,
|
||||
labelStyle: TextStyle(
|
||||
color: selected ? cs.onPrimary : cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
backgroundColor: cs.surface,
|
||||
side: BorderSide(
|
||||
color:
|
||||
selected ? Colors.transparent : cs.outline.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentCardChip(BuildContext context, PaymentCardModel card) {
|
||||
final color = PaymentCardUtils.colorFromHex(card.colorHex);
|
||||
final icon = PaymentCardUtils.iconForName(card.iconName);
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final selected = _filterType == AnalysisCardFilterType.card &&
|
||||
_selectedCardId == card.id;
|
||||
final labelText = '${card.issuerName} · ****${card.last4}';
|
||||
return Semantics(
|
||||
label: labelText,
|
||||
selected: selected,
|
||||
button: true,
|
||||
child: ChoiceChip(
|
||||
avatar: CircleAvatar(
|
||||
backgroundColor:
|
||||
selected ? cs.onPrimary : color.withValues(alpha: 0.15),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: selected ? color : cs.onSurface,
|
||||
),
|
||||
),
|
||||
label: Text(labelText),
|
||||
selected: selected,
|
||||
onSelected: (_) =>
|
||||
_onFilterChanged(AnalysisCardFilterType.card, cardId: card.id),
|
||||
selectedColor: color,
|
||||
backgroundColor: cs.surface,
|
||||
labelStyle: TextStyle(
|
||||
color: selected ? cs.onPrimary : cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: selected ? Colors.transparent : color.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Provider를 직접 사용하여 변경 감지
|
||||
@@ -142,6 +317,9 @@ class _AnalysisScreenState extends State<AnalysisScreen>
|
||||
);
|
||||
}
|
||||
|
||||
final cardProvider = Provider.of<PaymentCardProvider>(context);
|
||||
final filteredSubscriptions = _filterSubscriptions(subscriptions);
|
||||
|
||||
return CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
@@ -159,9 +337,13 @@ class _AnalysisScreenState extends State<AnalysisScreen>
|
||||
|
||||
const AnalysisScreenSpacer(),
|
||||
|
||||
_buildCardFilterSection(context, cardProvider),
|
||||
|
||||
const AnalysisScreenSpacer(),
|
||||
|
||||
// 1. 구독 비율 파이 차트
|
||||
SubscriptionPieChartCard(
|
||||
subscriptions: subscriptions,
|
||||
subscriptions: filteredSubscriptions,
|
||||
animationController: _animationController,
|
||||
),
|
||||
|
||||
@@ -170,7 +352,7 @@ class _AnalysisScreenState extends State<AnalysisScreen>
|
||||
// 2. 총 지출 요약 카드
|
||||
TotalExpenseSummaryCard(
|
||||
key: ValueKey('total_expense_$_lastDataHash'),
|
||||
subscriptions: subscriptions,
|
||||
subscriptions: filteredSubscriptions,
|
||||
totalExpense: _totalExpense,
|
||||
animationController: _animationController,
|
||||
),
|
||||
@@ -189,6 +371,7 @@ class _AnalysisScreenState extends State<AnalysisScreen>
|
||||
// 4. 이벤트 분석
|
||||
EventAnalysisCard(
|
||||
animationController: _animationController,
|
||||
subscriptions: filteredSubscriptions,
|
||||
),
|
||||
|
||||
// FloatingNavigationBar를 위한 충분한 하단 여백
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
import '../models/subscription_model.dart';
|
||||
import '../controllers/detail_screen_controller.dart';
|
||||
import '../widgets/detail/detail_header_section.dart';
|
||||
import '../widgets/detail/detail_payment_info_section.dart';
|
||||
import '../widgets/detail/detail_form_section.dart';
|
||||
import '../widgets/detail/detail_event_section.dart';
|
||||
import '../widgets/detail/detail_url_section.dart';
|
||||
@@ -120,6 +121,13 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
DetailPaymentInfoSection(
|
||||
controller: _controller,
|
||||
fadeAnimation: _controller.fadeAnimation!,
|
||||
slideAnimation: _controller.slideAnimation!,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 기본 정보 폼 섹션
|
||||
DetailFormSection(
|
||||
controller: _controller,
|
||||
|
||||
144
lib/screens/payment_card_management_screen.dart
Normal file
144
lib/screens/payment_card_management_screen.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../models/payment_card_model.dart';
|
||||
import '../providers/payment_card_provider.dart';
|
||||
import '../utils/payment_card_utils.dart';
|
||||
import '../widgets/payment_card/payment_card_form_sheet.dart';
|
||||
|
||||
class PaymentCardManagementScreen extends StatelessWidget {
|
||||
const PaymentCardManagementScreen({super.key});
|
||||
|
||||
Future<void> _openForm(BuildContext context, {PaymentCardModel? card}) async {
|
||||
await PaymentCardFormSheet.show(context, card: card);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(loc.paymentCardManagement),
|
||||
),
|
||||
body: Consumer<PaymentCardProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final cards = provider.cards;
|
||||
if (cards.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
loc.noPaymentCards,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
itemCount: cards.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final card = cards[index];
|
||||
final color = PaymentCardUtils.colorFromHex(card.colorHex);
|
||||
final icon = PaymentCardUtils.iconForName(card.iconName);
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: color.withValues(alpha: 0.15),
|
||||
child: Icon(icon, color: color),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(card.issuerName)),
|
||||
if (card.isDefault)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
loc.cardDefaultBadge,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text('****${card.last4}'),
|
||||
trailing: PopupMenuButton<String>(
|
||||
onSelected: (value) =>
|
||||
_handleMenuSelection(context, value, card, provider),
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
value: 'default',
|
||||
child: Text(loc.setAsDefaultCard),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Text(loc.editPaymentCard),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text(loc.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => _openForm(context, card: card),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _openForm(context),
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(loc.addPaymentCard),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleMenuSelection(
|
||||
BuildContext context,
|
||||
String value,
|
||||
PaymentCardModel card,
|
||||
PaymentCardProvider provider,
|
||||
) async {
|
||||
switch (value) {
|
||||
case 'default':
|
||||
await provider.setDefaultCard(card.id);
|
||||
break;
|
||||
case 'edit':
|
||||
await _openForm(context, card: card);
|
||||
break;
|
||||
case 'delete':
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).delete),
|
||||
content: Text(AppLocalizations.of(context).areYouSure),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(AppLocalizations.of(context).delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
await provider.deleteCard(card.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import '../providers/theme_provider.dart';
|
||||
import '../theme/adaptive_theme.dart';
|
||||
import '../widgets/common/layout/page_container.dart';
|
||||
import '../theme/color_scheme_ext.dart';
|
||||
import '../widgets/app_navigator.dart';
|
||||
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
const SettingsScreen({super.key});
|
||||
@@ -79,6 +80,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -99,6 +101,48 @@ class SettingsScreen extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 테마 모드 설정
|
||||
Card(
|
||||
margin:
|
||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: loc.paymentCardManagement,
|
||||
hint: loc.paymentCardManagementDescription,
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
Icons.credit_card,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
title: Text(
|
||||
loc.paymentCardManagement,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
loc.paymentCardManagementDescription,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(Icons.chevron_right_rounded,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
onTap: () =>
|
||||
AppNavigator.toPaymentCardManagement(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
Card(
|
||||
margin:
|
||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
|
||||
@@ -6,6 +6,9 @@ import '../widgets/sms_scan/scan_progress_widget.dart';
|
||||
import '../widgets/sms_scan/subscription_card_widget.dart';
|
||||
import '../widgets/common/snackbar/app_snackbar.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../widgets/payment_card/payment_card_form_sheet.dart';
|
||||
import '../routes/app_routes.dart';
|
||||
import '../models/payment_card_suggestion.dart';
|
||||
|
||||
class SmsScanScreen extends StatefulWidget {
|
||||
const SmsScanScreen({super.key});
|
||||
@@ -96,8 +99,23 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
websiteUrlController: _controller.websiteUrlController,
|
||||
selectedCategoryId: _controller.selectedCategoryId,
|
||||
onCategoryChanged: _controller.setSelectedCategoryId,
|
||||
selectedPaymentCardId: _controller.selectedPaymentCardId,
|
||||
onPaymentCardChanged: _controller.setSelectedPaymentCardId,
|
||||
onAddCard: () async {
|
||||
final newCardId = await PaymentCardFormSheet.show(context);
|
||||
if (newCardId != null) {
|
||||
_controller.setSelectedPaymentCardId(newCardId);
|
||||
}
|
||||
},
|
||||
onManageCards: () {
|
||||
Navigator.of(context).pushNamed(AppRoutes.paymentCardManagement);
|
||||
},
|
||||
onAdd: _handleAddSubscription,
|
||||
onSkip: _handleSkipSubscription,
|
||||
detectedCardSuggestion: _controller.currentSuggestion,
|
||||
showDetectedCardShortcut: _controller.shouldSuggestCardCreation,
|
||||
onAddDetectedCard: (suggestion) =>
|
||||
_handleDetectedCardCreation(suggestion),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -114,6 +132,18 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToTop());
|
||||
}
|
||||
|
||||
Future<void> _handleDetectedCardCreation(
|
||||
PaymentCardSuggestion suggestion) async {
|
||||
final newCardId = await PaymentCardFormSheet.show(
|
||||
context,
|
||||
initialIssuerName: suggestion.issuerName,
|
||||
initialLast4: suggestion.last4,
|
||||
);
|
||||
if (newCardId != null) {
|
||||
_controller.setSelectedPaymentCardId(newCardId);
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollToTop() {
|
||||
if (!_scrollController.hasClients) return;
|
||||
_scrollController.animateTo(
|
||||
|
||||
Reference in New Issue
Block a user