app state fixed

This commit is contained in:
2026-02-25 18:37:17 +01:00
parent 4b4eb807d7
commit 0c93440f9b
54 changed files with 941 additions and 500 deletions

View File

@@ -1,3 +1,4 @@
import 'package:auth/auth.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -8,27 +9,31 @@ import 'package:design_system/design_system.dart';
import 'package:sca_treezor/sca_treezor_module.dart';
import 'package:sf_app_platform/config/env/questia_env_config.dart';
import 'package:sf_app_platform/navigation/app_router.dart';
import 'package:navigation/navigation_module.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_app_platform/providers/app_state_provider.dart';
import 'package:sf_app_platform/providers/permissions/permissions_provider.dart';
import 'package:sf_app_platform/providers/wallet_heartbeat_service.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
import 'package:fonts/fonts.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
navigationModule();
scaTreezorModule();
configureAppRouter();
themePackages();
await dotenv.load(fileName: '.env');
await configureDependencies(QuestiaEnvConfig(), log: kDebugMode);
await configureDependencies(
QuestiaEnvConfig(),
log: kDebugMode,
onTreezorTokenExpired: () => appRouter.go(AppRoutes.scaTreezor),
);
runApp(const ProviderScope(child: PlatformApp()));
}
@@ -47,7 +52,11 @@ class PlatformAppState extends ConsumerState<PlatformApp>
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
walletHeartbeat = WalletHeartbeatService.create();
walletHeartbeat = WalletHeartbeatService(
repository: ref.read(treezorRepositoryProvider),
sessionLocal: SessionLocalDatasourceImpl(),
);
onBeforeSessionCleared = walletHeartbeat.stop;
walletHeartbeat.start();
}

View File

