add edit profile screens (child/parent) with SCA, paginated transactions, and reactive state refresh

This commit is contained in:
2026-03-12 22:42:38 +01:00
parent 76c7eb606f
commit 69b3cf358a
79 changed files with 7033 additions and 1361 deletions

View File

@@ -7,3 +7,4 @@ export 'src/features/lock_card/lock_card_builder.dart';
export 'src/features/limits/limits_builder.dart';
export 'src/features/goals/goals_builder.dart';
export 'src/features/extract/extract_builder.dart';
export 'src/features/edit_child_profile/edit_child_profile_builder.dart';

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
@@ -15,11 +14,12 @@ class ChildDataNotifier extends Notifier<ChildDataState> {
final String childId;
ChildDataNotifier(this.childId);
late final TreezorRepository _treezorRepository;
late final UserRepository _userRepository;
late TreezorRepository _treezorRepository;
late UserRepository _userRepository;
@override
ChildDataState build() {
ref.watch(walletRefreshProvider);
final link = ref.keepAlive();
final timer = Timer(const Duration(minutes: 5), link.close);
ref.onDispose(timer.cancel);
@@ -56,9 +56,7 @@ class ChildDataNotifier extends Notifier<ChildDataState> {
device = await _userRepository.getDeviceByIdentificator(
identificator: childProfile.deviceIdentificator,
);
} catch (e) {
debugPrint('Error fetching device for child $childId: $e');
}
} catch (_) {}
if (!ref.mounted) return;

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:navigation/navigation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
import '../../card_colors.dart';
import '../../presentation/state/home_view_model.dart';
@@ -162,14 +163,60 @@ class _ChildWalletScreenState extends ConsumerState<ChildWalletScreen> {
),
),
_buildGenderAvatar(device?.carrierGenre, 50),
Text(
childName,
style: TextStyle(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
fontWeight: FontWeight.bold,
fontSize: 20,
Expanded(
child: Text(
childName,
style: TextStyle(
color: theme.getColorFor(
ThemeCode.backgroundPrimary,
),
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
),
PopupMenuButton<String>(
icon: Icon(
Icons.more_vert,
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
onSelected: (value) {
if (value == 'edit') {
widget.navigation.pushTo(
AppRoutes.editChildProfile(widget.childId),
);
} else if (value == 'delete') {
_showDeleteConfirmation();
}
},
itemBuilder: (_) => [
PopupMenuItem(
value: 'edit',
child: Row(
spacing: 8,
children: [
Icon(Icons.edit_outlined),
Text(
context.translate(I18n.editChildProfile),
),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
spacing: 8,
children: [
Icon(Icons.delete_outline, color: Colors.red),
Text(
context.translate(I18n.deleteDevice),
style: TextStyle(color: Colors.red),
),
],
),
),
],
),
],
),
Column(
@@ -196,76 +243,37 @@ class _ChildWalletScreenState extends ConsumerState<ChildWalletScreen> {
minHeight: 10,
borderRadius: BorderRadius.all(Radius.circular(5)),
),
if (CardStatus.fromString(viewState.cardStatus) ==
CardStatus.lost ||
CardStatus.fromString(viewState.cardStatus) ==
CardStatus.stolen)
TextButton(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
onPressed: viewState.isUpdatingCard
? null
: () => _showDeleteConfirmation(context, ref),
child: Row(
spacing: 10,
children: [
Icon(
Icons.delete_outline,
size: 24,
color: theme.getColorFor(
ThemeCode.textSecondary,
),
),
Text(
context.translate(I18n.deleteDevice),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: theme.getColorFor(
ThemeCode.textSecondary,
),
),
),
],
),
)
else
TextButton(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
onPressed: () =>
_showCardStatusSheet(context, ref, theme),
child: Row(
spacing: 10,
children: [
Icon(
Icons.lock_outline,
size: 24,
color: theme.getColorFor(
ThemeCode.textSecondary,
),
),
Text(
locked
? context.translate(
I18n.childWalletUnlockCard,
)
: context.translate(
I18n.childWalletLockCard,
),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: theme.getColorFor(
ThemeCode.textSecondary,
),
),
),
],
),
TextButton(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
onPressed: () =>
_showCardStatusSheet(context, ref, theme),
child: Row(
spacing: 10,
children: [
Icon(
Icons.lock_outline,
size: 24,
color: theme.getColorFor(ThemeCode.textSecondary),
),
Text(
locked
? context.translate(
I18n.childWalletUnlockCard,
)
: context.translate(I18n.childWalletLockCard),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: theme.getColorFor(
ThemeCode.textSecondary,
),
),
),
],
),
),
],
),
Column(
@@ -302,7 +310,7 @@ class _ChildWalletScreenState extends ConsumerState<ChildWalletScreen> {
child: AppLoadingIndicator(size: 48),
),
)
else if (viewState.transactions.isEmpty)
else if (viewState.transactionPages.isEmpty)
Padding(
padding: const EdgeInsets.all(24),
child: Center(
@@ -319,13 +327,34 @@ class _ChildWalletScreenState extends ConsumerState<ChildWalletScreen> {
),
),
)
else
...viewState.transactions.map(
else ...[
...viewState.transactionPages[viewState.currentPage].map(
(tx) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TransactionTile(transaction: tx),
),
),
PaginationBar(
currentPage: viewState.currentPage,
totalPages: viewState.transactionPages.length,
hasMore: viewState.nextCursor != null,
isLoadingMore: viewState.isLoadingMore,
onPageChanged: (page) => ref
.read(
childWalletViewModelProvider(
widget.childId,
).notifier,
)
.setPage(page),
onLoadMore: () => ref
.read(
childWalletViewModelProvider(
widget.childId,
).notifier,
)
.loadMore(),
),
],
],
),
),
@@ -342,17 +371,13 @@ class _ChildWalletScreenState extends ConsumerState<ChildWalletScreen> {
Widget _buildGenderAvatar(String? carrierGenre, double size) {
final IconData icon;
final Color color;
switch (carrierGenre) {
case 'M':
icon = Icons.face;
color = const Color(0xFF64B5F6);
case 'F':
icon = Icons.face_3;
color = const Color(0xFFF48FB1);
default:
icon = Icons.face_2;
color = const Color(0xFF90A4AE);
}
return CircleAvatar(
radius: size / 2,
@@ -376,47 +401,108 @@ class _ChildWalletScreenState extends ConsumerState<ChildWalletScreen> {
);
}
void _showDeleteConfirmation(BuildContext context, WidgetRef ref) {
Future<void> _showDeleteConfirmation() async {
final theme = ref.read(themePortProvider);
final userRepo = ref.read(userRepositoryProvider);
final navigator = Navigator.of(context, rootNavigator: true);
final checkingText = context.translate(I18n.deleteDeviceChecking);
final notAllowedTitle = context.translate(I18n.deleteDeviceNotAllowedTitle);
final nonZeroText = context.translate(
I18n.deleteDeviceWalletNonZeroBalance,
);
final acceptText = context.translate(I18n.accept);
final confirmTitle = context.translate(I18n.deleteDeviceConfirmTitle);
final confirmMessage = context.translate(I18n.deleteDeviceConfirmMessage);
final cancelText = context.translate(I18n.cancel);
final deleteText = context.translate(I18n.deleteDevice);
final successText = context.translate(I18n.deleteDeviceSuccess);
final bgColor = theme.getColorFor(ThemeCode.backgroundPrimary);
final shape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
);
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text(context.translate(I18n.deleteDeviceConfirmTitle)),
content: Text(context.translate(I18n.deleteDeviceConfirmMessage)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(context.translate(I18n.cancel)),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final viewModel = ref.read(
childWalletViewModelProvider(widget.childId).notifier,
);
final success = await viewModel.deleteDevice();
if (success && context.mounted) {
ref
.read(homeViewModelProvider.notifier)
.removeChild(widget.childId);
showTopSnackbar(
context,
message: context.translate(I18n.deleteDeviceSuccess),
type: MessageType.success,
);
widget.navigation.goBack();
}
},
child: Text(
context.translate(I18n.deleteDevice),
style: TextStyle(color: Colors.red),
),
),
],
backgroundColor: bgColor,
shape: shape,
content: Row(
spacing: 16,
children: [const AppLoadingIndicator(size: 24), Text(checkingText)],
),
),
);
try {
final deletability = await userRepo.checkChildProfileDeletability(
childProfileId: widget.childId,
);
if (!mounted) return;
navigator.pop();
if (!deletability.isDeletable) {
if (!mounted) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: bgColor,
shape: shape,
title: Text(notAllowedTitle),
content: Text(nonZeroText),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(acceptText),
),
],
),
);
return;
}
if (!mounted) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: bgColor,
shape: shape,
title: Text(confirmTitle),
content: Text(confirmMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(cancelText),
),
TextButton(
onPressed: () async {
Navigator.of(ctx).pop();
final viewModel = ref.read(
childWalletViewModelProvider(widget.childId).notifier,
);
final success = await viewModel.deleteDevice();
if (success && mounted) {
ref
.read(homeViewModelProvider.notifier)
.removeChild(widget.childId);
showTopSnackbar(
context,
message: successText,
type: MessageType.success,
);
widget.navigation.goBack();
}
},
child: Text(deleteText, style: TextStyle(color: Colors.red)),
),
],
),
);
} catch (e) {
if (!mounted) return;
navigator.pop();
showTopSnackbar(context, message: e.toString(), type: MessageType.error);
}
}
}
@@ -463,13 +549,19 @@ class _CardStatusSheetState extends ConsumerState<_CardStatusSheet> {
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
const SizedBox(height: 20),
...statuses.map(
(status) => RadioListTile<String>(
title: Text(context.translate(_labelKey(status))),
value: status,
groupValue: _selected,
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
onChanged: (v) => setState(() => _selected = v!),
RadioGroup<String>(
groupValue: _selected!,
onChanged: (v) => setState(() => _selected = v),
child: Column(
children: statuses
.map(
(status) => RadioListTile<String>(
title: Text(context.translate(_labelKey(status))),
value: status,
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
)
.toList(),
),
),
const SizedBox(height: 16),

View File

@@ -5,7 +5,6 @@ import 'package:get_it/get_it.dart';
import 'package:sca_treezor/sca_treezor.dart';
import 'package:sf_shared/sf_shared.dart';
import '../../card_colors.dart';
import 'child_data_provider.dart';
import 'child_wallet_view_state.dart';
@@ -19,8 +18,8 @@ class ChildWalletViewModel extends Notifier<ChildWalletViewState> {
final String childId;
ChildWalletViewModel(this.childId);
late final TreezorWalletConnectionService _connectionService;
late final TreezorWalletSignatureService _signatureService;
late TreezorWalletConnectionService _connectionService;
late TreezorWalletSignatureService _signatureService;
@override
ChildWalletViewState build() {
@@ -43,6 +42,8 @@ class ChildWalletViewModel extends Notifier<ChildWalletViewState> {
}
});
ref.watch(walletRefreshProvider);
final data = ref.read(childDataProvider(childId));
final initialState = ChildWalletViewState(
isLoading: data.isLoading,
@@ -66,19 +67,53 @@ class ChildWalletViewModel extends Notifier<ChildWalletViewState> {
state = state.copyWith(isLoadingTransactions: true);
try {
final query = TransactionsQuery(walletId: walletId);
final transactions =
await ref.read(walletTransactionsProvider(query).future);
final response = await ref.read(
walletTransactionsProvider(query).future,
);
if (!ref.mounted) return;
state = state.copyWith(
isLoadingTransactions: false,
transactions: transactions,
transactionPages: [response.items],
nextCursor: response.nextCursor,
currentPage: 0,
);
} catch (_) {
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoadingTransactions: false);
}
}
Future<void> loadMore() async {
final walletId = state.childProfile?.walletId;
if (walletId == null || state.isLoadingMore || state.nextCursor == null) return;
state = state.copyWith(isLoadingMore: true);
try {
final query = TransactionsQuery(
walletId: walletId,
cursor: state.nextCursor,
);
final response = await ref.read(
walletTransactionsProvider(query).future,
);
if (!ref.mounted) return;
state = state.copyWith(
isLoadingMore: false,
transactionPages: [...state.transactionPages, response.items],
nextCursor: response.nextCursor,
currentPage: state.transactionPages.length,
);
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(isLoadingMore: false);
}
}
void setPage(int page) {
state = state.copyWith(currentPage: page);
}
Future<void> _loadCard(String walletId) async {
try {
final card = await ref
@@ -181,6 +216,12 @@ class ChildWalletViewModel extends Notifier<ChildWalletViewState> {
}
}
Future<ChildProfileDeletabilityEntity> checkDeletability() async {
return ref
.read(userRepositoryProvider)
.checkChildProfileDeletability(childProfileId: childId);
}
Future<bool> deleteDevice() async {
final deviceId = state.device?.id;
if (deviceId == null || deviceId.isEmpty) return false;
@@ -195,7 +236,10 @@ class ChildWalletViewModel extends Notifier<ChildWalletViewState> {
return true;
} catch (e) {
if (!ref.mounted) return false;
state = state.copyWith(isUpdatingCard: false, cardStatusError: e.toString());
state = state.copyWith(
isUpdatingCard: false,
cardStatusError: e.toString(),
);
return false;
}
}

View File

@@ -22,6 +22,9 @@ abstract class ChildWalletViewState with _$ChildWalletViewState {
@Default('') String pin,
@Default(false) bool isSigning,
@Default(false) bool isLoadingTransactions,
@Default([]) List<WalletTransactionEntity> transactions,
@Default([]) List<List<WalletTransactionEntity>> transactionPages,
String? nextCursor,
@Default(false) bool isLoadingMore,
@Default(0) int currentPage,
}) = _ChildWalletViewState;
}

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ChildWalletViewState {
bool get isLoading; ChildProfileEntity? get childProfile; ChildWalletEntity? get childWallet; DeviceEntity? get device; String get cardId; String get cardStatus; bool get locked; String get errorMessage; bool get isUpdatingCard; String get cardStatusError; bool get cardStatusSuccess; bool get showPin; String get selectedStatus; String get pin; bool get isSigning; bool get isLoadingTransactions; List<WalletTransactionEntity> get transactions;
bool get isLoading; ChildProfileEntity? get childProfile; ChildWalletEntity? get childWallet; DeviceEntity? get device; String get cardId; String get cardStatus; bool get locked; String get errorMessage; bool get isUpdatingCard; String get cardStatusError; bool get cardStatusSuccess; bool get showPin; String get selectedStatus; String get pin; bool get isSigning; bool get isLoadingTransactions; List<List<WalletTransactionEntity>> get transactionPages; String? get nextCursor; bool get isLoadingMore; int get currentPage;
/// Create a copy of ChildWalletViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $ChildWalletViewStateCopyWith<ChildWalletViewState> get copyWith => _$ChildWalle
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChildWalletViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.childProfile, childProfile) || other.childProfile == childProfile)&&(identical(other.childWallet, childWallet) || other.childWallet == childWallet)&&(identical(other.device, device) || other.device == device)&&(identical(other.cardId, cardId) || other.cardId == cardId)&&(identical(other.cardStatus, cardStatus) || other.cardStatus == cardStatus)&&(identical(other.locked, locked) || other.locked == locked)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isUpdatingCard, isUpdatingCard) || other.isUpdatingCard == isUpdatingCard)&&(identical(other.cardStatusError, cardStatusError) || other.cardStatusError == cardStatusError)&&(identical(other.cardStatusSuccess, cardStatusSuccess) || other.cardStatusSuccess == cardStatusSuccess)&&(identical(other.showPin, showPin) || other.showPin == showPin)&&(identical(other.selectedStatus, selectedStatus) || other.selectedStatus == selectedStatus)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.isSigning, isSigning) || other.isSigning == isSigning)&&(identical(other.isLoadingTransactions, isLoadingTransactions) || other.isLoadingTransactions == isLoadingTransactions)&&const DeepCollectionEquality().equals(other.transactions, transactions));
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChildWalletViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.childProfile, childProfile) || other.childProfile == childProfile)&&(identical(other.childWallet, childWallet) || other.childWallet == childWallet)&&(identical(other.device, device) || other.device == device)&&(identical(other.cardId, cardId) || other.cardId == cardId)&&(identical(other.cardStatus, cardStatus) || other.cardStatus == cardStatus)&&(identical(other.locked, locked) || other.locked == locked)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isUpdatingCard, isUpdatingCard) || other.isUpdatingCard == isUpdatingCard)&&(identical(other.cardStatusError, cardStatusError) || other.cardStatusError == cardStatusError)&&(identical(other.cardStatusSuccess, cardStatusSuccess) || other.cardStatusSuccess == cardStatusSuccess)&&(identical(other.showPin, showPin) || other.showPin == showPin)&&(identical(other.selectedStatus, selectedStatus) || other.selectedStatus == selectedStatus)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.isSigning, isSigning) || other.isSigning == isSigning)&&(identical(other.isLoadingTransactions, isLoadingTransactions) || other.isLoadingTransactions == isLoadingTransactions)&&const DeepCollectionEquality().equals(other.transactionPages, transactionPages)&&(identical(other.nextCursor, nextCursor) || other.nextCursor == nextCursor)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,childProfile,childWallet,device,cardId,cardStatus,locked,errorMessage,isUpdatingCard,cardStatusError,cardStatusSuccess,showPin,selectedStatus,pin,isSigning,isLoadingTransactions,const DeepCollectionEquality().hash(transactions));
int get hashCode => Object.hashAll([runtimeType,isLoading,childProfile,childWallet,device,cardId,cardStatus,locked,errorMessage,isUpdatingCard,cardStatusError,cardStatusSuccess,showPin,selectedStatus,pin,isSigning,isLoadingTransactions,const DeepCollectionEquality().hash(transactionPages),nextCursor,isLoadingMore,currentPage]);
@override
String toString() {
return 'ChildWalletViewState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, cardId: $cardId, cardStatus: $cardStatus, locked: $locked, errorMessage: $errorMessage, isUpdatingCard: $isUpdatingCard, cardStatusError: $cardStatusError, cardStatusSuccess: $cardStatusSuccess, showPin: $showPin, selectedStatus: $selectedStatus, pin: $pin, isSigning: $isSigning, isLoadingTransactions: $isLoadingTransactions, transactions: $transactions)';
return 'ChildWalletViewState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, cardId: $cardId, cardStatus: $cardStatus, locked: $locked, errorMessage: $errorMessage, isUpdatingCard: $isUpdatingCard, cardStatusError: $cardStatusError, cardStatusSuccess: $cardStatusSuccess, showPin: $showPin, selectedStatus: $selectedStatus, pin: $pin, isSigning: $isSigning, isLoadingTransactions: $isLoadingTransactions, transactionPages: $transactionPages, nextCursor: $nextCursor, isLoadingMore: $isLoadingMore, currentPage: $currentPage)';
}
@@ -45,7 +45,7 @@ abstract mixin class $ChildWalletViewStateCopyWith<$Res> {
factory $ChildWalletViewStateCopyWith(ChildWalletViewState value, $Res Function(ChildWalletViewState) _then) = _$ChildWalletViewStateCopyWithImpl;
@useResult
$Res call({
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardId, String cardStatus, bool locked, String errorMessage, bool isUpdatingCard, String cardStatusError, bool cardStatusSuccess, bool showPin, String selectedStatus, String pin, bool isSigning, bool isLoadingTransactions, List<WalletTransactionEntity> transactions
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardId, String cardStatus, bool locked, String errorMessage, bool isUpdatingCard, String cardStatusError, bool cardStatusSuccess, bool showPin, String selectedStatus, String pin, bool isSigning, bool isLoadingTransactions, List<List<WalletTransactionEntity>> transactionPages, String? nextCursor, bool isLoadingMore, int currentPage
});
@@ -62,7 +62,7 @@ class _$ChildWalletViewStateCopyWithImpl<$Res>
/// Create a copy of ChildWalletViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? childProfile = freezed,Object? childWallet = freezed,Object? device = freezed,Object? cardId = null,Object? cardStatus = null,Object? locked = null,Object? errorMessage = null,Object? isUpdatingCard = null,Object? cardStatusError = null,Object? cardStatusSuccess = null,Object? showPin = null,Object? selectedStatus = null,Object? pin = null,Object? isSigning = null,Object? isLoadingTransactions = null,Object? transactions = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? childProfile = freezed,Object? childWallet = freezed,Object? device = freezed,Object? cardId = null,Object? cardStatus = null,Object? locked = null,Object? errorMessage = null,Object? isUpdatingCard = null,Object? cardStatusError = null,Object? cardStatusSuccess = null,Object? showPin = null,Object? selectedStatus = null,Object? pin = null,Object? isSigning = null,Object? isLoadingTransactions = null,Object? transactionPages = null,Object? nextCursor = freezed,Object? isLoadingMore = null,Object? currentPage = null,}) {
return _then(_self.copyWith(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,childProfile: freezed == childProfile ? _self.childProfile : childProfile // ignore: cast_nullable_to_non_nullable
@@ -80,8 +80,11 @@ as bool,selectedStatus: null == selectedStatus ? _self.selectedStatus : selected
as String,pin: null == pin ? _self.pin : pin // ignore: cast_nullable_to_non_nullable
as String,isSigning: null == isSigning ? _self.isSigning : isSigning // ignore: cast_nullable_to_non_nullable
as bool,isLoadingTransactions: null == isLoadingTransactions ? _self.isLoadingTransactions : isLoadingTransactions // ignore: cast_nullable_to_non_nullable
as bool,transactions: null == transactions ? _self.transactions : transactions // ignore: cast_nullable_to_non_nullable
as List<WalletTransactionEntity>,
as bool,transactionPages: null == transactionPages ? _self.transactionPages : transactionPages // ignore: cast_nullable_to_non_nullable
as List<List<WalletTransactionEntity>>,nextCursor: freezed == nextCursor ? _self.nextCursor : nextCursor // ignore: cast_nullable_to_non_nullable
as String?,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
as bool,currentPage: null == currentPage ? _self.currentPage : currentPage // ignore: cast_nullable_to_non_nullable
as int,
));
}
/// Create a copy of ChildWalletViewState
@@ -202,10 +205,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardId, String cardStatus, bool locked, String errorMessage, bool isUpdatingCard, String cardStatusError, bool cardStatusSuccess, bool showPin, String selectedStatus, String pin, bool isSigning, bool isLoadingTransactions, List<WalletTransactionEntity> transactions)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardId, String cardStatus, bool locked, String errorMessage, bool isUpdatingCard, String cardStatusError, bool cardStatusSuccess, bool showPin, String selectedStatus, String pin, bool isSigning, bool isLoadingTransactions, List<List<WalletTransactionEntity>> transactionPages, String? nextCursor, bool isLoadingMore, int currentPage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ChildWalletViewState() when $default != null:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.cardId,_that.cardStatus,_that.locked,_that.errorMessage,_that.isUpdatingCard,_that.cardStatusError,_that.cardStatusSuccess,_that.showPin,_that.selectedStatus,_that.pin,_that.isSigning,_that.isLoadingTransactions,_that.transactions);case _:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.cardId,_that.cardStatus,_that.locked,_that.errorMessage,_that.isUpdatingCard,_that.cardStatusError,_that.cardStatusSuccess,_that.showPin,_that.selectedStatus,_that.pin,_that.isSigning,_that.isLoadingTransactions,_that.transactionPages,_that.nextCursor,_that.isLoadingMore,_that.currentPage);case _:
return orElse();
}
@@ -223,10 +226,10 @@ return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.devic
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardId, String cardStatus, bool locked, String errorMessage, bool isUpdatingCard, String cardStatusError, bool cardStatusSuccess, bool showPin, String selectedStatus, String pin, bool isSigning, bool isLoadingTransactions, List<WalletTransactionEntity> transactions) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardId, String cardStatus, bool locked, String errorMessage, bool isUpdatingCard, String cardStatusError, bool cardStatusSuccess, bool showPin, String selectedStatus, String pin, bool isSigning, bool isLoadingTransactions, List<List<WalletTransactionEntity>> transactionPages, String? nextCursor, bool isLoadingMore, int currentPage) $default,) {final _that = this;
switch (_that) {
case _ChildWalletViewState():
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.cardId,_that.cardStatus,_that.locked,_that.errorMessage,_that.isUpdatingCard,_that.cardStatusError,_that.cardStatusSuccess,_that.showPin,_that.selectedStatus,_that.pin,_that.isSigning,_that.isLoadingTransactions,_that.transactions);case _:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.cardId,_that.cardStatus,_that.locked,_that.errorMessage,_that.isUpdatingCard,_that.cardStatusError,_that.cardStatusSuccess,_that.showPin,_that.selectedStatus,_that.pin,_that.isSigning,_that.isLoadingTransactions,_that.transactionPages,_that.nextCursor,_that.isLoadingMore,_that.currentPage);case _:
throw StateError('Unexpected subclass');
}
@@ -243,10 +246,10 @@ return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.devic
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardId, String cardStatus, bool locked, String errorMessage, bool isUpdatingCard, String cardStatusError, bool cardStatusSuccess, bool showPin, String selectedStatus, String pin, bool isSigning, bool isLoadingTransactions, List<WalletTransactionEntity> transactions)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardId, String cardStatus, bool locked, String errorMessage, bool isUpdatingCard, String cardStatusError, bool cardStatusSuccess, bool showPin, String selectedStatus, String pin, bool isSigning, bool isLoadingTransactions, List<List<WalletTransactionEntity>> transactionPages, String? nextCursor, bool isLoadingMore, int currentPage)? $default,) {final _that = this;
switch (_that) {
case _ChildWalletViewState() when $default != null:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.cardId,_that.cardStatus,_that.locked,_that.errorMessage,_that.isUpdatingCard,_that.cardStatusError,_that.cardStatusSuccess,_that.showPin,_that.selectedStatus,_that.pin,_that.isSigning,_that.isLoadingTransactions,_that.transactions);case _:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.cardId,_that.cardStatus,_that.locked,_that.errorMessage,_that.isUpdatingCard,_that.cardStatusError,_that.cardStatusSuccess,_that.showPin,_that.selectedStatus,_that.pin,_that.isSigning,_that.isLoadingTransactions,_that.transactionPages,_that.nextCursor,_that.isLoadingMore,_that.currentPage);case _:
return null;
}
@@ -258,7 +261,7 @@ return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.devic
class _ChildWalletViewState implements ChildWalletViewState {
const _ChildWalletViewState({this.isLoading = true, this.childProfile, this.childWallet, this.device, this.cardId = '', this.cardStatus = '', this.locked = false, this.errorMessage = '', this.isUpdatingCard = false, this.cardStatusError = '', this.cardStatusSuccess = false, this.showPin = false, this.selectedStatus = '', this.pin = '', this.isSigning = false, this.isLoadingTransactions = false, final List<WalletTransactionEntity> transactions = const []}): _transactions = transactions;
const _ChildWalletViewState({this.isLoading = true, this.childProfile, this.childWallet, this.device, this.cardId = '', this.cardStatus = '', this.locked = false, this.errorMessage = '', this.isUpdatingCard = false, this.cardStatusError = '', this.cardStatusSuccess = false, this.showPin = false, this.selectedStatus = '', this.pin = '', this.isSigning = false, this.isLoadingTransactions = false, final List<List<WalletTransactionEntity>> transactionPages = const [], this.nextCursor, this.isLoadingMore = false, this.currentPage = 0}): _transactionPages = transactionPages;
@override@JsonKey() final bool isLoading;
@@ -277,13 +280,16 @@ class _ChildWalletViewState implements ChildWalletViewState {
@override@JsonKey() final String pin;
@override@JsonKey() final bool isSigning;
@override@JsonKey() final bool isLoadingTransactions;
final List<WalletTransactionEntity> _transactions;
@override@JsonKey() List<WalletTransactionEntity> get transactions {
if (_transactions is EqualUnmodifiableListView) return _transactions;
final List<List<WalletTransactionEntity>> _transactionPages;
@override@JsonKey() List<List<WalletTransactionEntity>> get transactionPages {
if (_transactionPages is EqualUnmodifiableListView) return _transactionPages;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_transactions);
return EqualUnmodifiableListView(_transactionPages);
}
@override final String? nextCursor;
@override@JsonKey() final bool isLoadingMore;
@override@JsonKey() final int currentPage;
/// Create a copy of ChildWalletViewState
/// with the given fields replaced by the non-null parameter values.
@@ -295,16 +301,16 @@ _$ChildWalletViewStateCopyWith<_ChildWalletViewState> get copyWith => __$ChildWa
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChildWalletViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.childProfile, childProfile) || other.childProfile == childProfile)&&(identical(other.childWallet, childWallet) || other.childWallet == childWallet)&&(identical(other.device, device) || other.device == device)&&(identical(other.cardId, cardId) || other.cardId == cardId)&&(identical(other.cardStatus, cardStatus) || other.cardStatus == cardStatus)&&(identical(other.locked, locked) || other.locked == locked)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isUpdatingCard, isUpdatingCard) || other.isUpdatingCard == isUpdatingCard)&&(identical(other.cardStatusError, cardStatusError) || other.cardStatusError == cardStatusError)&&(identical(other.cardStatusSuccess, cardStatusSuccess) || other.cardStatusSuccess == cardStatusSuccess)&&(identical(other.showPin, showPin) || other.showPin == showPin)&&(identical(other.selectedStatus, selectedStatus) || other.selectedStatus == selectedStatus)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.isSigning, isSigning) || other.isSigning == isSigning)&&(identical(other.isLoadingTransactions, isLoadingTransactions) || other.isLoadingTransactions == isLoadingTransactions)&&const DeepCollectionEquality().equals(other._transactions, _transactions));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChildWalletViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.childProfile, childProfile) || other.childProfile == childProfile)&&(identical(other.childWallet, childWallet) || other.childWallet == childWallet)&&(identical(other.device, device) || other.device == device)&&(identical(other.cardId, cardId) || other.cardId == cardId)&&(identical(other.cardStatus, cardStatus) || other.cardStatus == cardStatus)&&(identical(other.locked, locked) || other.locked == locked)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isUpdatingCard, isUpdatingCard) || other.isUpdatingCard == isUpdatingCard)&&(identical(other.cardStatusError, cardStatusError) || other.cardStatusError == cardStatusError)&&(identical(other.cardStatusSuccess, cardStatusSuccess) || other.cardStatusSuccess == cardStatusSuccess)&&(identical(other.showPin, showPin) || other.showPin == showPin)&&(identical(other.selectedStatus, selectedStatus) || other.selectedStatus == selectedStatus)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.isSigning, isSigning) || other.isSigning == isSigning)&&(identical(other.isLoadingTransactions, isLoadingTransactions) || other.isLoadingTransactions == isLoadingTransactions)&&const DeepCollectionEquality().equals(other._transactionPages, _transactionPages)&&(identical(other.nextCursor, nextCursor) || other.nextCursor == nextCursor)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,childProfile,childWallet,device,cardId,cardStatus,locked,errorMessage,isUpdatingCard,cardStatusError,cardStatusSuccess,showPin,selectedStatus,pin,isSigning,isLoadingTransactions,const DeepCollectionEquality().hash(_transactions));
int get hashCode => Object.hashAll([runtimeType,isLoading,childProfile,childWallet,device,cardId,cardStatus,locked,errorMessage,isUpdatingCard,cardStatusError,cardStatusSuccess,showPin,selectedStatus,pin,isSigning,isLoadingTransactions,const DeepCollectionEquality().hash(_transactionPages),nextCursor,isLoadingMore,currentPage]);
@override
String toString() {
return 'ChildWalletViewState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, cardId: $cardId, cardStatus: $cardStatus, locked: $locked, errorMessage: $errorMessage, isUpdatingCard: $isUpdatingCard, cardStatusError: $cardStatusError, cardStatusSuccess: $cardStatusSuccess, showPin: $showPin, selectedStatus: $selectedStatus, pin: $pin, isSigning: $isSigning, isLoadingTransactions: $isLoadingTransactions, transactions: $transactions)';
return 'ChildWalletViewState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, cardId: $cardId, cardStatus: $cardStatus, locked: $locked, errorMessage: $errorMessage, isUpdatingCard: $isUpdatingCard, cardStatusError: $cardStatusError, cardStatusSuccess: $cardStatusSuccess, showPin: $showPin, selectedStatus: $selectedStatus, pin: $pin, isSigning: $isSigning, isLoadingTransactions: $isLoadingTransactions, transactionPages: $transactionPages, nextCursor: $nextCursor, isLoadingMore: $isLoadingMore, currentPage: $currentPage)';
}
@@ -315,7 +321,7 @@ abstract mixin class _$ChildWalletViewStateCopyWith<$Res> implements $ChildWalle
factory _$ChildWalletViewStateCopyWith(_ChildWalletViewState value, $Res Function(_ChildWalletViewState) _then) = __$ChildWalletViewStateCopyWithImpl;
@override @useResult
$Res call({
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardId, String cardStatus, bool locked, String errorMessage, bool isUpdatingCard, String cardStatusError, bool cardStatusSuccess, bool showPin, String selectedStatus, String pin, bool isSigning, bool isLoadingTransactions, List<WalletTransactionEntity> transactions
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardId, String cardStatus, bool locked, String errorMessage, bool isUpdatingCard, String cardStatusError, bool cardStatusSuccess, bool showPin, String selectedStatus, String pin, bool isSigning, bool isLoadingTransactions, List<List<WalletTransactionEntity>> transactionPages, String? nextCursor, bool isLoadingMore, int currentPage
});
@@ -332,7 +338,7 @@ class __$ChildWalletViewStateCopyWithImpl<$Res>
/// Create a copy of ChildWalletViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? childProfile = freezed,Object? childWallet = freezed,Object? device = freezed,Object? cardId = null,Object? cardStatus = null,Object? locked = null,Object? errorMessage = null,Object? isUpdatingCard = null,Object? cardStatusError = null,Object? cardStatusSuccess = null,Object? showPin = null,Object? selectedStatus = null,Object? pin = null,Object? isSigning = null,Object? isLoadingTransactions = null,Object? transactions = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? childProfile = freezed,Object? childWallet = freezed,Object? device = freezed,Object? cardId = null,Object? cardStatus = null,Object? locked = null,Object? errorMessage = null,Object? isUpdatingCard = null,Object? cardStatusError = null,Object? cardStatusSuccess = null,Object? showPin = null,Object? selectedStatus = null,Object? pin = null,Object? isSigning = null,Object? isLoadingTransactions = null,Object? transactionPages = null,Object? nextCursor = freezed,Object? isLoadingMore = null,Object? currentPage = null,}) {
return _then(_ChildWalletViewState(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,childProfile: freezed == childProfile ? _self.childProfile : childProfile // ignore: cast_nullable_to_non_nullable
@@ -350,8 +356,11 @@ as bool,selectedStatus: null == selectedStatus ? _self.selectedStatus : selected
as String,pin: null == pin ? _self.pin : pin // ignore: cast_nullable_to_non_nullable
as String,isSigning: null == isSigning ? _self.isSigning : isSigning // ignore: cast_nullable_to_non_nullable
as bool,isLoadingTransactions: null == isLoadingTransactions ? _self.isLoadingTransactions : isLoadingTransactions // ignore: cast_nullable_to_non_nullable
as bool,transactions: null == transactions ? _self._transactions : transactions // ignore: cast_nullable_to_non_nullable
as List<WalletTransactionEntity>,
as bool,transactionPages: null == transactionPages ? _self._transactionPages : transactionPages // ignore: cast_nullable_to_non_nullable
as List<List<WalletTransactionEntity>>,nextCursor: freezed == nextCursor ? _self.nextCursor : nextCursor // ignore: cast_nullable_to_non_nullable
as String?,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
as bool,currentPage: null == currentPage ? _self.currentPage : currentPage // ignore: cast_nullable_to_non_nullable
as int,
));
}

