app state fixed
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
100
modules/home/lib/src/presentation/child_wallets_slider.dart
Normal file
100
modules/home/lib/src/presentation/child_wallets_slider.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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 (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
<versions>
|
||||
<version>2.6.4</version>
|
||||
</versions>
|
||||
<lastUpdated>20260224000000</lastUpdated>
|
||||
<lastUpdated>20260225000000</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
||||
@@ -1 +1 @@
|
||||
2c321972b90055bcdf1c2015d5df1d47
|
||||
7f7d74256c9acd1f6cc92eeee34d86a8
|
||||
@@ -1 +1 @@
|
||||
fd6452bd1ecfb9dec5bc975bef3263b0453f1660
|
||||
6072df0b16aa4891cfd1a4c2c81cf5042c2972ee
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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++;
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user