@@ -2,29 +2,21 @@ import 'dart:async';
import 'package:auth/auth.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/sf_shared.dart';
class WalletHeartbeatService {
WalletHeartbeatService({
required QuestiaRepository repository,
required TreezorRepository repository,
required SessionLocalDatasource sessionLocal,
}) : _repository = repository,
_sessionLocal = sessionLocal;
final QuestiaRepository _repository;
final TreezorRepository _repository;
final SessionLocalDatasource _sessionLocal;
Timer? _timer;
static const _interval = Duration(minutes: 4);
factory WalletHeartbeatService.create() {
return WalletHeartbeatService(
repository: GetIt.I<QuestiaRepository>(),
sessionLocal: SessionLocalDatasourceImpl(),
);
}
void start() {
if (_timer != null) return;
_beat();
@@ -43,12 +35,8 @@ class WalletHeartbeatService {
if (walletId == null || walletId.isEmpty) return;
try {
final response = await _repository.get<Map<String, dynamic>>(
'/wallets/$walletId',
);
debugPrint(
'[WalletHeartbeat] /wallets/$walletId => ${response.statusCode}',
);
await _repository.getWallet(walletId: walletId);
debugPrint('[WalletHeartbeat] /wallets/$walletId => OK');
} catch (e) {
debugPrint('[WalletHeartbeat] error: $e');
}

View File

@@ -968,7 +968,7 @@ packages:
source: path
version: "0.0.1"
sf_shared:
dependency: "direct overridden"
dependency: "direct main"
description:
path: "../../packages/sf_shared"
relative: true

View File

@@ -60,6 +60,8 @@ dependencies:
path: ../../packages/sf_localizations
fonts:
path: ../../packages/fonts
sf_shared:
path: ../../packages/sf_shared
sf_infrastructure:
path: ../../packages/sf_infrastructure
utils:

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,3 @@ export 'src/widgets/activity_list.dart';
export 'src/widgets/transaction_tile.dart';
export 'src/activity_builder.dart';
export 'src/providers/activity_providers.dart';
export 'src/domain/date_filter.dart';
export 'src/domain/use_cases/get_wallet_balance_use_case.dart';
export 'src/domain/use_cases/get_wallet_transactions_use_case.dart';

View File

@@ -3,29 +3,31 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:activity/src/domain/date_filter.dart';
import 'package:activity/src/providers/activity_providers.dart';
import 'package:activity/src/presentation/state/activity_view_model.dart';
import 'package:activity/src/presentation/state/activity_view_state.dart';
import 'package:activity/src/widgets/transaction_tile.dart';
class ActivityScreen extends ConsumerWidget {
class ActivityScreen extends ConsumerStatefulWidget {
const ActivityScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<ActivityScreen> createState() => _ActivityScreenState();
}
class _ActivityScreenState extends ConsumerState<ActivityScreen> {
@override
Widget build(BuildContext context) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(activityViewModelProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
body: _buildBody(context, ref, theme, viewState),
body: _buildBody(context, theme, viewState),
);
}
Widget _buildBody(
BuildContext context,
WidgetRef ref,
ThemePort theme,
ActivityViewState viewState,
) {
@@ -34,7 +36,7 @@ class ActivityScreen extends ConsumerWidget {
}
if (viewState.errorMessage.isNotEmpty && viewState.tabs.isEmpty) {
return _buildError(context, ref, theme, viewState.errorMessage);
return _buildError(context, theme, viewState.errorMessage);
}
if (viewState.tabs.isEmpty) {
@@ -51,15 +53,14 @@ class ActivityScreen extends ConsumerWidget {
return Column(
children: [
_buildHeader(context, ref, theme, viewState),
Expanded(child: _buildTransactions(context, ref, theme, viewState)),
_buildHeader(context, theme, viewState),
Expanded(child: _buildTransactions(context, theme, viewState)),
],
);
}
Widget _buildHeader(
BuildContext context,
WidgetRef ref,
ThemePort theme,
ActivityViewState viewState,
) {
@@ -74,9 +75,9 @@ class ActivityScreen extends ConsumerWidget {
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 24),
),
const SizedBox(height: 16),
_buildDateFilter(context, ref, theme, viewState),
_buildDateFilter(context, theme, viewState),
const SizedBox(height: 12),
_buildWalletSelector(context, ref, theme, viewState),
_buildWalletSelector(context, theme, viewState),
],
),
),
@@ -85,7 +86,6 @@ class ActivityScreen extends ConsumerWidget {
Widget _buildDateFilter(
BuildContext context,
WidgetRef ref,
ThemePort theme,
ActivityViewState viewState,
) {
@@ -115,7 +115,6 @@ class ActivityScreen extends ConsumerWidget {
Widget _buildWalletSelector(
BuildContext context,
WidgetRef ref,
ThemePort theme,
ActivityViewState viewState,
) {
@@ -163,7 +162,6 @@ class ActivityScreen extends ConsumerWidget {
Widget _buildTransactions(
BuildContext context,
WidgetRef ref,
ThemePort theme,
ActivityViewState viewState,
) {
@@ -172,7 +170,7 @@ class ActivityScreen extends ConsumerWidget {
}
if (viewState.errorMessage.isNotEmpty) {
return _buildError(context, ref, theme, viewState.errorMessage);
return _buildError(context, theme, viewState.errorMessage);
}
if (viewState.transactions.isEmpty) {
@@ -196,7 +194,7 @@ class ActivityScreen extends ConsumerWidget {
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 24),
itemCount: viewState.transactions.length + 2,
itemCount: viewState.transactions.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
@@ -206,21 +204,14 @@ class ActivityScreen extends ConsumerWidget {
const Center(child: CircularProgressIndicator()),
error: (_, __) => const SizedBox.shrink(),
data: (balance) => WalletBalanceBlock(
value: balance.availableBalance,
savings: balance.allocatedBalance,
availableBalance: balance.availableBalance,
allocatedBalance: balance.allocatedBalance,
totalBalance: balance.totalBalance,
),
),
);
}
if (index == 1) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: LineGraph(
lineLabels: viewState.tabs.map((t) => t.label).toList(),
),
);
}
final transaction = viewState.transactions[index - 2];
final transaction = viewState.transactions[index - 1];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TransactionTile(transaction: transaction),
@@ -230,9 +221,7 @@ class ActivityScreen extends ConsumerWidget {
);
}
Widget _buildError(BuildContext context, WidgetRef ref, ThemePort theme, String message) {
final viewModel = ref.read(activityViewModelProvider.notifier);
Widget _buildError(BuildContext context, ThemePort theme, String message) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
@@ -255,7 +244,8 @@ class ActivityScreen extends ConsumerWidget {
),
const SizedBox(height: 16),
TextButton(
onPressed: () => viewModel.retry(),
onPressed: () =>
ref.read(activityViewModelProvider.notifier).retry(),
child: Text(context.translate(I18n.retry)),
),
],

View File

@@ -1,11 +1,6 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:activity/src/domain/date_filter.dart';
import 'package:activity/src/domain/wallet_tab.dart';
import 'package:activity/src/domain/use_cases/get_wallet_transactions_use_case.dart';
import 'package:activity/src/providers/activity_providers.dart';
import 'activity_view_state.dart';
final activityViewModelProvider =
@@ -14,18 +9,17 @@ final activityViewModelProvider =
);
class ActivityViewModel extends Notifier<ActivityViewState> {
late final GetWalletTransactionsUseCase _getTransactionsUseCase;
late final GetPaymentProfileUseCase _getPaymentProfileUseCase;
late final GetUserInfoUseCase _getUserInfoUseCase;
late final UserRepository _userRepository;
late GetPaymentProfileUseCase _getPaymentProfileUseCase;
late GetUserInfoUseCase _getUserInfoUseCase;
late UserRepository _userRepository;
@override
ActivityViewState build() {
_getTransactionsUseCase = ref.read(getWalletTransactionsUseCaseProvider);
_getPaymentProfileUseCase = ref.read(getPaymentProfileUseCaseProvider);
_getUserInfoUseCase = ref.read(getUserInfoUseCaseProvider);
_userRepository = ref.read(userRepositoryProvider);
ref.watch(walletRefreshProvider);
Future.microtask(() => loadTabs());
return const ActivityViewState(isLoading: true);
}
@@ -80,23 +74,19 @@ class ActivityViewModel extends Notifier<ActivityViewState> {
if (tab == null) return;
state = state.copyWith(isLoadingTransactions: true, errorMessage: '');
try {
final filtersJson = jsonEncode({
'createdDate': {
'gte': state.selectedDateFilter.startDate.toIso8601String(),
},
});
final filtersBase64 = base64Encode(utf8.encode(filtersJson));
final response = await _getTransactionsUseCase(
try {
final query = TransactionsQuery(
walletId: tab.walletId,
queryParameters: {'filters': filtersBase64},
dateFilter: state.selectedDateFilter,
);
final transactions =
await ref.read(walletTransactionsProvider(query).future);
if (!ref.mounted) return;
state = state.copyWith(
isLoadingTransactions: false,
transactions: response.items,
transactions: transactions,
);
} catch (e) {
if (!ref.mounted) return;

View File

@@ -1,6 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:activity/src/domain/date_filter.dart';
import 'package:activity/src/domain/wallet_tab.dart';
part 'activity_view_state.freezed.dart';

View File

@@ -1,37 +1,7 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:activity/src/domain/date_filter.dart';
import 'package:activity/src/domain/use_cases/get_wallet_balance_use_case.dart';
import 'package:activity/src/domain/use_cases/get_wallet_transactions_use_case.dart';
import 'package:activity/src/domain/wallet_tab.dart';
final getWalletTransactionsUseCaseProvider =
Provider.autoDispose<GetWalletTransactionsUseCase>((ref) {
final repository = ref.watch(treezorRepositoryProvider);
return GetWalletTransactionsUseCaseImpl(repository);
});
final getWalletBalanceUseCaseProvider =
Provider.autoDispose<GetWalletBalanceUseCase>((ref) {
final repository = ref.watch(treezorRepositoryProvider);
return GetWalletBalanceUseCaseImpl(repository);
});
final walletBalanceProvider =
FutureProvider.autoDispose.family<WalletBalanceEntity, String>(
(ref, walletId) async {
final useCase = ref.read(getWalletBalanceUseCaseProvider);
return useCase(walletId: walletId);
});
final childProfilesProvider =
FutureProvider.autoDispose<List<ChildProfileEntity>>((ref) async {
final repository = ref.read(userRepositoryProvider);
return repository.getChildProfiles();
});
final childProfileByIdProvider =
FutureProvider.autoDispose.family<ChildProfileEntity?, String>(
(ref, childId) async {
@@ -80,23 +50,3 @@ final deviceByIdentificatorProvider =
final repository = ref.read(userRepositoryProvider);
return repository.getDeviceByIdentificator(identificator: identificator);
});
final walletTransactionsProvider = FutureProvider.autoDispose
.family<WalletTransactionsResponseEntity, TransactionsQuery>(
(ref, query) async {
final useCase = ref.read(getWalletTransactionsUseCaseProvider);
final filtersJson = jsonEncode({
'createdDate': {
'gte': query.dateFilter.startDate.toIso8601String(),
},
});
final filtersBase64 = base64Encode(utf8.encode(filtersJson));
return useCase(
walletId: query.walletId,
queryParameters: {
'filters': filtersBase64,
},
);
});

View File

@@ -32,7 +32,7 @@ class TransactionTile extends ConsumerWidget {
const SizedBox(width: 12),
Expanded(
child: Text(
transaction.name.isNotEmpty ? transaction.name : label,
label,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
@@ -50,10 +50,10 @@ class TransactionTile extends ConsumerWidget {
),
],
),
if (transaction.description.isNotEmpty) ...[
if (transaction.name.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
transaction.description,
transaction.name,
style: TextStyle(
fontSize: 14,
color: theme.getColorFor(ThemeCode.textPrimary),
@@ -73,60 +73,83 @@ class TransactionTile extends ConsumerWidget {
);
}
static IconData _icon(TransactionType type) {
switch (type) {
case TransactionType.payin:
return Icons.arrow_downward;
case TransactionType.payinrefund:
return Icons.replay;
case TransactionType.payout:
return Icons.arrow_upward;
case TransactionType.payoutrefund:
return Icons.replay;
case TransactionType.transfer:
return Icons.swap_horiz;
case TransactionType.cardtransaction:
return Icons.credit_card;
case TransactionType.unknown:
return Icons.help_outline;
}
}
static IconData _icon(TransactionType type) => switch (type) {
TransactionType.payin ||
TransactionType.payinAcquiring ||
TransactionType.checkPayin => Icons.arrow_downward,
TransactionType.payout ||
TransactionType.payoutSctInstantEmit => Icons.arrow_upward,
TransactionType.payinRefund ||
TransactionType.payoutRefund ||
TransactionType.payinRefundAcquiring ||
TransactionType.checkRefund => Icons.replay,
TransactionType.transfer ||
TransactionType.sctr ||
TransactionType.sctrInst ||
TransactionType.creditInternationalTransfer => Icons.swap_horiz,
TransactionType.cardTransaction => Icons.credit_card,
TransactionType.sdde || TransactionType.sddr => Icons.account_balance,
TransactionType.creditTransferReturned ||
TransactionType.payinSctInstantRecall ||
TransactionType.payinSctInstantEmitRecall ||
TransactionType.sctrRecall ||
TransactionType.sddrReversal => Icons.undo,
TransactionType.unknown => Icons.help_outline,
};
static Color _color(TransactionType type) {
switch (type) {
case TransactionType.payin:
return Colors.green;
case TransactionType.payinrefund:
return Colors.orange;
case TransactionType.payout:
return Colors.red;
case TransactionType.payoutrefund:
return Colors.orange;
case TransactionType.transfer:
return Colors.blue;
case TransactionType.cardtransaction:
return Colors.purple;
case TransactionType.unknown:
return Colors.grey;
}
}
static Color _color(TransactionType type) => switch (type) {
TransactionType.payin ||
TransactionType.payinAcquiring ||
TransactionType.checkPayin => Colors.green,
TransactionType.payout ||
TransactionType.payoutSctInstantEmit => Colors.red,
TransactionType.payinRefund ||
TransactionType.payoutRefund ||
TransactionType.payinRefundAcquiring ||
TransactionType.checkRefund ||
TransactionType.creditTransferReturned ||
TransactionType.payinSctInstantRecall ||
TransactionType.payinSctInstantEmitRecall ||
TransactionType.sctrRecall ||
TransactionType.sddrReversal => Colors.orange,
TransactionType.transfer ||
TransactionType.sctr ||
TransactionType.sctrInst ||
TransactionType.creditInternationalTransfer ||
TransactionType.sdde ||
TransactionType.sddr => Colors.blue,
TransactionType.cardTransaction => Colors.purple,
TransactionType.unknown => Colors.grey,
};
static String _i18nKey(TransactionType type) {
switch (type) {
case TransactionType.payin:
return I18n.transactionPayin;
case TransactionType.payinrefund:
return I18n.transactionPayinRefund;
case TransactionType.payout:
return I18n.transactionPayout;
case TransactionType.payoutrefund:
return I18n.transactionPayoutRefund;
case TransactionType.transfer:
return I18n.transactionTransfer;
case TransactionType.cardtransaction:
return I18n.transactionCardPayment;
case TransactionType.unknown:
return I18n.transactionUnknown;
}
}
static String _i18nKey(TransactionType type) => switch (type) {
TransactionType.payin => I18n.transactionPayin,
TransactionType.payout => I18n.transactionPayout,
TransactionType.transfer => I18n.transactionTransfer,
TransactionType.payinRefund => I18n.transactionPayinRefund,
TransactionType.payoutRefund => I18n.transactionPayoutRefund,
TransactionType.cardTransaction => I18n.transactionCardPayment,
TransactionType.payinAcquiring => I18n.transactionPayinAcquiring,
TransactionType.payinRefundAcquiring =>
I18n.transactionPayinRefundAcquiring,
TransactionType.sctrInst => I18n.transactionSctrInst,
TransactionType.payinSctInstantRecall =>
I18n.transactionPayinSctInstantRecall,
TransactionType.payoutSctInstantEmit =>
I18n.transactionPayoutSctInstantEmit,
TransactionType.payinSctInstantEmitRecall =>
I18n.transactionPayinSctInstantEmitRecall,
TransactionType.creditTransferReturned =>
I18n.transactionCreditTransferReturned,
TransactionType.checkPayin => I18n.transactionCheckPayin,
TransactionType.sdde => I18n.transactionSdde,
TransactionType.sddr => I18n.transactionSddr,
TransactionType.sddrReversal => I18n.transactionSddrReversal,
TransactionType.sctrRecall => I18n.transactionSctrRecall,
TransactionType.checkRefund => I18n.transactionCheckRefund,
TransactionType.sctr => I18n.transactionSctr,
TransactionType.creditInternationalTransfer =>
I18n.transactionCreditInternationalTransfer,
TransactionType.unknown => I18n.transactionUnknown,
};
}

View File

@@ -5,6 +5,7 @@ import 'package:get_it/get_it.dart';
import 'package:sca_treezor/sca_treezor.dart';
import 'package:sf_shared/sf_shared.dart';
import 'child_data_provider.dart';
import 'child_wallet_view_state.dart';
@@ -61,13 +62,13 @@ class ChildWalletViewModel extends Notifier<ChildWalletViewState> {
Future<void> _loadTransactions(String walletId) async {
state = state.copyWith(isLoadingTransactions: true);
try {
final response = await ref
.read(treezorRepositoryProvider)
.getWalletTransactions(walletId: walletId);
final query = TransactionsQuery(walletId: walletId);
final transactions =
await ref.read(walletTransactionsProvider(query).future);
if (!ref.mounted) return;
state = state.copyWith(
isLoadingTransactions: false,
transactions: response.items,
transactions: transactions,
);
} catch (_) {
if (!ref.mounted) return;

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import '../../presentation/state/home_view_model.dart';
import '../child_wallet/child_data_provider.dart';
import 'deposit_view_state.dart';
@@ -100,6 +101,8 @@ class DepositViewModel extends Notifier<DepositViewState> {
if (!ref.mounted) return;
ref.read(childDataProvider(childId).notifier).load();
ref.read(homeViewModelProvider.notifier).refreshChildWallet(childId);
ref.read(parentWalletBalanceProvider.notifier).refresh();
state = state.copyWith(isSubmitting: false, success: true);
} catch (e) {
if (!ref.mounted) return;

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import '../../presentation/state/home_view_model.dart';
import '../child_wallet/child_data_provider.dart';
import 'extract_view_state.dart';
@@ -92,6 +93,8 @@ class ExtractViewModel extends Notifier<ExtractViewState> {
if (!ref.mounted) return;
ref.read(childDataProvider(childId).notifier).load();
ref.read(homeViewModelProvider.notifier).refreshChildWallet(childId);
ref.read(parentWalletBalanceProvider.notifier).refresh();
state = state.copyWith(isSubmitting: false, success: true);
} catch (e) {
if (!ref.mounted) return;

View File

@@ -0,0 +1,100 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:home/src/presentation/wallet_item.dart';
import 'package:home/src/presentation/state/home_view_model.dart';
class ChildWalletsSlider extends ConsumerStatefulWidget {
const ChildWalletsSlider({super.key});
@override
ConsumerState<ChildWalletsSlider> createState() => _ChildWalletsSliderState();
}
class _ChildWalletsSliderState extends ConsumerState<ChildWalletsSlider> {
final PageController _pageController = PageController(viewportFraction: 0.92);
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final viewState = ref.watch(homeViewModelProvider);
final viewModel = ref.read(homeViewModelProvider.notifier);
final children = viewState.children;
if (children.isEmpty) return const SizedBox.shrink();
return Column(
children: [
SizedBox(
height: WalletItem.cardHeight,
child: PageView.builder(
controller: _pageController,
itemCount: children.length,
onPageChanged: viewModel.onPageChanged,
itemBuilder: (context, index) {
final child = children[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: WalletItem(
childProfile: child,
childWallet: viewState.childWallets[child.id],
device: viewState.childDevices[child.id],
index: index,
),
);
},
),
),
if (children.length > 1) ...[
const SizedBox(height: 12),
_DotsIndicator(
currentPage: viewState.currentPage,
count: children.length,
),
],
],
);
}
}
class _DotsIndicator extends ConsumerWidget {
final int currentPage;
final int count;
const _DotsIndicator({
required this.currentPage,
required this.count,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
return Semantics(
label: 'Page ${currentPage + 1} of $count',
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(count, (index) {
final isActive = index == currentPage;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 4),
height: 8,
width: isActive ? 24 : 8,
decoration: BoxDecoration(
color: isActive
? theme.getColorFor(ThemeCode.buttonPrimary)
: theme.getColorFor(ThemeCode.textPrimary).withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(4),
),
);
}),
),
);
}
}

View File

@@ -1,7 +1,7 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:home/src/presentation/wallet_item.dart';
import 'package:home/src/presentation/child_wallets_slider.dart';
import 'package:home/src/presentation/state/home_view_model.dart';
import 'package:payments/payments.dart';
import 'package:sf_shared/sf_shared.dart';
@@ -26,7 +26,9 @@ class HomeScreen extends ConsumerWidget {
}
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
final balance = viewState.walletBalance;
final balance = ref.watch(parentWalletBalanceProvider);
final accountTotalBalance =
ref.watch(accountTotalBalanceProvider).value ?? balance?.totalBalance ?? 0;
return SafeArea(
child: SingleChildScrollView(
@@ -62,7 +64,7 @@ class HomeScreen extends ConsumerWidget {
),
],
),
_buildWalletsList(context, viewState.children, viewState.childWallets, viewState.childDevices, ref),
const ChildWalletsSlider(),
Align(
alignment: Alignment.topLeft,
child: TextButton(
@@ -79,12 +81,13 @@ class HomeScreen extends ConsumerWidget {
),
if (balance != null)
WalletBalanceBlock(
value: balance.availableBalance,
savings: balance.allocatedBalance,
availableBalance: balance.availableBalance,
allocatedBalance: balance.allocatedBalance,
totalBalance: balance.totalBalance,
),
if (balance != null)
DepositBlock(
max: 150 - balance.totalBalance,
max: 150 - accountTotalBalance,
onDeposit: (amount) async {
final result = await showPayinBottomSheet(
context,
@@ -93,8 +96,10 @@ class HomeScreen extends ConsumerWidget {
);
if (result == true && context.mounted) {
showTopSnackbar(context, message: context.translate(I18n.payinSuccess), type: MessageType.success);
ref.read(homeViewModelProvider.notifier).retry();
ref.read(parentWalletBalanceProvider.notifier).applyOptimisticPayin(amount);
return true;
}
return false;
},
),
],
@@ -104,24 +109,8 @@ class HomeScreen extends ConsumerWidget {
);
}
Widget _buildWalletsList(BuildContext context, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, WidgetRef ref) {
return Column(
spacing: 20,
children: List<Widget>.generate(children.length, (int index) {
final child = children[index];
return WalletItem(
childProfile: child,
childWallet: childWallets[child.id],
device: childDevices[child.id],
index: index,
);
}),
);
}
Widget _buildError(BuildContext context, WidgetRef ref, ThemePort theme, String message) {
final viewModel = ref.read(homeViewModelProvider.notifier);
return Center(
child: Padding(
padding: const EdgeInsets.all(24),

View File

@@ -10,13 +10,18 @@ final homeViewModelProvider =
);
class HomeViewModel extends Notifier<HomeViewState> {
late final GetWalletBalanceUseCase _getBalanceUseCase;
int _currentPage = 0;
@override
HomeViewState build() {
_getBalanceUseCase = ref.read(getWalletBalanceUseCaseProvider);
ref.watch(walletRefreshProvider);
Future.microtask(() => load());
return const HomeViewState(isLoading: true);
return HomeViewState(isLoading: true, currentPage: _currentPage);
}
void onPageChanged(int index) {
_currentPage = index;
state = state.copyWith(currentPage: index);
}
Future<void> load() async {
@@ -43,11 +48,12 @@ class HomeViewModel extends Notifier<HomeViewState> {
if (!ref.mounted) return;
// Fetch parent wallet balance
WalletBalanceEntity? parentBalance;
// Load parent wallet balance via the shared notifier
final parentWalletId = paymentProfile.paymentWalletId;
if (parentWalletId != null && parentWalletId.isNotEmpty) {
parentBalance = await _getBalanceUseCase(walletId: parentWalletId);
await ref
.read(parentWalletBalanceProvider.notifier)
.load(parentWalletId);
}
if (!ref.mounted) return;
@@ -73,13 +79,18 @@ class HomeViewModel extends Notifier<HomeViewState> {
if (!ref.mounted) return;
final clampedPage = children.isEmpty
? 0
: _currentPage.clamp(0, children.length - 1);
_currentPage = clampedPage;
state = state.copyWith(
isLoading: false,
userName: name,
children: children,
childWallets: childWallets,
childDevices: childDevices,
walletBalance: parentBalance,
currentPage: clampedPage,
);
} catch (e) {
if (!ref.mounted) return;
@@ -87,7 +98,22 @@ class HomeViewModel extends Notifier<HomeViewState> {
}
}
Future<void> retry() async {
await load();
void retry() {
ref.read(walletRefreshProvider.notifier).refresh();
}
Future<void> refreshChildWallet(String childId) async {
final child = state.children.where((c) => c.id == childId).firstOrNull;
if (child == null || !ref.mounted) return;
try {
final treezorRepo = ref.read(treezorRepositoryProvider);
final freshChild =
await treezorRepo.getChildWallet(walletId: child.walletId);
if (!ref.mounted) return;
final updated =
Map<String, ChildWalletEntity>.from(state.childWallets);
updated[childId] = freshChild;
state = state.copyWith(childWallets: updated);
} catch (_) {}
}
}

View File

@@ -11,7 +11,7 @@ abstract class HomeViewState with _$HomeViewState {
@Default([]) List<ChildProfileEntity> children,
@Default({}) Map<String, ChildWalletEntity> childWallets,
@Default({}) Map<String, DeviceEntity> childDevices,
WalletBalanceEntity? walletBalance,
@Default('') String errorMessage,
@Default(0) int currentPage,
}) = _HomeViewState;
}

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$HomeViewState {
bool get isLoading; String get userName; List<ChildProfileEntity> get children; Map<String, ChildWalletEntity> get childWallets; Map<String, DeviceEntity> get childDevices; WalletBalanceEntity? get walletBalance; String get errorMessage;
bool get isLoading; String get userName; List<ChildProfileEntity> get children; Map<String, ChildWalletEntity> get childWallets; Map<String, DeviceEntity> get childDevices; String get errorMessage; int get currentPage;
/// Create a copy of HomeViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $HomeViewStateCopyWith<HomeViewState> get copyWith => _$HomeViewStateCopyWithImp
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is HomeViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.userName, userName) || other.userName == userName)&&const DeepCollectionEquality().equals(other.children, children)&&const DeepCollectionEquality().equals(other.childWallets, childWallets)&&const DeepCollectionEquality().equals(other.childDevices, childDevices)&&(identical(other.walletBalance, walletBalance) || other.walletBalance == walletBalance)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is HomeViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.userName, userName) || other.userName == userName)&&const DeepCollectionEquality().equals(other.children, children)&&const DeepCollectionEquality().equals(other.childWallets, childWallets)&&const DeepCollectionEquality().equals(other.childDevices, childDevices)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,userName,const DeepCollectionEquality().hash(children),const DeepCollectionEquality().hash(childWallets),const DeepCollectionEquality().hash(childDevices),walletBalance,errorMessage);
int get hashCode => Object.hash(runtimeType,isLoading,userName,const DeepCollectionEquality().hash(children),const DeepCollectionEquality().hash(childWallets),const DeepCollectionEquality().hash(childDevices),errorMessage,currentPage);
@override
String toString() {
return 'HomeViewState(isLoading: $isLoading, userName: $userName, children: $children, childWallets: $childWallets, childDevices: $childDevices, walletBalance: $walletBalance, errorMessage: $errorMessage)';
return 'HomeViewState(isLoading: $isLoading, userName: $userName, children: $children, childWallets: $childWallets, childDevices: $childDevices, errorMessage: $errorMessage, currentPage: $currentPage)';
}
@@ -45,11 +45,11 @@ abstract mixin class $HomeViewStateCopyWith<$Res> {
factory $HomeViewStateCopyWith(HomeViewState value, $Res Function(HomeViewState) _then) = _$HomeViewStateCopyWithImpl;
@useResult
$Res call({
bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, WalletBalanceEntity? walletBalance, String errorMessage
bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, String errorMessage, int currentPage
});
$WalletBalanceEntityCopyWith<$Res>? get walletBalance;
}
/// @nodoc
@@ -62,31 +62,19 @@ class _$HomeViewStateCopyWithImpl<$Res>
/// Create a copy of HomeViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? userName = null,Object? children = null,Object? childWallets = null,Object? childDevices = null,Object? walletBalance = freezed,Object? errorMessage = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? userName = null,Object? children = null,Object? childWallets = null,Object? childDevices = null,Object? errorMessage = null,Object? currentPage = null,}) {
return _then(_self.copyWith(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable
as String,children: null == children ? _self.children : children // ignore: cast_nullable_to_non_nullable
as List<ChildProfileEntity>,childWallets: null == childWallets ? _self.childWallets : childWallets // ignore: cast_nullable_to_non_nullable
as Map<String, ChildWalletEntity>,childDevices: null == childDevices ? _self.childDevices : childDevices // ignore: cast_nullable_to_non_nullable
as Map<String, DeviceEntity>,walletBalance: freezed == walletBalance ? _self.walletBalance : walletBalance // ignore: cast_nullable_to_non_nullable
as WalletBalanceEntity?,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
as Map<String, DeviceEntity>,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,currentPage: null == currentPage ? _self.currentPage : currentPage // ignore: cast_nullable_to_non_nullable
as int,
));
}
/// Create a copy of HomeViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$WalletBalanceEntityCopyWith<$Res>? get walletBalance {
if (_self.walletBalance == null) {
return null;
}
return $WalletBalanceEntityCopyWith<$Res>(_self.walletBalance!, (value) {
return _then(_self.copyWith(walletBalance: value));
});
}
}
@@ -168,10 +156,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, WalletBalanceEntity? walletBalance, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, String errorMessage, int currentPage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _HomeViewState() when $default != null:
return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets,_that.childDevices,_that.walletBalance,_that.errorMessage);case _:
return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets,_that.childDevices,_that.errorMessage,_that.currentPage);case _:
return orElse();
}
@@ -189,10 +177,10 @@ return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, WalletBalanceEntity? walletBalance, String errorMessage) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, String errorMessage, int currentPage) $default,) {final _that = this;
switch (_that) {
case _HomeViewState():
return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets,_that.childDevices,_that.walletBalance,_that.errorMessage);case _:
return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets,_that.childDevices,_that.errorMessage,_that.currentPage);case _:
throw StateError('Unexpected subclass');
}
@@ -209,10 +197,10 @@ return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, WalletBalanceEntity? walletBalance, String errorMessage)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, String errorMessage, int currentPage)? $default,) {final _that = this;
switch (_that) {
case _HomeViewState() when $default != null:
return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets,_that.childDevices,_that.walletBalance,_that.errorMessage);case _:
return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets,_that.childDevices,_that.errorMessage,_that.currentPage);case _:
return null;
}
@@ -224,7 +212,7 @@ return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets
class _HomeViewState implements HomeViewState {
const _HomeViewState({this.isLoading = false, this.userName = '', final List<ChildProfileEntity> children = const [], final Map<String, ChildWalletEntity> childWallets = const {}, final Map<String, DeviceEntity> childDevices = const {}, this.walletBalance, this.errorMessage = ''}): _children = children,_childWallets = childWallets,_childDevices = childDevices;
const _HomeViewState({this.isLoading = false, this.userName = '', final List<ChildProfileEntity> children = const [], final Map<String, ChildWalletEntity> childWallets = const {}, final Map<String, DeviceEntity> childDevices = const {}, this.errorMessage = '', this.currentPage = 0}): _children = children,_childWallets = childWallets,_childDevices = childDevices;
@override@JsonKey() final bool isLoading;
@@ -250,8 +238,8 @@ class _HomeViewState implements HomeViewState {
return EqualUnmodifiableMapView(_childDevices);
}
@override final WalletBalanceEntity? walletBalance;
@override@JsonKey() final String errorMessage;
@override@JsonKey() final int currentPage;
/// Create a copy of HomeViewState
/// with the given fields replaced by the non-null parameter values.
@@ -263,16 +251,16 @@ _$HomeViewStateCopyWith<_HomeViewState> get copyWith => __$HomeViewStateCopyWith
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HomeViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.userName, userName) || other.userName == userName)&&const DeepCollectionEquality().equals(other._children, _children)&&const DeepCollectionEquality().equals(other._childWallets, _childWallets)&&const DeepCollectionEquality().equals(other._childDevices, _childDevices)&&(identical(other.walletBalance, walletBalance) || other.walletBalance == walletBalance)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HomeViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.userName, userName) || other.userName == userName)&&const DeepCollectionEquality().equals(other._children, _children)&&const DeepCollectionEquality().equals(other._childWallets, _childWallets)&&const DeepCollectionEquality().equals(other._childDevices, _childDevices)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,userName,const DeepCollectionEquality().hash(_children),const DeepCollectionEquality().hash(_childWallets),const DeepCollectionEquality().hash(_childDevices),walletBalance,errorMessage);
int get hashCode => Object.hash(runtimeType,isLoading,userName,const DeepCollectionEquality().hash(_children),const DeepCollectionEquality().hash(_childWallets),const DeepCollectionEquality().hash(_childDevices),errorMessage,currentPage);
@override
String toString() {
return 'HomeViewState(isLoading: $isLoading, userName: $userName, children: $children, childWallets: $childWallets, childDevices: $childDevices, walletBalance: $walletBalance, errorMessage: $errorMessage)';
return 'HomeViewState(isLoading: $isLoading, userName: $userName, children: $children, childWallets: $childWallets, childDevices: $childDevices, errorMessage: $errorMessage, currentPage: $currentPage)';
}
@@ -283,11 +271,11 @@ abstract mixin class _$HomeViewStateCopyWith<$Res> implements $HomeViewStateCopy
factory _$HomeViewStateCopyWith(_HomeViewState value, $Res Function(_HomeViewState) _then) = __$HomeViewStateCopyWithImpl;
@override @useResult
$Res call({
bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, WalletBalanceEntity? walletBalance, String errorMessage
bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, String errorMessage, int currentPage
});
@override $WalletBalanceEntityCopyWith<$Res>? get walletBalance;
}
/// @nodoc
@@ -300,32 +288,20 @@ class __$HomeViewStateCopyWithImpl<$Res>
/// Create a copy of HomeViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? userName = null,Object? children = null,Object? childWallets = null,Object? childDevices = null,Object? walletBalance = freezed,Object? errorMessage = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? userName = null,Object? children = null,Object? childWallets = null,Object? childDevices = null,Object? errorMessage = null,Object? currentPage = null,}) {
return _then(_HomeViewState(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable
as String,children: null == children ? _self._children : children // ignore: cast_nullable_to_non_nullable
as List<ChildProfileEntity>,childWallets: null == childWallets ? _self._childWallets : childWallets // ignore: cast_nullable_to_non_nullable
as Map<String, ChildWalletEntity>,childDevices: null == childDevices ? _self._childDevices : childDevices // ignore: cast_nullable_to_non_nullable
as Map<String, DeviceEntity>,walletBalance: freezed == walletBalance ? _self.walletBalance : walletBalance // ignore: cast_nullable_to_non_nullable
as WalletBalanceEntity?,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
as Map<String, DeviceEntity>,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,currentPage: null == currentPage ? _self.currentPage : currentPage // ignore: cast_nullable_to_non_nullable
as int,
));
}
/// Create a copy of HomeViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$WalletBalanceEntityCopyWith<$Res>? get walletBalance {
if (_self.walletBalance == null) {
return null;
}
return $WalletBalanceEntityCopyWith<$Res>(_self.walletBalance!, (value) {
return _then(_self.copyWith(walletBalance: value));
});
}
}
// dart format on