View File

@@ -2,7 +2,6 @@ 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,8 +99,7 @@ class DepositViewModel extends Notifier<DepositViewState> {
);
if (!ref.mounted) return;
ref.read(childDataProvider(childId).notifier).load();
ref.read(homeViewModelProvider.notifier).refreshChildWallet(childId);
ref.read(walletRefreshProvider.notifier).refresh();
await ref.read(parentWalletBalanceProvider.notifier).refresh();
if (!ref.mounted) return;
state = state.copyWith(isSubmitting: false, success: true);

View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:navigation/navigation.dart';
import 'presentation/edit_child_profile_screen.dart';
class EditChildProfileBuilder {
const EditChildProfileBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final childWalletId = state.pathParameters['childWalletId'] ?? '';
final navigationContract = GetIt.I<NavigationContract>();
return MaterialPage(
key: state.pageKey,
child: EditChildProfileScreen(
childId: childWalletId,
navigation: navigationContract,
),
);
}
}

View File

@@ -0,0 +1,191 @@
import 'package:auth/auth.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'edit_child_profile_view_model.dart';
class EditChildProfileScreen extends ConsumerWidget {
final String childId;
final NavigationContract navigation;
const EditChildProfileScreen({
super.key,
required this.childId,
required this.navigation,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(editChildProfileViewModelProvider(childId));
final viewModel =
ref.read(editChildProfileViewModelProvider(childId).notifier);
ref.listen(editChildProfileViewModelProvider(childId), (prev, next) {
if (next.saveSuccess && !(prev?.saveSuccess ?? false)) {
showTopSnackbar(
context,
message: context.translate(I18n.editChildProfileSaveSuccess),
type: MessageType.success,
);
navigation.goBack();
}
if (next.errorMessage.isNotEmpty &&
!next.showPin &&
next.errorMessage != (prev?.errorMessage ?? '')) {
showTopSnackbar(
context,
message: next.errorMessage,
type: MessageType.error,
);
}
});
if (viewState.isLoading) {
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
body: const Center(child: AppLoadingIndicator()),
);
}
if (viewState.showPin) {
return _buildPinScaffold(context, theme, viewState, viewModel);
}
return _buildFormScaffold(context, theme, viewState, viewModel);
}
Widget _buildPinScaffold(
BuildContext context,
ThemePort theme,
EditChildProfileViewState viewState,
EditChildProfileViewModel viewModel,
) {
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
appBar: AppBar(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: theme.getColorFor(ThemeCode.textPrimary),
),
onPressed: viewModel.cancelPin,
),
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: ScaPinView(
title: context.translate(I18n.scaPinEnter),
pin: viewState.pin,
isProcessing: viewState.isSigning || viewState.isSaving,
processingText: context.translate(I18n.scaSigning),
canSubmit: viewModel.canSubmitPin,
submitText: context.translate(I18n.scaConnect),
clearPinText: context.translate(I18n.scaClearPin),
onDigitPressed: viewModel.onDigitPressed,
onBackspacePressed: viewModel.onBackspacePressed,
onClearPin: viewModel.onClearPin,
onSubmit: () => viewModel.onPinSubmit(),
),
),
),
if (viewState.errorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
viewState.errorMessage,
style: const TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
),
TextButton(
onPressed: viewModel.cancelPin,
child: Text(context.translate(I18n.cancel)),
),
],
),
),
);
}
Widget _buildFormScaffold(
BuildContext context,
ThemePort theme,
EditChildProfileViewState viewState,
EditChildProfileViewModel viewModel,
) {
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
appBar: AppBar(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: theme.getColorFor(ThemeCode.textPrimary),
),
onPressed: () => navigation.goBack(),
),
title: Text(
context.translate(I18n.editChildProfileTitle),
style: TextStyle(color: theme.getColorFor(ThemeCode.textPrimary)),
),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Expanded(
child: ListView(
children: [
TextFormField(
initialValue: viewState.firstName,
decoration: InputDecoration(
labelText: context.translate(I18n.firstNameLabel),
border: const OutlineInputBorder(),
),
onChanged: viewModel.setFirstName,
),
const SizedBox(height: 16),
TextFormField(
initialValue: viewState.lastName,
decoration: InputDecoration(
labelText: context.translate(I18n.lastNameLabel),
border: const OutlineInputBorder(),
),
onChanged: viewModel.setLastName,
),
const SizedBox(height: 16),
TextFormField(
initialValue: viewState.address,
decoration: InputDecoration(
labelText: context.translate(I18n.streetLabel),
border: const OutlineInputBorder(),
),
onChanged: viewModel.setAddress,
),
],
),
),
PrimaryButton(
onPressed: () => viewModel.requestPin(),
text: context.translate(I18n.profileSettingsSave),
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
TextButton(
onPressed: () => navigation.goBack(),
child: Text(context.translate(I18n.cancel)),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,155 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get_it/get_it.dart';
import 'package:sca_treezor/sca_treezor.dart';
import 'package:sf_shared/sf_shared.dart';
import '../../child_wallet/child_data_provider.dart';
export 'edit_child_profile_view_state.dart';
import 'edit_child_profile_view_state.dart';
final editChildProfileViewModelProvider = NotifierProvider.autoDispose
.family<EditChildProfileViewModel, EditChildProfileViewState, String>(
EditChildProfileViewModel.new,
);
class EditChildProfileViewModel extends Notifier<EditChildProfileViewState> {
final String childId;
EditChildProfileViewModel(this.childId);
late TreezorWalletConnectionService _connectionService;
late TreezorWalletSignatureService _signatureService;
@override
EditChildProfileViewState build() {
_connectionService = GetIt.I<TreezorWalletConnectionService>();
_signatureService = GetIt.I<TreezorWalletSignatureService>();
Future.microtask(() => _loadChildProfile());
return const EditChildProfileViewState();
}
Future<void> _loadChildProfile() async {
state = state.copyWith(isLoading: true, errorMessage: '');
try {
final childData = ref.read(childDataProvider(childId));
var childProfile = childData.childProfile;
if (childProfile == null) {
final profiles =
await ref.read(userRepositoryProvider).getChildProfiles();
childProfile = profiles.where((p) => p.id == childId).firstOrNull;
if (childProfile == null) {
state = state.copyWith(
isLoading: false,
errorMessage: 'Child profile not found',
);
return;
}
if (!ref.mounted) return;
}
state = state.copyWith(
isLoading: false,
firstName: childProfile.firstName,
lastName: childProfile.lastName,
address: childProfile.address,
childProfileId: childProfile.id,
treezorUserId: childProfile.treezorUserId,
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoading: false, errorMessage: e.toString());
}
}
void setFirstName(String value) =>
state = state.copyWith(firstName: value, saveSuccess: false);
void setLastName(String value) =>
state = state.copyWith(lastName: value, saveSuccess: false);
void setAddress(String value) =>
state = state.copyWith(address: value, saveSuccess: false);
void requestPin() {
state = state.copyWith(showPin: true, pin: '', errorMessage: '');
}
void cancelPin() {
state = state.copyWith(showPin: false, pin: '');
}
void onDigitPressed(String digit) {
if (state.pin.length >= 6) return;
state = state.copyWith(pin: state.pin + digit, errorMessage: '');
}
void onBackspacePressed() {
if (state.pin.isEmpty) return;
state = state.copyWith(pin: state.pin.substring(0, state.pin.length - 1));
}
void onClearPin() {
state = state.copyWith(pin: '');
}
bool get canSubmitPin => state.pin.length == 6;
Future<void> onPinSubmit() async {
state = state.copyWith(isSigning: true, errorMessage: '');
try {
await _connectionService.connectWithPin(loginPin: state.pin);
if (!ref.mounted) return;
final scaProof = await _generateScaProof();
if (!ref.mounted) return;
state = state.copyWith(isSigning: false, isSaving: true, pin: '');
await ref.read(userRepositoryProvider).updateChildProfile(
childProfileId: state.childProfileId,
scaProof: scaProof,
firstName: state.firstName,
lastName: state.lastName,
address: state.address,
);
if (!ref.mounted) return;
ref.read(walletRefreshProvider.notifier).refresh();
state = state.copyWith(
isSaving: false,
saveSuccess: true,
showPin: false,
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isSigning: false,
isSaving: false,
pin: '',
errorMessage: e.toString(),
);
}
}
Future<String> _generateScaProof() async {
final url =
'https://savefamily.sandbox.treezor.co/v1/users/${state.treezorUserId}';
final scaBody = <String, dynamic>{
'firstName': state.firstName,
'lastName': state.lastName,
};
return _signatureService.generateJwsWithPin(
message: '',
input: jsonEncode({'url': url, 'body': scaBody}),
pin: state.pin,
);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'edit_child_profile_view_state.freezed.dart';
@freezed
abstract class EditChildProfileViewState with _$EditChildProfileViewState {
const factory EditChildProfileViewState({
@Default(true) bool isLoading,
@Default(false) bool isSaving,
@Default(false) bool isSigning,
@Default(false) bool showPin,
@Default('') String pin,
@Default('') String firstName,
@Default('') String lastName,
@Default('') String address,
@Default('') String childProfileId,
@Default('') String treezorUserId,
@Default('') String errorMessage,
@Default(false) bool saveSuccess,
}) = _EditChildProfileViewState;
}

View File

@@ -0,0 +1,304 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'edit_child_profile_view_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$EditChildProfileViewState {
bool get isLoading; bool get isSaving; bool get isSigning; bool get showPin; String get pin; String get firstName; String get lastName; String get address; String get childProfileId; String get treezorUserId; String get errorMessage; bool get saveSuccess;
/// Create a copy of EditChildProfileViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$EditChildProfileViewStateCopyWith<EditChildProfileViewState> get copyWith => _$EditChildProfileViewStateCopyWithImpl<EditChildProfileViewState>(this as EditChildProfileViewState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is EditChildProfileViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.isSigning, isSigning) || other.isSigning == isSigning)&&(identical(other.showPin, showPin) || other.showPin == showPin)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.address, address) || other.address == address)&&(identical(other.childProfileId, childProfileId) || other.childProfileId == childProfileId)&&(identical(other.treezorUserId, treezorUserId) || other.treezorUserId == treezorUserId)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.saveSuccess, saveSuccess) || other.saveSuccess == saveSuccess));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,isSaving,isSigning,showPin,pin,firstName,lastName,address,childProfileId,treezorUserId,errorMessage,saveSuccess);
@override
String toString() {
return 'EditChildProfileViewState(isLoading: $isLoading, isSaving: $isSaving, isSigning: $isSigning, showPin: $showPin, pin: $pin, firstName: $firstName, lastName: $lastName, address: $address, childProfileId: $childProfileId, treezorUserId: $treezorUserId, errorMessage: $errorMessage, saveSuccess: $saveSuccess)';
}
}
/// @nodoc
abstract mixin class $EditChildProfileViewStateCopyWith<$Res> {
factory $EditChildProfileViewStateCopyWith(EditChildProfileViewState value, $Res Function(EditChildProfileViewState) _then) = _$EditChildProfileViewStateCopyWithImpl;
@useResult
$Res call({
bool isLoading, bool isSaving, bool isSigning, bool showPin, String pin, String firstName, String lastName, String address, String childProfileId, String treezorUserId, String errorMessage, bool saveSuccess
});
}
/// @nodoc
class _$EditChildProfileViewStateCopyWithImpl<$Res>
implements $EditChildProfileViewStateCopyWith<$Res> {
_$EditChildProfileViewStateCopyWithImpl(this._self, this._then);
final EditChildProfileViewState _self;
final $Res Function(EditChildProfileViewState) _then;
/// Create a copy of EditChildProfileViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? isSaving = null,Object? isSigning = null,Object? showPin = null,Object? pin = null,Object? firstName = null,Object? lastName = null,Object? address = null,Object? childProfileId = null,Object? treezorUserId = null,Object? errorMessage = null,Object? saveSuccess = null,}) {
return _then(_self.copyWith(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
as bool,isSigning: null == isSigning ? _self.isSigning : isSigning // ignore: cast_nullable_to_non_nullable
as bool,showPin: null == showPin ? _self.showPin : showPin // ignore: cast_nullable_to_non_nullable
as bool,pin: null == pin ? _self.pin : pin // ignore: cast_nullable_to_non_nullable
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String,lastName: null == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String,address: null == address ? _self.address : address // ignore: cast_nullable_to_non_nullable
as String,childProfileId: null == childProfileId ? _self.childProfileId : childProfileId // ignore: cast_nullable_to_non_nullable
as String,treezorUserId: null == treezorUserId ? _self.treezorUserId : treezorUserId // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,saveSuccess: null == saveSuccess ? _self.saveSuccess : saveSuccess // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [EditChildProfileViewState].
extension EditChildProfileViewStatePatterns on EditChildProfileViewState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _EditChildProfileViewState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _EditChildProfileViewState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _EditChildProfileViewState value) $default,){
final _that = this;
switch (_that) {
case _EditChildProfileViewState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _EditChildProfileViewState value)? $default,){
final _that = this;
switch (_that) {
case _EditChildProfileViewState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, bool isSaving, bool isSigning, bool showPin, String pin, String firstName, String lastName, String address, String childProfileId, String treezorUserId, String errorMessage, bool saveSuccess)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _EditChildProfileViewState() when $default != null:
return $default(_that.isLoading,_that.isSaving,_that.isSigning,_that.showPin,_that.pin,_that.firstName,_that.lastName,_that.address,_that.childProfileId,_that.treezorUserId,_that.errorMessage,_that.saveSuccess);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, bool isSaving, bool isSigning, bool showPin, String pin, String firstName, String lastName, String address, String childProfileId, String treezorUserId, String errorMessage, bool saveSuccess) $default,) {final _that = this;
switch (_that) {
case _EditChildProfileViewState():
return $default(_that.isLoading,_that.isSaving,_that.isSigning,_that.showPin,_that.pin,_that.firstName,_that.lastName,_that.address,_that.childProfileId,_that.treezorUserId,_that.errorMessage,_that.saveSuccess);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, bool isSaving, bool isSigning, bool showPin, String pin, String firstName, String lastName, String address, String childProfileId, String treezorUserId, String errorMessage, bool saveSuccess)? $default,) {final _that = this;
switch (_that) {
case _EditChildProfileViewState() when $default != null:
return $default(_that.isLoading,_that.isSaving,_that.isSigning,_that.showPin,_that.pin,_that.firstName,_that.lastName,_that.address,_that.childProfileId,_that.treezorUserId,_that.errorMessage,_that.saveSuccess);case _:
return null;
}
}
}
/// @nodoc
class _EditChildProfileViewState implements EditChildProfileViewState {
const _EditChildProfileViewState({this.isLoading = true, this.isSaving = false, this.isSigning = false, this.showPin = false, this.pin = '', this.firstName = '', this.lastName = '', this.address = '', this.childProfileId = '', this.treezorUserId = '', this.errorMessage = '', this.saveSuccess = false});
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isSaving;
@override@JsonKey() final bool isSigning;
@override@JsonKey() final bool showPin;
@override@JsonKey() final String pin;
@override@JsonKey() final String firstName;
@override@JsonKey() final String lastName;
@override@JsonKey() final String address;
@override@JsonKey() final String childProfileId;
@override@JsonKey() final String treezorUserId;
@override@JsonKey() final String errorMessage;
@override@JsonKey() final bool saveSuccess;
/// Create a copy of EditChildProfileViewState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$EditChildProfileViewStateCopyWith<_EditChildProfileViewState> get copyWith => __$EditChildProfileViewStateCopyWithImpl<_EditChildProfileViewState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _EditChildProfileViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.isSigning, isSigning) || other.isSigning == isSigning)&&(identical(other.showPin, showPin) || other.showPin == showPin)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.address, address) || other.address == address)&&(identical(other.childProfileId, childProfileId) || other.childProfileId == childProfileId)&&(identical(other.treezorUserId, treezorUserId) || other.treezorUserId == treezorUserId)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.saveSuccess, saveSuccess) || other.saveSuccess == saveSuccess));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,isSaving,isSigning,showPin,pin,firstName,lastName,address,childProfileId,treezorUserId,errorMessage,saveSuccess);
@override
String toString() {
return 'EditChildProfileViewState(isLoading: $isLoading, isSaving: $isSaving, isSigning: $isSigning, showPin: $showPin, pin: $pin, firstName: $firstName, lastName: $lastName, address: $address, childProfileId: $childProfileId, treezorUserId: $treezorUserId, errorMessage: $errorMessage, saveSuccess: $saveSuccess)';
}
}
/// @nodoc
abstract mixin class _$EditChildProfileViewStateCopyWith<$Res> implements $EditChildProfileViewStateCopyWith<$Res> {
factory _$EditChildProfileViewStateCopyWith(_EditChildProfileViewState value, $Res Function(_EditChildProfileViewState) _then) = __$EditChildProfileViewStateCopyWithImpl;
@override @useResult
$Res call({
bool isLoading, bool isSaving, bool isSigning, bool showPin, String pin, String firstName, String lastName, String address, String childProfileId, String treezorUserId, String errorMessage, bool saveSuccess
});
}
/// @nodoc
class __$EditChildProfileViewStateCopyWithImpl<$Res>
implements _$EditChildProfileViewStateCopyWith<$Res> {
__$EditChildProfileViewStateCopyWithImpl(this._self, this._then);
final _EditChildProfileViewState _self;
final $Res Function(_EditChildProfileViewState) _then;
/// Create a copy of EditChildProfileViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? isSaving = null,Object? isSigning = null,Object? showPin = null,Object? pin = null,Object? firstName = null,Object? lastName = null,Object? address = null,Object? childProfileId = null,Object? treezorUserId = null,Object? errorMessage = null,Object? saveSuccess = null,}) {
return _then(_EditChildProfileViewState(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable
as bool,isSigning: null == isSigning ? _self.isSigning : isSigning // ignore: cast_nullable_to_non_nullable
as bool,showPin: null == showPin ? _self.showPin : showPin // ignore: cast_nullable_to_non_nullable
as bool,pin: null == pin ? _self.pin : pin // ignore: cast_nullable_to_non_nullable
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String,lastName: null == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String,address: null == address ? _self.address : address // ignore: cast_nullable_to_non_nullable
as String,childProfileId: null == childProfileId ? _self.childProfileId : childProfileId // ignore: cast_nullable_to_non_nullable
as String,treezorUserId: null == treezorUserId ? _self.treezorUserId : treezorUserId // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,saveSuccess: null == saveSuccess ? _self.saveSuccess : saveSuccess // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

View File

@@ -2,7 +2,6 @@ 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,8 +91,7 @@ class ExtractViewModel extends Notifier<ExtractViewState> {
);
if (!ref.mounted) return;
ref.read(childDataProvider(childId).notifier).load();
ref.read(homeViewModelProvider.notifier).refreshChildWallet(childId);
ref.read(walletRefreshProvider.notifier).refresh();
ref.read(parentWalletBalanceProvider.notifier).applyOptimisticPayin(amount);
state = state.copyWith(isSubmitting: false, success: true);
} catch (e) {