View File

@@ -9,6 +9,8 @@ import 'package:sf_shared/sf_shared.dart';
import 'package:sf_localizations/sf_localizations.dart';
class WalletItem extends ConsumerWidget {
static const double cardHeight = 227;
final ChildProfileEntity childProfile;
final ChildWalletEntity? childWallet;
final DeviceEntity? device;
@@ -37,13 +39,11 @@ class WalletItem extends ConsumerWidget {
AppRoutes.childWallet(childProfile.id),
),
child: SizedBox(
height: 227,
width: 382,
height: cardHeight,
width: double.infinity,
child: Stack(
children: [
SizedBox(
height: 227,
width: 382,
Positioned.fill(
child: CustomPaint(painter: WalletPainter(_cardColors(theme))),
),
Column(

View File

@@ -435,7 +435,7 @@ class ProfileSettingsScreen extends ConsumerWidget {
style: const ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
onPressed: () {},
onPressed: () => _logout(context, ref),
child: Row(
spacing: 4,
children: [

View File

@@ -9,36 +9,31 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:profile/src/presentation/state/profile_view_model.dart';
import 'package:profile/src/presentation/state/profile_view_state.dart';
class ProfileScreen extends ConsumerWidget {
class ProfileScreen extends ConsumerStatefulWidget {
final NavigationContract navigationContract;
const ProfileScreen({super.key, required this.navigationContract});
List<num> _expensesByDay(List<WalletTransactionEntity> transactions) {
final result = List<num>.filled(7, 0);
for (final tx in transactions) {
final date = DateTime.tryParse(tx.createdDate);
if (date == null) continue;
final amount = double.tryParse(tx.amount) ?? 0;
result[date.weekday - 1] += amount.abs();
}
return result;
}
@override
ConsumerState<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends ConsumerState<ProfileScreen> {
NavigationContract get navigationContract => widget.navigationContract;
@override
Widget build(BuildContext context, WidgetRef ref) {
Widget build(BuildContext context) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(profileViewModelProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
body: _buildBody(context, ref, theme, viewState),
body: _buildBody(context, theme, viewState),
);
}
Widget _buildBody(
BuildContext context,
WidgetRef ref,
ThemePort theme,
ProfileViewState viewState,
) {
@@ -47,12 +42,14 @@ class ProfileScreen extends ConsumerWidget {
}
if (viewState.errorMessage.isNotEmpty) {
return _buildError(context, ref, theme, viewState.errorMessage);
return _buildError(context, theme, viewState.errorMessage);
}
final balance = viewState.walletBalance;
final balance = ref.watch(parentWalletBalanceProvider);
final accountTotalBalance =
ref.watch(accountTotalBalanceProvider).value ?? balance?.totalBalance ?? 0;
final content = <Widget>[
final header = <Widget>[
Row(
children: [
Text(
@@ -78,16 +75,13 @@ class ProfileScreen extends ConsumerWidget {
),
if (balance != null)
WalletBalanceBlock(
value: balance.availableBalance,
savings: balance.allocatedBalance,
availableBalance: balance.availableBalance,
allocatedBalance: balance.allocatedBalance,
totalBalance: balance.totalBalance,
),
LineGraph(
lines: [_expensesByDay(viewState.transactions)],
lineLabels: [context.translate(I18n.profileMyAccount)],
),
if (balance != null)
DepositBlock(
max: 150 - balance.totalBalance,
max: 150 - accountTotalBalance,
onDeposit: (amount) async {
final result = await showPayinBottomSheet(
context,
@@ -100,8 +94,10 @@ class ProfileScreen extends ConsumerWidget {
message: context.translate(I18n.payinSuccess),
type: MessageType.success,
);
ref.read(profileViewModelProvider.notifier).load();
ref.read(parentWalletBalanceProvider.notifier).applyOptimisticPayin(amount);
return true;
}
return false;
},
),
Container(
@@ -133,9 +129,15 @@ class ProfileScreen extends ConsumerWidget {
),
),
),
_buildTransactions(context, theme, viewState),
];
final transactions = viewState.transactions;
final hasTransactions = transactions.isNotEmpty;
final itemCount = header.length
+ 1 // transactions title or empty message
+ (hasTransactions ? transactions.length : 0);
return Stack(
children: [
DecoratedBox(
@@ -158,13 +160,43 @@ class ProfileScreen extends ConsumerWidget {
ref.read(profileViewModelProvider.notifier).load(),
child: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return content[index];
itemCount: itemCount,
separatorBuilder: (_, __) =>
const Divider(color: Colors.transparent, height: 20),
itemBuilder: (context, index) {
// Header items
if (index < header.length) {
return header[index];
}
final offset = index - header.length;
// Transactions title or empty message
if (offset == 0) {
if (!hasTransactions) {
return Center(
child: Text(
context.translate(I18n.profileNoRecentTransactions),
style: TextStyle(
fontSize: 14,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
);
}
return Text(
context.translate(I18n.profileRecentTransactions),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 18,
color: theme.getColorFor(ThemeCode.textPrimary),
),
);
}
final txIndex = offset - 1;
return TransactionTile(transaction: transactions[txIndex]);
},
separatorBuilder: (BuildContext context, int index) {
return Divider(color: Colors.transparent, height: 20);
},
itemCount: content.length,
),
),
),
@@ -175,55 +207,11 @@ class ProfileScreen extends ConsumerWidget {
);
}
Widget _buildTransactions(
BuildContext context,
ThemePort theme,
ProfileViewState viewState,
) {
final transactions = viewState.transactions;
if (transactions.isEmpty) {
return Center(
child: Text(
context.translate(I18n.profileNoRecentTransactions),
style: TextStyle(
fontSize: 14,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.translate(I18n.profileRecentTransactions),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 18,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
const SizedBox(height: 12),
...transactions.map(
(transaction) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TransactionTile(transaction: transaction),
),
),
],
);
}
Widget _buildError(
BuildContext context,
WidgetRef ref,
ThemePort theme,
String message,
) {
final viewModel = ref.read(profileViewModelProvider.notifier);
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
@@ -246,7 +234,8 @@ class ProfileScreen extends ConsumerWidget {
),
const SizedBox(height: 16),
TextButton(
onPressed: () => viewModel.retry(),
onPressed: () =>
ref.read(profileViewModelProvider.notifier).retry(),
child: Text(context.translate(I18n.retry)),
),
],

View File

@@ -1,8 +1,5 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:activity/activity.dart';
import 'profile_view_state.dart';
final profileViewModelProvider =
@@ -11,18 +8,9 @@ final profileViewModelProvider =
);
class ProfileViewModel extends Notifier<ProfileViewState> {
late final GetWalletBalanceUseCase _getBalanceUseCase;
late final GetWalletTransactionsUseCase _getTransactionsUseCase;
late final GetPaymentProfileUseCase _getPaymentProfileUseCase;
late final GetUserInfoUseCase _getUserInfoUseCase;
@override
ProfileViewState build() {
_getBalanceUseCase = ref.read(getWalletBalanceUseCaseProvider);
_getTransactionsUseCase = ref.read(getWalletTransactionsUseCaseProvider);
_getPaymentProfileUseCase = ref.read(getPaymentProfileUseCaseProvider);
_getUserInfoUseCase = ref.read(getUserInfoUseCaseProvider);
ref.watch(walletRefreshProvider);
Future.microtask(() => load());
return const ProfileViewState(isLoading: true);
}
@@ -31,7 +19,7 @@ class ProfileViewModel extends Notifier<ProfileViewState> {
state = state.copyWith(isLoading: true, errorMessage: '');
try {
final user = await _getUserInfoUseCase.getUserInfo();
final user = await ref.read(getUserInfoUseCaseProvider).getUserInfo();
if (!ref.mounted) return;
final rawName = user.firstName;
@@ -39,7 +27,7 @@ class ProfileViewModel extends Notifier<ProfileViewState> {
? ''
: rawName[0].toUpperCase() + rawName.substring(1).toLowerCase();
final paymentProfile = await _getPaymentProfileUseCase.getPaymentProfile(
final paymentProfile = await ref.read(getPaymentProfileUseCaseProvider).getPaymentProfile(
userId: user.id,
);
if (!ref.mounted) return;
@@ -50,18 +38,21 @@ class ProfileViewModel extends Notifier<ProfileViewState> {
return;
}
final results = await Future.wait([
_getBalanceUseCase(walletId: walletId),
_loadTransactions(walletId),
]);
final query = TransactionsQuery(
walletId: walletId,
dateFilter: DateFilter.lastWeek,
);
final transactions = await ref
.read(walletTransactionsProvider(query).future);
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
userName: name,
walletBalance: results[0] as WalletBalanceEntity,
transactions: results[1] as List<WalletTransactionEntity>,
walletId: walletId,
transactions: transactions,
);
} catch (e) {
if (!ref.mounted) return;
@@ -69,24 +60,6 @@ class ProfileViewModel extends Notifier<ProfileViewState> {
}
}
Future<List<WalletTransactionEntity>> _loadTransactions(
String walletId,
) async {
final filtersJson = jsonEncode({
'createdDate': {
'gte': DateFilter.lastWeek.startDate.toIso8601String(),
},
});
final filtersBase64 = base64Encode(utf8.encode(filtersJson));
final response = await _getTransactionsUseCase(
walletId: walletId,
queryParameters: {'filters': filtersBase64},
);
return response.items;
}
Future<void> retry() async {
await load();
}

View File

@@ -8,7 +8,7 @@ abstract class ProfileViewState with _$ProfileViewState {
const factory ProfileViewState({
@Default(false) bool isLoading,
@Default('') String userName,
WalletBalanceEntity? walletBalance,
@Default('') String walletId,
@Default([]) List<WalletTransactionEntity> transactions,
@Default('') String errorMessage,
}) = _ProfileViewState;

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ProfileViewState {
bool get isLoading; String get userName; WalletBalanceEntity? get walletBalance; List<WalletTransactionEntity> get transactions; String get errorMessage;
bool get isLoading; String get userName; String get walletId; List<WalletTransactionEntity> get transactions; String get errorMessage;
/// Create a copy of ProfileViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $ProfileViewStateCopyWith<ProfileViewState> get copyWith => _$ProfileViewStateCo
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProfileViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.userName, userName) || other.userName == userName)&&(identical(other.walletBalance, walletBalance) || other.walletBalance == walletBalance)&&const DeepCollectionEquality().equals(other.transactions, transactions)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProfileViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.userName, userName) || other.userName == userName)&&(identical(other.walletId, walletId) || other.walletId == walletId)&&const DeepCollectionEquality().equals(other.transactions, transactions)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,userName,walletBalance,const DeepCollectionEquality().hash(transactions),errorMessage);
int get hashCode => Object.hash(runtimeType,isLoading,userName,walletId,const DeepCollectionEquality().hash(transactions),errorMessage);
@override
String toString() {
return 'ProfileViewState(isLoading: $isLoading, userName: $userName, walletBalance: $walletBalance, transactions: $transactions, errorMessage: $errorMessage)';
return 'ProfileViewState(isLoading: $isLoading, userName: $userName, walletId: $walletId, transactions: $transactions, errorMessage: $errorMessage)';
}
@@ -45,11 +45,11 @@ abstract mixin class $ProfileViewStateCopyWith<$Res> {
factory $ProfileViewStateCopyWith(ProfileViewState value, $Res Function(ProfileViewState) _then) = _$ProfileViewStateCopyWithImpl;
@useResult
$Res call({
bool isLoading, String userName, WalletBalanceEntity? walletBalance, List<WalletTransactionEntity> transactions, String errorMessage
bool isLoading, String userName, String walletId, List<WalletTransactionEntity> transactions, String errorMessage
});
$WalletBalanceEntityCopyWith<$Res>? get walletBalance;
}
/// @nodoc
@@ -62,29 +62,17 @@ class _$ProfileViewStateCopyWithImpl<$Res>
/// Create a copy of ProfileViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? userName = null,Object? walletBalance = freezed,Object? transactions = null,Object? errorMessage = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? userName = null,Object? walletId = null,Object? transactions = null,Object? errorMessage = null,}) {
return _then(_self.copyWith(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable
as String,walletBalance: freezed == walletBalance ? _self.walletBalance : walletBalance // ignore: cast_nullable_to_non_nullable
as WalletBalanceEntity?,transactions: null == transactions ? _self.transactions : transactions // ignore: cast_nullable_to_non_nullable
as String,walletId: null == walletId ? _self.walletId : walletId // ignore: cast_nullable_to_non_nullable
as String,transactions: null == transactions ? _self.transactions : transactions // ignore: cast_nullable_to_non_nullable
as List<WalletTransactionEntity>,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of ProfileViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$WalletBalanceEntityCopyWith<$Res>? get walletBalance {
if (_self.walletBalance == null) {
return null;
}
return $WalletBalanceEntityCopyWith<$Res>(_self.walletBalance!, (value) {
return _then(_self.copyWith(walletBalance: value));
});
}
}
@@ -166,10 +154,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, String userName, WalletBalanceEntity? walletBalance, List<WalletTransactionEntity> transactions, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, String userName, String walletId, List<WalletTransactionEntity> transactions, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ProfileViewState() when $default != null:
return $default(_that.isLoading,_that.userName,_that.walletBalance,_that.transactions,_that.errorMessage);case _:
return $default(_that.isLoading,_that.userName,_that.walletId,_that.transactions,_that.errorMessage);case _:
return orElse();
}
@@ -187,10 +175,10 @@ return $default(_that.isLoading,_that.userName,_that.walletBalance,_that.transac
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, String userName, WalletBalanceEntity? walletBalance, List<WalletTransactionEntity> transactions, String errorMessage) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, String userName, String walletId, List<WalletTransactionEntity> transactions, String errorMessage) $default,) {final _that = this;
switch (_that) {
case _ProfileViewState():
return $default(_that.isLoading,_that.userName,_that.walletBalance,_that.transactions,_that.errorMessage);case _:
return $default(_that.isLoading,_that.userName,_that.walletId,_that.transactions,_that.errorMessage);case _:
throw StateError('Unexpected subclass');
}
@@ -207,10 +195,10 @@ return $default(_that.isLoading,_that.userName,_that.walletBalance,_that.transac
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, String userName, WalletBalanceEntity? walletBalance, List<WalletTransactionEntity> transactions, String errorMessage)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, String userName, String walletId, List<WalletTransactionEntity> transactions, String errorMessage)? $default,) {final _that = this;
switch (_that) {
case _ProfileViewState() when $default != null:
return $default(_that.isLoading,_that.userName,_that.walletBalance,_that.transactions,_that.errorMessage);case _:
return $default(_that.isLoading,_that.userName,_that.walletId,_that.transactions,_that.errorMessage);case _:
return null;
}
@@ -222,12 +210,12 @@ return $default(_that.isLoading,_that.userName,_that.walletBalance,_that.transac
class _ProfileViewState implements ProfileViewState {
const _ProfileViewState({this.isLoading = false, this.userName = '', this.walletBalance, final List<WalletTransactionEntity> transactions = const [], this.errorMessage = ''}): _transactions = transactions;
const _ProfileViewState({this.isLoading = false, this.userName = '', this.walletId = '', final List<WalletTransactionEntity> transactions = const [], this.errorMessage = ''}): _transactions = transactions;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final String userName;
@override final WalletBalanceEntity? walletBalance;
@override@JsonKey() final String walletId;
final List<WalletTransactionEntity> _transactions;
@override@JsonKey() List<WalletTransactionEntity> get transactions {
if (_transactions is EqualUnmodifiableListView) return _transactions;
@@ -247,16 +235,16 @@ _$ProfileViewStateCopyWith<_ProfileViewState> get copyWith => __$ProfileViewStat
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProfileViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.userName, userName) || other.userName == userName)&&(identical(other.walletBalance, walletBalance) || other.walletBalance == walletBalance)&&const DeepCollectionEquality().equals(other._transactions, _transactions)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProfileViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.userName, userName) || other.userName == userName)&&(identical(other.walletId, walletId) || other.walletId == walletId)&&const DeepCollectionEquality().equals(other._transactions, _transactions)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,userName,walletBalance,const DeepCollectionEquality().hash(_transactions),errorMessage);
int get hashCode => Object.hash(runtimeType,isLoading,userName,walletId,const DeepCollectionEquality().hash(_transactions),errorMessage);
@override
String toString() {
return 'ProfileViewState(isLoading: $isLoading, userName: $userName, walletBalance: $walletBalance, transactions: $transactions, errorMessage: $errorMessage)';
return 'ProfileViewState(isLoading: $isLoading, userName: $userName, walletId: $walletId, transactions: $transactions, errorMessage: $errorMessage)';
}
@@ -267,11 +255,11 @@ abstract mixin class _$ProfileViewStateCopyWith<$Res> implements $ProfileViewSta
factory _$ProfileViewStateCopyWith(_ProfileViewState value, $Res Function(_ProfileViewState) _then) = __$ProfileViewStateCopyWithImpl;
@override @useResult
$Res call({
bool isLoading, String userName, WalletBalanceEntity? walletBalance, List<WalletTransactionEntity> transactions, String errorMessage
bool isLoading, String userName, String walletId, List<WalletTransactionEntity> transactions, String errorMessage
});
@override $WalletBalanceEntityCopyWith<$Res>? get walletBalance;
}
/// @nodoc
@@ -284,30 +272,18 @@ class __$ProfileViewStateCopyWithImpl<$Res>
/// Create a copy of ProfileViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? userName = null,Object? walletBalance = freezed,Object? transactions = null,Object? errorMessage = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? userName = null,Object? walletId = null,Object? transactions = null,Object? errorMessage = null,}) {
return _then(_ProfileViewState(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable
as String,walletBalance: freezed == walletBalance ? _self.walletBalance : walletBalance // ignore: cast_nullable_to_non_nullable
as WalletBalanceEntity?,transactions: null == transactions ? _self._transactions : transactions // ignore: cast_nullable_to_non_nullable
as String,walletId: null == walletId ? _self.walletId : walletId // ignore: cast_nullable_to_non_nullable
as String,transactions: null == transactions ? _self._transactions : transactions // ignore: cast_nullable_to_non_nullable
as List<WalletTransactionEntity>,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of ProfileViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$WalletBalanceEntityCopyWith<$Res>? get walletBalance {
if (_self.walletBalance == null) {
return null;
}
return $WalletBalanceEntityCopyWith<$Res>(_self.walletBalance!, (value) {
return _then(_self.copyWith(walletBalance: value));
});
}
}
// dart format on

View File

@@ -1,27 +1,33 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sca_treezor/sca_treezor.dart'
show TreezorWalletConnectionService;
import 'package:sf_infrastructure/sf_infrastructure.dart';
// import 'package:sf_shared/sf_shared.dart' show treezorRepositoryProvider;
import 'package:sf_shared/sf_shared.dart' show treezorRepositoryProvider;
final logoutProvider = FutureProvider.autoDispose<void>((ref) async {
final repository = getIt<QuestiaRepository>();
// final treezorRepository = ref.read(treezorRepositoryProvider);
// // Delete SCA wallet on the server before clearing local data
// try {
// final cachedWallets = await treezorRepository.scaWallets();
// final scaWalletId = cachedWallets.item.id;
// if (scaWalletId.isNotEmpty) {
// await treezorRepository.deleteScaWallet(scaWalletId: scaWalletId);
// }
// } catch (_) {
// // Don't block logout if SCA wallet deletion fails
// }
// 1. Logout from Treezor SDK (disconnect active wallet session)
try {
final connectionService = getIt<TreezorWalletConnectionService>();
await connectionService.logout();
} catch (_) {}
// 2. Clear treezor repository in-memory cache
try {
final treezorRepository = ref.read(treezorRepositoryProvider);
await treezorRepository.resetScaWallets();
} catch (_) {}
// 3. Backend logout
try {
await repository.post<void>('/auth/logout');
} on DioException catch (error) {
throw Exception(error.message ?? 'Error in logout');
}
// 4. Clear local data (stops heartbeat via onBeforeSessionCleared,
// then clears cookies + SharedPreferences)
await clearSessionData();
});

View File

@@ -41,9 +41,9 @@ class _SplashScreenState extends State<SplashScreen> {
case InitialRoute.login:
widget.navigationContract.goTo(AppRoutes.login);
case InitialRoute.deviceSetup:
widget.navigationContract.goTo(AppRoutes.login);
widget.navigationContract.goTo(AppRoutes.dashboardHome);
case InitialRoute.home:
widget.navigationContract.goTo(AppRoutes.login);
widget.navigationContract.goTo(AppRoutes.dashboardHome);
}
}

View File

@@ -7,6 +7,6 @@
<versions>
<version>2.6.4</version>
</versions>
<lastUpdated>20260224000000</lastUpdated>
<lastUpdated>20260225000000</lastUpdated>
</versioning>
</metadata>

View File

@@ -1 +1 @@
2c321972b90055bcdf1c2015d5df1d47
7f7d74256c9acd1f6cc92eeee34d86a8

View File

@@ -1 +1 @@
fd6452bd1ecfb9dec5bc975bef3263b0453f1660
6072df0b16aa4891cfd1a4c2c81cf5042c2972ee

View File

@@ -4,6 +4,7 @@ import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'src/network/dio_client.dart';
import 'src/network/treezor_token_interceptor.dart';
import 'src/env/env_contract.dart';
import 'src/api/questia_api.dart';
import 'src/repositories/questia_repository_impl.dart';
@@ -13,7 +14,11 @@ export 'src/repositories/questia_repository.dart';
final getIt = GetIt.instance;
Future<void> configureDependencies(EnvConfig env, {bool log = false}) async {
Future<void> configureDependencies(
EnvConfig env, {
bool log = false,
void Function()? onTreezorTokenExpired,
}) async {
final cookieJar = await buildPersistCookieJar();
final dio = await buildDioClient(
@@ -23,6 +28,12 @@ Future<void> configureDependencies(EnvConfig env, {bool log = false}) async {
cookieJar: cookieJar,
);
if (onTreezorTokenExpired != null) {
dio.interceptors.add(
TreezorTokenInterceptor(onTokenExpired: onTreezorTokenExpired),
);
}
getIt.registerLazySingleton<CookieJar>(() => cookieJar);
getIt.registerLazySingleton<Dio>(() => dio);
getIt.registerLazySingleton<QuestiaApi>(() => QuestiaApi(getIt<Dio>()));
@@ -31,7 +42,11 @@ Future<void> configureDependencies(EnvConfig env, {bool log = false}) async {
);
}
void Function()? onBeforeSessionCleared;
Future<void> clearSessionData() async {
onBeforeSessionCleared?.call();
final cookieJar = getIt<CookieJar>();
await cookieJar.deleteAll();

View File

@@ -0,0 +1,51 @@
import 'dart:convert';
import 'package:dio/dio.dart';
class TreezorTokenInterceptor extends Interceptor {
TreezorTokenInterceptor({required void Function() onTokenExpired})
: _onTokenExpired = onTokenExpired;
final void Function() _onTokenExpired;
bool _handling = false;
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final message = _extractApiMessage(err.response?.data);
if (message == 'Treezor Token Expired' && !_handling) {
_handling = true;
_onTokenExpired();
Future.delayed(const Duration(seconds: 2), () => _handling = false);
}
handler.next(err);
}
String? _extractApiMessage(Object? data) {
if (data == null) return null;
if (data is Map) {
final errorObj = data['error'];
if (errorObj is Map && errorObj['message'] is String) {
return (errorObj['message'] as String).trim();
}
if (data['message'] is String) {
return (data['message'] as String).trim();
}
return null;
}
if (data is String) {
final raw = data.trim();
if (raw.isEmpty) return null;
try {
final decoded = jsonDecode(raw);
return _extractApiMessage(decoded);
} catch (_) {
return raw;
}
}
return null;
}
}

View File

@@ -215,12 +215,14 @@
"walletTitle": "Wallet",
"walletTotal": "{amount}€ gesamt",
"walletAvailable": "Verfügbar",
"walletSavings": "Ersparnisse",
"depositTitle": "Geld auf das Wallet einzahlen",
"depositAmountLabel": "Betrag",
"depositAmountHint": "0€",
"depositButton": "Einzahlen",
"depositMaxInfo": "Maximalbetrag: {amount}€",
"depositErrorMaxExceeded": "Der Betrag übersteigt das zulässige Maximum ({amount}€)",
"expensesTitle": "Ausgaben",
"filterToday": "Heute",
@@ -242,7 +244,23 @@
"transactionPayoutRefund": "Ausgangserstattung",
"transactionTransfer": "Überweisung",
"transactionCardPayment": "Kartenzahlung",
"transactionPayinAcquiring": "Zahlung erhalten",
"transactionPayinRefundAcquiring": "Zahlungserstattung",
"transactionSctrInst": "Sofortüberweisung",
"transactionPayinSctInstantRecall": "Überweisungsrückruf",
"transactionPayoutSctInstantEmit": "Ausgehende Sofortüberweisung",
"transactionPayinSctInstantEmitRecall": "Überweisungsrückruf",
"transactionCreditTransferReturned": "Überweisung zurückgesendet",
"transactionCheckPayin": "Scheckeinzahlung",
"transactionSdde": "Lastschrift",
"transactionSddr": "Lastschriftrückgabe",
"transactionSddrReversal": "Lastschriftstornierung",
"transactionSctrRecall": "Überweisungsrückruf",
"transactionCheckRefund": "Scheckerstattung",
"transactionSctr": "Banküberweisung",
"transactionCreditInternationalTransfer": "Auslandsüberweisung",
"transactionUnknown": "Bewegung",
"loadMore": "Mehr laden",
"profileAccountSettings": "Kontoeinstellungen",
"profileWithdrawMoney": "Geld vom Wallet abheben",

View File

@@ -215,12 +215,14 @@
"walletTitle": "Wallet",
"walletTotal": "{amount}€ total",
"walletAvailable": "Available",
"walletSavings": "Savings",
"depositTitle": "Add money to wallet",
"depositAmountLabel": "Amount",
"depositAmountHint": "0€",
"depositButton": "Deposit",
"depositMaxInfo": "Maximum you can add: {amount}€",
"depositErrorMaxExceeded": "The amount exceeds the maximum allowed ({amount}€)",
"expensesTitle": "Expenses",
"filterToday": "Today",
@@ -242,7 +244,23 @@
"transactionPayoutRefund": "Outgoing refund",
"transactionTransfer": "Transfer",
"transactionCardPayment": "Card payment",
"transactionPayinAcquiring": "Payment received",
"transactionPayinRefundAcquiring": "Payment refund",
"transactionSctrInst": "Instant transfer",
"transactionPayinSctInstantRecall": "Transfer recall",
"transactionPayoutSctInstantEmit": "Instant outgoing transfer",
"transactionPayinSctInstantEmitRecall": "Transfer recall",
"transactionCreditTransferReturned": "Transfer returned",
"transactionCheckPayin": "Check deposit",
"transactionSdde": "Direct debit",
"transactionSddr": "Direct debit return",
"transactionSddrReversal": "Direct debit reversal",
"transactionSctrRecall": "Transfer recall",
"transactionCheckRefund": "Check refund",
"transactionSctr": "Credit transfer",
"transactionCreditInternationalTransfer": "International transfer",
"transactionUnknown": "Transaction",
"loadMore": "Load more",
"profileAccountSettings": "Account settings",
"profileWithdrawMoney": "Withdraw money from wallet",

View File

@@ -215,12 +215,14 @@
"walletTitle": "Wallet",
"walletTotal": "{amount}€ total",
"walletAvailable": "Disponible",
"walletSavings": "Ahorros",
"depositTitle": "Ingresar dinero en el wallet",
"depositAmountLabel": "Cantidad",
"depositAmountHint": "0€",
"depositButton": "Ingresar",
"depositMaxInfo": "Máximo que puedes añadir: {amount}€",
"depositErrorMaxExceeded": "El monto supera el máximo permitido ({amount}€)",
"expensesTitle": "Gastos",
"filterToday": "Hoy",
@@ -242,7 +244,23 @@
"transactionPayoutRefund": "Reembolso de salida",
"transactionTransfer": "Transferencia",
"transactionCardPayment": "Pago con tarjeta",
"transactionPayinAcquiring": "Cobro recibido",
"transactionPayinRefundAcquiring": "Reembolso de cobro",
"transactionSctrInst": "Transferencia instantánea",
"transactionPayinSctInstantRecall": "Reclamación de transferencia",
"transactionPayoutSctInstantEmit": "Transferencia instantánea saliente",
"transactionPayinSctInstantEmitRecall": "Reclamación de transferencia",
"transactionCreditTransferReturned": "Transferencia devuelta",
"transactionCheckPayin": "Ingreso por cheque",
"transactionSdde": "Domiciliación",
"transactionSddr": "Devolución de domiciliación",
"transactionSddrReversal": "Reversión de domiciliación",
"transactionSctrRecall": "Reclamación de transferencia",
"transactionCheckRefund": "Reembolso de cheque",
"transactionSctr": "Transferencia bancaria",
"transactionCreditInternationalTransfer": "Transferencia internacional",
"transactionUnknown": "Movimiento",
"loadMore": "Cargar más",
"profileAccountSettings": "Ajustes de la cuenta",
"profileWithdrawMoney": "Retirar dinero del wallet",

View File

@@ -215,12 +215,14 @@
"walletTitle": "Portefeuille",
"walletTotal": "{amount}€ total",
"walletAvailable": "Disponible",
"walletSavings": "Épargne",
"depositTitle": "Ajouter de l'argent au portefeuille",
"depositAmountLabel": "Montant",
"depositAmountHint": "0€",
"depositButton": "Déposer",
"depositMaxInfo": "Maximum que vous pouvez ajouter : {amount}€",
"depositErrorMaxExceeded": "Le montant dépasse le maximum autorisé ({amount}€)",
"expensesTitle": "Dépenses",
"filterToday": "Aujourd'hui",
@@ -242,7 +244,23 @@
"transactionPayoutRefund": "Remboursement de sortie",
"transactionTransfer": "Virement",
"transactionCardPayment": "Paiement par carte",
"transactionPayinAcquiring": "Paiement reçu",
"transactionPayinRefundAcquiring": "Remboursement de paiement",
"transactionSctrInst": "Virement instantané",
"transactionPayinSctInstantRecall": "Rappel de virement",
"transactionPayoutSctInstantEmit": "Virement instantané sortant",
"transactionPayinSctInstantEmitRecall": "Rappel de virement",
"transactionCreditTransferReturned": "Virement retourné",
"transactionCheckPayin": "Dépôt par chèque",
"transactionSdde": "Prélèvement",
"transactionSddr": "Retour de prélèvement",
"transactionSddrReversal": "Annulation de prélèvement",
"transactionSctrRecall": "Rappel de virement",
"transactionCheckRefund": "Remboursement de chèque",
"transactionSctr": "Virement bancaire",
"transactionCreditInternationalTransfer": "Virement international",
"transactionUnknown": "Mouvement",
"loadMore": "Charger plus",
"profileAccountSettings": "Paramètres du compte",
"profileWithdrawMoney": "Retirer de l'argent du portefeuille",

View File

@@ -215,12 +215,14 @@
"walletTitle": "Portafoglio",
"walletTotal": "{amount}€ totale",
"walletAvailable": "Disponibile",
"walletSavings": "Risparmi",
"depositTitle": "Aggiungi denaro al portafoglio",
"depositAmountLabel": "Importo",
"depositAmountHint": "0€",
"depositButton": "Deposita",
"depositMaxInfo": "Massimo che puoi aggiungere: {amount}€",
"depositErrorMaxExceeded": "L'importo supera il massimo consentito ({amount}€)",
"expensesTitle": "Spese",
"filterToday": "Oggi",
@@ -242,7 +244,23 @@
"transactionPayoutRefund": "Rimborso in uscita",
"transactionTransfer": "Trasferimento",
"transactionCardPayment": "Pagamento con carta",
"transactionPayinAcquiring": "Pagamento ricevuto",
"transactionPayinRefundAcquiring": "Rimborso pagamento",
"transactionSctrInst": "Bonifico istantaneo",
"transactionPayinSctInstantRecall": "Richiamo bonifico",
"transactionPayoutSctInstantEmit": "Bonifico istantaneo in uscita",
"transactionPayinSctInstantEmitRecall": "Richiamo bonifico",
"transactionCreditTransferReturned": "Bonifico restituito",
"transactionCheckPayin": "Deposito assegno",
"transactionSdde": "Addebito diretto",
"transactionSddr": "Restituzione addebito diretto",
"transactionSddrReversal": "Storno addebito diretto",
"transactionSctrRecall": "Richiamo bonifico",
"transactionCheckRefund": "Rimborso assegno",
"transactionSctr": "Bonifico bancario",
"transactionCreditInternationalTransfer": "Bonifico internazionale",
"transactionUnknown": "Movimento",
"loadMore": "Carica altro",
"profileAccountSettings": "Impostazioni account",
"profileWithdrawMoney": "Preleva denaro dal portafoglio",

View File

@@ -215,12 +215,14 @@
"walletTitle": "Carteira",
"walletTotal": "{amount}€ total",
"walletAvailable": "Disponível",
"walletSavings": "Poupança",
"depositTitle": "Adicionar dinheiro à carteira",
"depositAmountLabel": "Quantia",
"depositAmountHint": "0€",
"depositButton": "Depositar",
"depositMaxInfo": "Máximo que podes adicionar: {amount}€",
"depositErrorMaxExceeded": "O valor excede o máximo permitido ({amount}€)",
"expensesTitle": "Despesas",
"filterToday": "Hoje",
@@ -242,7 +244,23 @@
"transactionPayoutRefund": "Reembolso de saída",
"transactionTransfer": "Transferência",
"transactionCardPayment": "Pagamento com cartão",
"transactionPayinAcquiring": "Pagamento recebido",
"transactionPayinRefundAcquiring": "Reembolso de pagamento",
"transactionSctrInst": "Transferência instantânea",
"transactionPayinSctInstantRecall": "Revogação de transferência",
"transactionPayoutSctInstantEmit": "Transferência instantânea de saída",
"transactionPayinSctInstantEmitRecall": "Revogação de transferência",
"transactionCreditTransferReturned": "Transferência devolvida",
"transactionCheckPayin": "Depósito por cheque",
"transactionSdde": "Débito direto",
"transactionSddr": "Devolução de débito direto",
"transactionSddrReversal": "Reversão de débito direto",
"transactionSctrRecall": "Revogação de transferência",
"transactionCheckRefund": "Reembolso de cheque",
"transactionSctr": "Transferência bancária",
"transactionCreditInternationalTransfer": "Transferência internacional",
"transactionUnknown": "Movimento",
"loadMore": "Carregar mais",
"profileAccountSettings": "Definições da conta",
"profileWithdrawMoney": "Levantar dinheiro da carteira",

View File

@@ -258,11 +258,13 @@ class I18n {
static const String walletTitle = 'walletTitle';
static const String walletTotal = 'walletTotal';
static const String walletAvailable = 'walletAvailable';
static const String walletSavings = 'walletSavings';
static const String depositTitle = 'depositTitle';
static const String depositAmountLabel = 'depositAmountLabel';
static const String depositAmountHint = 'depositAmountHint';
static const String depositButton = 'depositButton';
static const String depositMaxInfo = 'depositMaxInfo';
static const String depositErrorMaxExceeded = 'depositErrorMaxExceeded';
static const String expensesTitle = 'expensesTitle';
static const String filterToday = 'filterToday';
static const String filterThisWeek = 'filterThisWeek';
@@ -281,7 +283,23 @@ class I18n {
static const String transactionPayoutRefund = 'transactionPayoutRefund';
static const String transactionTransfer = 'transactionTransfer';
static const String transactionCardPayment = 'transactionCardPayment';
static const String transactionPayinAcquiring = 'transactionPayinAcquiring';
static const String transactionPayinRefundAcquiring = 'transactionPayinRefundAcquiring';
static const String transactionSctrInst = 'transactionSctrInst';
static const String transactionPayinSctInstantRecall = 'transactionPayinSctInstantRecall';
static const String transactionPayoutSctInstantEmit = 'transactionPayoutSctInstantEmit';
static const String transactionPayinSctInstantEmitRecall = 'transactionPayinSctInstantEmitRecall';
static const String transactionCreditTransferReturned = 'transactionCreditTransferReturned';
static const String transactionCheckPayin = 'transactionCheckPayin';
static const String transactionSdde = 'transactionSdde';
static const String transactionSddr = 'transactionSddr';
static const String transactionSddrReversal = 'transactionSddrReversal';
static const String transactionSctrRecall = 'transactionSctrRecall';
static const String transactionCheckRefund = 'transactionCheckRefund';
static const String transactionSctr = 'transactionSctr';
static const String transactionCreditInternationalTransfer = 'transactionCreditInternationalTransfer';
static const String transactionUnknown = 'transactionUnknown';
static const String loadMore = 'loadMore';
static const String profileAccountSettings = 'profileAccountSettings';
static const String profileWithdrawMoney = 'profileWithdrawMoney';
static const String profileNoRecentTransactions =

View File

@@ -35,3 +35,10 @@ export 'src/data/models/device_model.dart';
export 'src/domain/entities/device_entity.dart';
export 'src/data/models/child_wallet_model.dart';
export 'src/domain/entities/child_wallet_entity.dart';
export 'src/providers/wallet_refresh_provider.dart';
export 'src/providers/wallet_balance_provider.dart';
export 'src/providers/parent_wallet_balance_provider.dart';
export 'src/providers/child_profiles_provider.dart';
export 'src/providers/account_total_balance_provider.dart';
export 'src/domain/entities/date_filter.dart';
export 'src/providers/wallet_transactions_provider.dart';

View File

@@ -46,6 +46,8 @@ abstract class TreezorRemoteDatasource {
Future<ChildWalletModel> getChildWallet({required String walletId});
Future<ChildWalletModel> getWallet({required String walletId});
Future<void> walletMove({
required String walletId,
required String targetWalletId,

View File

@@ -292,6 +292,25 @@ class TreezorRemoteDatasourceImpl implements TreezorRemoteDatasource {
}
}
@override
Future<ChildWalletModel> getWallet({required String walletId}) async {
try {
final response = await _repository.get<Map<String, dynamic>>(
'/wallets/$walletId',
);
final data = response.data;
if (data == null || data.isEmpty) {
throw Exception('Empty response from /wallets/$walletId');
}
final parsed = ChildWalletResponseModel.fromJson(data);
return parsed.item;
} on DioException catch (error) {
throw _mapDioError(error, defaultMessage: 'Error in /wallets/$walletId');
}
}
@override
Future<void> walletMove({
required String walletId,

View File

@@ -167,6 +167,12 @@ class TreezorRepositoryImpl implements TreezorRepository {
return model.toEntity();
}
@override
Future<ChildWalletEntity> getWallet({required String walletId}) async {
final model = await _remote.getWallet(walletId: walletId);
return model.toEntity();
}
@override
Future<void> walletMove({
required String walletId,

View File

@@ -32,11 +32,11 @@ enum DateFilter {
class TransactionsQuery {
final String walletId;
final DateFilter dateFilter;
final DateFilter? dateFilter;
const TransactionsQuery({
required this.walletId,
required this.dateFilter,
this.dateFilter,
});
@override

View File

@@ -4,16 +4,32 @@ part 'wallet_transaction_entity.freezed.dart';
enum TransactionType {
payin,
payinrefund,
payout,
payoutrefund,
transfer,
cardtransaction,
payinRefund,
payoutRefund,
cardTransaction,
payinAcquiring,
payinRefundAcquiring,
sctrInst,
payinSctInstantRecall,
payoutSctInstantEmit,
payinSctInstantEmitRecall,
creditTransferReturned,
checkPayin,
sdde,
sddr,
sddrReversal,
sctrRecall,
checkRefund,
sctr,
creditInternationalTransfer,
unknown;
static TransactionType fromString(String value) {
final normalized = value.replaceAll(' ', '').toLowerCase();
return TransactionType.values.firstWhere(
(e) => e.name == value.toLowerCase(),
(e) => e.name.toLowerCase() == normalized,
orElse: () => TransactionType.unknown,
);
}

View File

@@ -46,6 +46,8 @@ abstract class TreezorRepository {
Future<ChildWalletEntity> getChildWallet({required String walletId});
Future<ChildWalletEntity> getWallet({required String walletId});
Future<void> walletMove({
required String walletId,
required String targetWalletId,

View File

@@ -0,0 +1,42 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/src/providers/child_profiles_provider.dart';
import 'package:sf_shared/src/providers/treezor_repository_provider.dart';
import 'package:sf_shared/src/providers/user_info_provider.dart';
import 'package:sf_shared/src/providers/get_payment_profile_use_case_provider.dart';
import 'package:sf_shared/src/providers/wallet_balance_provider.dart';
import 'package:sf_shared/src/providers/wallet_refresh_provider.dart';
/// Total balance across all wallets in the account (parent + children).
final accountTotalBalanceProvider =
FutureProvider.autoDispose<double>((ref) async {
ref.watch(walletRefreshProvider);
final user = await ref.read(userInfoProvider.future);
final paymentProfile =
await ref.read(getPaymentProfileUseCaseProvider).getPaymentProfile(
userId: user.id,
);
var total = 0.0;
// Parent wallet balance
final parentWalletId = paymentProfile.paymentWalletId;
if (parentWalletId != null && parentWalletId.isNotEmpty) {
final parentBalance =
await ref.read(walletBalanceProvider(parentWalletId).future);
total += parentBalance.totalBalance;
}
// Children wallets balance
final children = await ref.read(childProfilesProvider.future);
final treezorRepo = ref.read(treezorRepositoryProvider);
for (final child in children) {
try {
final cw = await treezorRepo.getChildWallet(walletId: child.walletId);
total += cw.solde;
} catch (_) {}
}
return total;
});

View File

@@ -0,0 +1,10 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/src/domain/entities/child_profile_entity.dart';
import 'package:sf_shared/src/providers/user_repository_provider.dart';
final childProfilesProvider =
FutureProvider.autoDispose<List<ChildProfileEntity>>((ref) async {
final repository = ref.read(userRepositoryProvider);
return repository.getChildProfiles();
});

View File

@@ -0,0 +1,47 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/src/domain/entities/wallet_balance_entity.dart';
import 'package:sf_shared/src/providers/treezor_repository_provider.dart';
final parentWalletBalanceProvider =
NotifierProvider<ParentWalletBalanceNotifier, WalletBalanceEntity?>(
ParentWalletBalanceNotifier.new,
);
class ParentWalletBalanceNotifier extends Notifier<WalletBalanceEntity?> {
String? _walletId;
Timer? _refreshTimer;
@override
WalletBalanceEntity? build() => null;
Future<void> load(String walletId) async {
_walletId = walletId;
final repo = ref.read(treezorRepositoryProvider);
state = await repo.getWalletBalance(walletId: walletId);
}
Future<void> refresh() async {
final walletId = _walletId;
if (walletId == null || walletId.isEmpty) return;
try {
final repo = ref.read(treezorRepositoryProvider);
state = await repo.getWalletBalance(walletId: walletId);
} catch (_) {}
}
void applyOptimisticPayin(double amount) {
final balance = state;
if (balance == null) return;
state = balance.copyWith(
availableBalance: balance.availableBalance + amount,
totalBalance: balance.totalBalance + amount,
);
_refreshTimer?.cancel();
_refreshTimer = Timer(const Duration(seconds: 3), () {
refresh();
});
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/src/domain/entities/wallet_balance_entity.dart';
import 'package:sf_shared/src/providers/treezor_repository_provider.dart';
import 'package:sf_shared/src/providers/wallet_refresh_provider.dart';
final walletBalanceProvider =
FutureProvider.autoDispose.family<WalletBalanceEntity, String>(
(ref, walletId) async {
ref.watch(walletRefreshProvider);
final repository = ref.read(treezorRepositoryProvider);
return repository.getWalletBalance(walletId: walletId);
});

View File

@@ -0,0 +1,15 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Signal provider that modules can watch to know when parent wallet data
/// should be refreshed. Increment it after any operation that changes the
/// parent balance (deposit, extract, payin, payout).
final walletRefreshProvider = NotifierProvider<WalletRefreshNotifier, int>(
WalletRefreshNotifier.new,
);
class WalletRefreshNotifier extends Notifier<int> {
@override
int build() => 0;
void refresh() => state++;
}

View File

@@ -0,0 +1,29 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/src/domain/entities/date_filter.dart';
import 'package:sf_shared/src/domain/entities/wallet_transaction_entity.dart';
import 'package:sf_shared/src/providers/treezor_repository_provider.dart';
import 'package:sf_shared/src/providers/wallet_refresh_provider.dart';
final walletTransactionsProvider = FutureProvider.autoDispose
.family<List<WalletTransactionEntity>, TransactionsQuery>(
(ref, query) async {
ref.watch(walletRefreshProvider);
final repository = ref.read(treezorRepositoryProvider);
Map<String, dynamic>? queryParameters;
if (query.dateFilter != null) {
final filtersJson = jsonEncode({
'createdDate': {'gte': query.dateFilter!.startDate.toIso8601String()},
});
queryParameters = {'filters': base64Encode(utf8.encode(filtersJson))};
}
final response = await repository.getWalletTransactions(
walletId: query.walletId,
queryParameters: queryParameters,
);
return response.items;
});

View File

@@ -5,7 +5,7 @@ import 'package:sf_localizations/sf_localizations.dart';
class DepositBlock extends ConsumerStatefulWidget {
final double max;
final ValueChanged<double>? onDeposit;
final Future<bool> Function(double amount)? onDeposit;
const DepositBlock({super.key, required this.max, this.onDeposit});
@@ -22,7 +22,7 @@ class _DepositBlockState extends ConsumerState<DepositBlock> {
super.dispose();
}
void _onPressed() {
Future<void> _onPressed() async {
final text = _amountController.text.trim();
final amount = double.tryParse(text);
if (amount == null || amount <= 0) {
@@ -33,8 +33,22 @@ class _DepositBlockState extends ConsumerState<DepositBlock> {
);
return;
}
if (amount > widget.max) return;
widget.onDeposit?.call(amount);
if (amount > widget.max) {
showTopSnackbar(
context,
message: context.translate(
I18n.depositErrorMaxExceeded,
args: {'amount': widget.max.toStringAsFixed(2)},
),
type: MessageType.warning,
);
return;
}
final success = await widget.onDeposit?.call(amount) ?? false;
if (success && mounted) {
_amountController.clear();
FocusScope.of(context).unfocus();
}
}
@override

View File

@@ -6,15 +6,15 @@ import 'package:sf_localizations/sf_localizations.dart';
class WalletBalanceBlock extends ConsumerWidget {
static const double _maxBalance = 150.0;
final double value;
final double savings;
final double savingsPlan;
final double availableBalance;
final double allocatedBalance;
final double totalBalance;
const WalletBalanceBlock({
super.key,
required this.value,
required this.savings,
this.savingsPlan = 30.0,
required this.availableBalance,
required this.allocatedBalance,
required this.totalBalance,
});
@override
@@ -40,7 +40,7 @@ class WalletBalanceBlock extends ConsumerWidget {
MoneyText(
text: context.translate(
I18n.walletTotal,
args: {'amount': '$value'},
args: {'amount': '${totalBalance.toStringAsFixed(2)}'},
),
size: 26,
secondarySize: 16,
@@ -48,33 +48,70 @@ class WalletBalanceBlock extends ConsumerWidget {
),
],
),
// ProgressBar(
// max: savingsPlan,
// value: savings,
// height: 24,
// textSize: 16,
// textSecondarySize: 12,
// backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
// foregroundColor: theme.getColorFor(ThemeCode.backgroundTertiary),
// textColor: theme.getColorFor(ThemeCode.textPrimary),
// ),
ProgressBar(
max: _maxBalance,
value: value,
height: 83,
textSize: 40,
textSecondarySize: 24,
value: totalBalance,
height: 24,
textSize: 0,
textSecondarySize: 0,
backgroundColor: theme.getColorFor(ThemeCode.backgroundTertiary),
foregroundColor: theme.getColorFor(ThemeCode.buttonPrimary),
textColor: theme.getColorFor(ThemeCode.textSecondary),
textColor: Colors.transparent,
),
Center(
child: Text(
'${value.toStringAsFixed(2)}\u20AC / ${_maxBalance.toStringAsFixed(0)}\u20AC',
'${totalBalance.toStringAsFixed(2)}\u20AC / ${_maxBalance.toStringAsFixed(0)}\u20AC',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
),
),
Center(child: Text(context.translate(I18n.walletAvailable))),
Row(
children: [
Expanded(
child: Column(
children: [
Text(
context.translate(I18n.walletAvailable),
style: TextStyle(
fontSize: 14,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
const SizedBox(height: 4),
Text(
'${availableBalance.toStringAsFixed(2)}\u20AC',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
],
),
),
Expanded(
child: Column(
children: [
Text(
context.translate(I18n.walletSavings),
style: TextStyle(
fontSize: 14,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
const SizedBox(height: 4),
Text(
'${allocatedBalance.toStringAsFixed(2)}\u20AC',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
],
),
),
],
),
],
),
);