mcc groups, limits to expend, delete child device, card status, responsive states, added some overrides to AppDelegate, router modified, sca wallet fixes

This commit is contained in:
2026-02-26 14:59:51 +01:00
parent 0c93440f9b
commit 7849240ff2
81 changed files with 4320 additions and 2068 deletions

View File

@@ -0,0 +1,44 @@
import 'dart:ui';
import 'package:design_system/design_system.dart';
enum CardStatus {
unlock('UNLOCK', 0),
lock('LOCK', 1),
customLock('CUSTOM_LOCK', 1),
lockInternal('LOCK_INTERNAL', 1),
lost('LOST', 2),
stolen('STOLEN', 3),
destroyed('DESTROYED', 4),
expired('EXPIRED', 5);
final String value;
final int apiCode;
const CardStatus(this.value, this.apiCode);
bool get isLocked => this != CardStatus.unlock;
static CardStatus fromString(String status) {
return CardStatus.values.firstWhere(
(e) => e.value == status,
orElse: () => CardStatus.lock,
);
}
}
bool isCardLocked(String cardStatus) {
return cardStatus.isNotEmpty && cardStatus != CardStatus.unlock.value;
}
List<Color> cardColorsFor({
required ThemePort theme,
String? carrierGenre,
String cardStatus = '',
}) {
if (isCardLocked(cardStatus)) return theme.getDisabledCardColors();
return switch (carrierGenre) {
'F' => theme.getCardColorFor(0),
'M' => theme.getCardColorFor(1),
_ => theme.getDisabledCardColors(),
};
}

View File

@@ -6,6 +6,8 @@ import 'package:sf_localizations/sf_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../card_colors.dart';
import '../child_wallet/child_data_provider.dart';
import 'allowance_view_model.dart';
class AllowanceScreen extends ConsumerWidget {
@@ -18,13 +20,6 @@ class AllowanceScreen extends ConsumerWidget {
required this.navigation,
});
List<Color> _cardColors(String? carrierGenre, ThemePort theme) =>
switch (carrierGenre) {
'F' => theme.getCardColorFor(0),
'M' => theme.getCardColorFor(1),
_ => theme.getDisabledCardColors(),
};
List<String> _localizedWeekDays(BuildContext context) {
final locale = Localizations.localeOf(context).toString();
return List.generate(7, (i) {
@@ -39,6 +34,7 @@ class AllowanceScreen extends ConsumerWidget {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(allowanceViewModelProvider(childId));
final viewModel = ref.read(allowanceViewModelProvider(childId).notifier);
final cardStatus = ref.watch(childDataProvider(childId)).cardStatus;
final weekDays = _localizedWeekDays(context);
if (viewState.isLoading) {
@@ -58,201 +54,146 @@ class AllowanceScreen extends ConsumerWidget {
return WalletManagementLayout(
childName: childName,
balance: availableBalance,
cardColors: _cardColors(viewState.device?.carrierGenre, theme),
cardColors: cardColorsFor(theme: theme, carrierGenre: viewState.device?.carrierGenre, cardStatus: cardStatus),
navigation: navigation,
footer: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Column(
spacing: 10,
footer: FooterContainer(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
primaryColor: theme.getColorFor(ThemeCode.buttonPrimary),
primaryText: context.translate(I18n.allowanceActivateAutoAllowance),
onPrimaryPressed: () => {},
cancelText: context.translate(I18n.cancel),
onCancelPressed: () {},
),
children: [
SectionContainer(
padding: 10,
borderRadius: 20,
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
children: [
PrimaryButton(
onPressed: () => {},
text: context.translate(I18n.allowanceActivateAutoAllowance),
color: theme.getColorFor(ThemeCode.buttonPrimary),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.allowanceAutoAllowance),
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
),
TextButton(
onPressed: () {},
child: Text(context.translate(I18n.cancel)),
CustomTextField(
controller: viewModel.amountController,
keyboardType: TextInputType.number,
label: context.translate(I18n.depositAmountLabel),
hint: context.translate(I18n.depositAmountHint),
),
Text(
context.translate(
I18n.allowanceBalanceAfter,
args: {'amount': '30'},
),
),
],
),
),
children: [
Container(
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
padding: const EdgeInsets.all(10),
child: Column(
spacing: 10,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.allowanceAutoAllowance),
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
SectionContainer(
borderRadius: 20,
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
title: context.translate(I18n.allowanceFrequency),
subtitle: context.translate(I18n.allowanceFrequencyDescription),
children: [
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.translate(I18n.allowanceWeekly)),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.frequency == 'weekly',
onChanged: (_) => viewModel.selectFrequency('weekly'),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.translate(I18n.allowanceBiweekly)),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.frequency == 'biweekly',
onChanged: (_) => viewModel.selectFrequency('biweekly'),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.translate(I18n.allowanceMonthly)),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.frequency == 'monthly',
onChanged: (_) => viewModel.selectFrequency('monthly'),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
CustomDropdown(
items: weekDays.map((d) => Text(d)).toList(),
values: weekDays,
onChanged: (value) => viewModel.selectDayOfWeek(value ?? ''),
hint: context.translate(I18n.allowanceDayOfWeek),
),
CustomDropdown(
hint: context.translate(I18n.allowanceTimeOfDay),
items: List<Widget>.generate(24, (int index) {
return Text("$index:00");
}),
onChanged: (value) => viewModel.selectTimeOfDay(value ?? ''),
),
CustomTextField(
controller: viewModel.messageController,
lines: 3,
length: 150,
label: context.translate(
I18n.allowanceMessageLabel,
args: {'name': childName},
),
CustomTextField(
controller: viewModel.amountController,
keyboardType: TextInputType.number,
label: context.translate(I18n.depositAmountLabel),
hint: context.translate(I18n.depositAmountHint),
),
Text(
hint: context.translate(I18n.allowanceMessageHint),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(
I18n.allowanceBalanceAfter,
args: {'amount': '30'},
I18n.allowanceMaxChars,
args: {'count': '150'},
),
),
],
),
),
],
),
Container(
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
padding: const EdgeInsets.all(24),
child: Column(
spacing: 10,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.allowanceFrequency),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
SectionContainer(
borderRadius: 20,
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
title: context.translate(I18n.allowanceConditions),
subtitle: context.translate(I18n.allowanceConditionsDescription),
children: [
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(
context.translate(I18n.allowanceConditionWeeklyLimits),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.allowanceFrequencyDescription),
),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.conditionWeeklyLimits,
onChanged: (_) => viewModel.toggleConditionWeeklyLimits(),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(
context.translate(I18n.allowanceConditionNoIncidents),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.translate(I18n.allowanceWeekly)),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.frequency == 'weekly',
onChanged: (_) => viewModel.selectFrequency('weekly'),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.conditionNoIncidents,
onChanged: (_) => viewModel.toggleConditionNoIncidents(),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(
context.translate(I18n.allowanceConditionPauseHolidays),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.translate(I18n.allowanceBiweekly)),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.frequency == 'biweekly',
onChanged: (_) => viewModel.selectFrequency('biweekly'),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.translate(I18n.allowanceMonthly)),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.frequency == 'monthly',
onChanged: (_) => viewModel.selectFrequency('monthly'),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
CustomDropdown(
items: weekDays.map((d) => Text(d)).toList(),
values: weekDays,
onChanged: (value) => viewModel.selectDayOfWeek(value ?? ''),
hint: context.translate(I18n.allowanceDayOfWeek),
),
CustomDropdown(
hint: context.translate(I18n.allowanceTimeOfDay),
items: List<Widget>.generate(24, (int index) {
return Text("$index:00");
}),
onChanged: (value) => viewModel.selectTimeOfDay(value ?? ''),
),
CustomTextField(
controller: viewModel.messageController,
lines: 3,
length: 150,
label: context.translate(
I18n.allowanceMessageLabel,
args: {'name': childName},
),
hint: context.translate(I18n.allowanceMessageHint),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(
I18n.allowanceMaxChars,
args: {'count': '150'},
),
),
),
],
),
),
Container(
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
padding: const EdgeInsets.all(24),
child: Column(
spacing: 10,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.allowanceConditions),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.allowanceConditionsDescription),
),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(
context.translate(I18n.allowanceConditionWeeklyLimits),
),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.conditionWeeklyLimits,
onChanged: (_) => viewModel.toggleConditionWeeklyLimits(),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(
context.translate(I18n.allowanceConditionNoIncidents),
),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.conditionNoIncidents,
onChanged: (_) => viewModel.toggleConditionNoIncidents(),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(
context.translate(I18n.allowanceConditionPauseHolidays),
),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.conditionPauseHolidays,
onChanged: (_) => viewModel.toggleConditionPauseHolidays(),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
],
),
controlAffinity: ListTileControlAffinity.leading,
value: viewState.conditionPauseHolidays,
onChanged: (_) => viewModel.toggleConditionPauseHolidays(),
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
),
],
),
],
);

View File

@@ -39,7 +39,7 @@ class ChildDataNotifier extends Notifier<ChildDataState> {
final childProfile = profiles.where((p) => p.id == childId).firstOrNull;
if (childProfile == null) {
state = state.copyWith(isLoading: false, errorMessage: 'Child not found');
state = state.copyWith(isLoading: false, errorMessage: 'child_not_found');
return;
}
@@ -62,11 +62,22 @@ class ChildDataNotifier extends Notifier<ChildDataState> {
if (!ref.mounted) return;
String cardStatus = '';
try {
final card = await _treezorRepository.getCard(
walletId: childProfile.walletId,
);
cardStatus = card.status;
} catch (_) {}
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
childProfile: childProfile,
childWallet: childWallet,
device: device,
cardStatus: cardStatus,
);
} catch (e) {
if (!ref.mounted) return;

View File

@@ -10,6 +10,7 @@ abstract class ChildDataState with _$ChildDataState {
ChildProfileEntity? childProfile,
ChildWalletEntity? childWallet,
DeviceEntity? device,
@Default('') String cardStatus,
@Default('') String errorMessage,
}) = _ChildDataState;
}

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ChildDataState {
bool get isLoading; ChildProfileEntity? get childProfile; ChildWalletEntity? get childWallet; DeviceEntity? get device; String get errorMessage;
bool get isLoading; ChildProfileEntity? get childProfile; ChildWalletEntity? get childWallet; DeviceEntity? get device; String get cardStatus; String get errorMessage;
/// Create a copy of ChildDataState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $ChildDataStateCopyWith<ChildDataState> get copyWith => _$ChildDataStateCopyWith
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChildDataState&&(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.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChildDataState&&(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.cardStatus, cardStatus) || other.cardStatus == cardStatus)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,childProfile,childWallet,device,errorMessage);
int get hashCode => Object.hash(runtimeType,isLoading,childProfile,childWallet,device,cardStatus,errorMessage);
@override
String toString() {
return 'ChildDataState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, errorMessage: $errorMessage)';
return 'ChildDataState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, cardStatus: $cardStatus, errorMessage: $errorMessage)';
}
@@ -45,7 +45,7 @@ abstract mixin class $ChildDataStateCopyWith<$Res> {
factory $ChildDataStateCopyWith(ChildDataState value, $Res Function(ChildDataState) _then) = _$ChildDataStateCopyWithImpl;
@useResult
$Res call({
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String errorMessage
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardStatus, String errorMessage
});
@@ -62,13 +62,14 @@ class _$ChildDataStateCopyWithImpl<$Res>
/// Create a copy of ChildDataState
/// 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? errorMessage = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? childProfile = freezed,Object? childWallet = freezed,Object? device = freezed,Object? cardStatus = null,Object? errorMessage = 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
as ChildProfileEntity?,childWallet: freezed == childWallet ? _self.childWallet : childWallet // ignore: cast_nullable_to_non_nullable
as ChildWalletEntity?,device: freezed == device ? _self.device : device // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,cardStatus: null == cardStatus ? _self.cardStatus : cardStatus // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
));
}
@@ -190,10 +191,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardStatus, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ChildDataState() when $default != null:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.errorMessage);case _:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.cardStatus,_that.errorMessage);case _:
return orElse();
}
@@ -211,10 +212,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 errorMessage) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardStatus, String errorMessage) $default,) {final _that = this;
switch (_that) {
case _ChildDataState():
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.errorMessage);case _:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.cardStatus,_that.errorMessage);case _:
throw StateError('Unexpected subclass');
}
@@ -231,10 +232,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 errorMessage)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardStatus, String errorMessage)? $default,) {final _that = this;
switch (_that) {
case _ChildDataState() when $default != null:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.errorMessage);case _:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.cardStatus,_that.errorMessage);case _:
return null;
}
@@ -246,13 +247,14 @@ return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.devic
class _ChildDataState implements ChildDataState {
const _ChildDataState({this.isLoading = true, this.childProfile, this.childWallet, this.device, this.errorMessage = ''});
const _ChildDataState({this.isLoading = true, this.childProfile, this.childWallet, this.device, this.cardStatus = '', this.errorMessage = ''});
@override@JsonKey() final bool isLoading;
@override final ChildProfileEntity? childProfile;
@override final ChildWalletEntity? childWallet;
@override final DeviceEntity? device;
@override@JsonKey() final String cardStatus;
@override@JsonKey() final String errorMessage;
/// Create a copy of ChildDataState
@@ -265,16 +267,16 @@ _$ChildDataStateCopyWith<_ChildDataState> get copyWith => __$ChildDataStateCopyW
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChildDataState&&(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.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChildDataState&&(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.cardStatus, cardStatus) || other.cardStatus == cardStatus)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,childProfile,childWallet,device,errorMessage);
int get hashCode => Object.hash(runtimeType,isLoading,childProfile,childWallet,device,cardStatus,errorMessage);
@override
String toString() {
return 'ChildDataState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, errorMessage: $errorMessage)';
return 'ChildDataState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, cardStatus: $cardStatus, errorMessage: $errorMessage)';
}
@@ -285,7 +287,7 @@ abstract mixin class _$ChildDataStateCopyWith<$Res> implements $ChildDataStateCo
factory _$ChildDataStateCopyWith(_ChildDataState value, $Res Function(_ChildDataState) _then) = __$ChildDataStateCopyWithImpl;
@override @useResult
$Res call({
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String errorMessage
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, String cardStatus, String errorMessage
});
@@ -302,13 +304,14 @@ class __$ChildDataStateCopyWithImpl<$Res>
/// Create a copy of ChildDataState
/// 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? errorMessage = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? childProfile = freezed,Object? childWallet = freezed,Object? device = freezed,Object? cardStatus = null,Object? errorMessage = null,}) {
return _then(_ChildDataState(
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
as ChildProfileEntity?,childWallet: freezed == childWallet ? _self.childWallet : childWallet // ignore: cast_nullable_to_non_nullable
as ChildWalletEntity?,device: freezed == device ? _self.device : device // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,cardStatus: null == cardStatus ? _self.cardStatus : cardStatus // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
));
}

View File

@@ -6,6 +6,8 @@ import 'package:navigation/navigation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
import '../../card_colors.dart';
import '../../presentation/state/home_view_model.dart';
import 'child_wallet_view_model.dart';
import 'wallet_actions_bar.dart';
@@ -26,6 +28,10 @@ class ChildWalletScreen extends ConsumerWidget {
ref.listen(childWalletViewModelProvider(childId), (prev, next) {
if (next.cardStatusSuccess && !(prev?.cardStatusSuccess ?? false)) {
ref.read(homeViewModelProvider.notifier).updateChildCardStatus(
childId,
next.cardStatus,
);
showTopSnackbar(
context,
message: context.translate(I18n.cardStatusSuccess),
@@ -105,7 +111,11 @@ class ChildWalletScreen extends ConsumerWidget {
final locked = viewState.locked;
final availableBalance = childWallet?.authorizedBalance ?? 0;
final childName = childProfile.firstName;
final cardColors = _cardColors(device?.carrierGenre, theme);
final cardColors = cardColorsFor(
theme: theme,
carrierGenre: device?.carrierGenre,
cardStatus: viewState.cardStatus,
);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
@@ -176,39 +186,69 @@ class ChildWalletScreen extends ConsumerWidget {
minHeight: 10,
borderRadius: BorderRadius.all(Radius.circular(5)),
),
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,
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),
),
),
],
),
),
),
],
),
Column(
spacing: 16,
children: [
WalletActionsBar(childId: childId, navigation: navigation),
if (!locked)
WalletActionsBar(childId: childId, navigation: navigation),
Container(
padding: EdgeInsets.all(15),
decoration: BoxDecoration(
@@ -272,13 +312,6 @@ class ChildWalletScreen extends ConsumerWidget {
);
}
List<Color> _cardColors(String? carrierGenre, ThemePort theme) =>
switch (carrierGenre) {
'F' => theme.getCardColorFor(0),
'M' => theme.getCardColorFor(1),
_ => theme.getDisabledCardColors(),
};
Widget _buildGenderAvatar(String? carrierGenre, double size) {
final IconData icon;
final Color color;
@@ -314,6 +347,49 @@ class ChildWalletScreen extends ConsumerWidget {
builder: (_) => _CardStatusSheet(childId: childId),
);
}
void _showDeleteConfirmation(BuildContext context, WidgetRef ref) {
final theme = ref.read(themePortProvider);
showDialog(
context: context,
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(childId).notifier,
);
final success = await viewModel.deleteDevice();
if (success && context.mounted) {
ref.read(homeViewModelProvider.notifier).removeChild(childId);
showTopSnackbar(
context,
message: context.translate(I18n.deleteDeviceSuccess),
type: MessageType.success,
);
navigation.goBack();
}
},
child: Text(
context.translate(I18n.deleteDevice),
style: TextStyle(color: Colors.red),
),
),
],
),
);
}
}
class _CardStatusSheet extends ConsumerStatefulWidget {
@@ -325,15 +401,13 @@ class _CardStatusSheet extends ConsumerStatefulWidget {
}
class _CardStatusSheetState extends ConsumerState<_CardStatusSheet> {
String _selected = 'LOCK';
String? _selected;
static const _statuses = ['LOCK', 'UNLOCK', 'LOST', 'STOLEN'];
String _labelKey(String status) => switch (status) {
'LOCK' => I18n.cardStatusLock,
'UNLOCK' => I18n.cardStatusUnlock,
'LOST' => I18n.cardStatusLost,
'STOLEN' => I18n.cardStatusStolen,
String _labelKey(String status) => switch (CardStatus.fromString(status)) {
CardStatus.lock => I18n.cardStatusLock,
CardStatus.unlock => I18n.cardStatusUnlock,
CardStatus.lost => I18n.cardStatusLost,
CardStatus.stolen => I18n.cardStatusStolen,
_ => status,
};
@@ -341,6 +415,11 @@ class _CardStatusSheetState extends ConsumerState<_CardStatusSheet> {
Widget build(BuildContext context) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(childWalletViewModelProvider(widget.childId));
final currentStatus = viewState.cardStatus;
final statuses = CardStatus.fromString(currentStatus) == CardStatus.unlock
? [CardStatus.lock.value, CardStatus.lost.value, CardStatus.stolen.value]
: [CardStatus.unlock.value];
_selected ??= statuses.first;
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 40),
@@ -352,7 +431,7 @@ class _CardStatusSheetState extends ConsumerState<_CardStatusSheet> {
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
const SizedBox(height: 20),
..._statuses.map(
...statuses.map(
(status) => RadioListTile<String>(
title: Text(context.translate(_labelKey(status))),
value: status,
@@ -369,7 +448,7 @@ class _CardStatusSheetState extends ConsumerState<_CardStatusSheet> {
Navigator.of(context).pop();
ref
.read(childWalletViewModelProvider(widget.childId).notifier)
.selectCardStatus(_selected);
.selectCardStatus(_selected!);
},
text: context.translate(I18n.cardStatusConfirm),
color: theme.getColorFor(ThemeCode.buttonPrimary),

View File

@@ -6,6 +6,7 @@ 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';
@@ -18,10 +19,12 @@ class ChildWalletViewModel extends Notifier<ChildWalletViewState> {
final String childId;
ChildWalletViewModel(this.childId);
late final TreezorWalletConnectionService _connectionService;
late final TreezorWalletSignatureService _signatureService;
@override
ChildWalletViewState build() {
_connectionService = GetIt.I<TreezorWalletConnectionService>();
_signatureService = GetIt.I<TreezorWalletSignatureService>();
ref.listen(childDataProvider(childId), (prev, next) {
@@ -83,9 +86,9 @@ class ChildWalletViewModel extends Notifier<ChildWalletViewState> {
.getCard(walletId: walletId);
if (!ref.mounted) return;
state = state.copyWith(
cardId: card.cardId,
cardStatus: card.statusCode,
locked: card.statusCode != 'UNLOCK',
cardId: card.cardId.toString(),
cardStatus: card.status,
locked: CardStatus.fromString(card.status).isLocked,
);
} catch (_) {}
}
@@ -133,6 +136,9 @@ class ChildWalletViewModel extends Notifier<ChildWalletViewState> {
state = state.copyWith(isSigning: true, cardStatusError: '');
try {
await _connectionService.connectWithPin(loginPin: state.pin);
if (!ref.mounted) return;
final url =
'https://savefamily.sandbox.treezor.co/v1/cards/$cardId/LockUnlock';
final scaProof = await _signatureService.generateJwsWithPin(
@@ -160,7 +166,7 @@ class ChildWalletViewModel extends Notifier<ChildWalletViewState> {
isUpdatingCard: false,
cardStatusSuccess: true,
cardStatus: status,
locked: status == 'LOCK' || status == 'LOST' || status == 'STOLEN',
locked: CardStatus.fromString(status).isLocked,
showPin: false,
selectedStatus: '',
);
@@ -175,14 +181,26 @@ class ChildWalletViewModel extends Notifier<ChildWalletViewState> {
}
}
Future<bool> deleteDevice() async {
final deviceId = state.device?.id;
if (deviceId == null || deviceId.isEmpty) return false;
state = state.copyWith(isUpdatingCard: true, cardStatusError: '');
try {
await ref.read(userRepositoryProvider).deleteDevice(deviceId: deviceId);
if (!ref.mounted) return false;
state = state.copyWith(isUpdatingCard: false);
return true;
} catch (e) {
if (!ref.mounted) return false;
state = state.copyWith(isUpdatingCard: false, cardStatusError: e.toString());
return false;
}
}
static int _cardStatusToInt(String status) {
return switch (status) {
'UNLOCK' => 0,
'LOCK' => 1,
'LOST' => 2,
'STOLEN' => 3,
'DESTROYED' => 4,
_ => 1,
};
return CardStatus.fromString(status).apiCode;
}
}

View File

@@ -42,12 +42,12 @@ class WalletActionsBar extends ConsumerWidget {
// AppRoutes.allowance(childId),
// ),
// ),
// _actionButton(
// theme: theme,
// icon: Icons.list_alt_outlined,
// label: context.translate(I18n.walletActionLimits),
// onPressed: () => navigation.pushTo(AppRoutes.limits(childId)),
// ),
_actionButton(
theme: theme,
icon: Icons.list_alt_outlined,
label: context.translate(I18n.walletActionLimits),
onPressed: () => navigation.pushTo(AppRoutes.limits(childId)),
),
// _actionButton(
// theme: theme,
// icon: Icons.emoji_events_outlined,

View File

@@ -5,6 +5,8 @@ import 'package:home/src/presentation/wallet_management_layout.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
import '../../card_colors.dart';
import '../child_wallet/child_data_provider.dart';
import 'deposit_view_model.dart';
class DepositScreen extends ConsumerWidget {
@@ -17,18 +19,12 @@ class DepositScreen extends ConsumerWidget {
required this.navigation,
});
List<Color> _cardColors(String? carrierGenre, ThemePort theme) =>
switch (carrierGenre) {
'F' => theme.getCardColorFor(0),
'M' => theme.getCardColorFor(1),
_ => theme.getDisabledCardColors(),
};
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(depositViewModelProvider(childId));
final viewModel = ref.read(depositViewModelProvider(childId).notifier);
final cardStatus = ref.watch(childDataProvider(childId)).cardStatus;
ref.listen(depositViewModelProvider(childId), (prev, next) {
if (next.success && !(prev?.success ?? false)) {
@@ -66,74 +62,65 @@ class DepositScreen extends ConsumerWidget {
return WalletManagementLayout(
childName: childName,
balance: availableBalance,
cardColors: _cardColors(viewState.device?.carrierGenre, theme),
cardColors: cardColorsFor(theme: theme, carrierGenre: viewState.device?.carrierGenre, cardStatus: cardStatus),
navigation: navigation,
footer: Container(
padding: const EdgeInsets.all(10),
child: Column(
children: [
PrimaryButton(
onPressed: viewState.isSubmitting
? null
: () {
final amount = double.tryParse(
viewModel.amountController.text.trim(),
);
if (amount == null || amount <= 0) {
showTopSnackbar(
context,
message: context.translate(
I18n.walletMoveAmountRequired,
),
type: MessageType.warning,
);
return;
}
viewModel.submit();
},
text: viewState.isSubmitting
? context.translate(I18n.walletMoveProcessing)
: context.translate(I18n.depositAddMoney),
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
TextButton(
onPressed: () => navigation.goBack(),
child: Text(context.translate(I18n.cancel)),
),
],
),
footer: FooterContainer(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
primaryColor: theme.getColorFor(ThemeCode.buttonPrimary),
primaryText: viewState.isSubmitting
? context.translate(I18n.walletMoveProcessing)
: context.translate(I18n.depositAddMoney),
onPrimaryPressed: viewState.isSubmitting
? null
: () {
final amount = double.tryParse(
viewModel.amountController.text.trim(),
);
if (amount == null || amount <= 0) {
showTopSnackbar(
context,
message: context.translate(
I18n.walletMoveAmountRequired,
),
type: MessageType.warning,
);
return;
}
viewModel.submit();
},
cancelText: context.translate(I18n.cancel),
onCancelPressed: () => navigation.goBack(),
),
children: [
Container(
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
padding: const EdgeInsets.all(10),
child: Column(
spacing: 10,
children: [
Text(
context.translate(I18n.depositTitle),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
CustomTextField(
controller: viewModel.amountController,
keyboardType: TextInputType.number,
label: context.translate(I18n.depositAmountLabel),
hint: context.translate(I18n.depositAmountHint),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(
I18n.allowanceBalanceAfter,
args: {'amount': '30'},
),
SectionContainer(
padding: 10,
borderRadius: 20,
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
children: [
Text(
context.translate(I18n.depositTitle),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
CustomTextField(
controller: viewModel.amountController,
keyboardType: TextInputType.number,
label: context.translate(I18n.depositAmountLabel),
hint: context.translate(I18n.depositAmountHint),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(
I18n.allowanceBalanceAfter,
args: {
'amount': (availableBalance +
(double.tryParse(viewState.amount) ?? 0))
.toStringAsFixed(2),
},
),
),
],
),
),
],
),
// Container(

View File

@@ -5,6 +5,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:home/src/presentation/wallet_management_layout.dart';
import 'package:sf_localizations/sf_localizations.dart';
import '../../card_colors.dart';
import '../child_wallet/child_data_provider.dart';
import 'extract_view_model.dart';
class ExtractScreen extends ConsumerWidget {
@@ -17,18 +19,12 @@ class ExtractScreen extends ConsumerWidget {
required this.navigation,
});
List<Color> _cardColors(String? carrierGenre, ThemePort theme) =>
switch (carrierGenre) {
'F' => theme.getCardColorFor(0),
'M' => theme.getCardColorFor(1),
_ => theme.getDisabledCardColors(),
};
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(extractViewModelProvider(childId));
final viewModel = ref.read(extractViewModelProvider(childId).notifier);
final cardStatus = ref.watch(childDataProvider(childId)).cardStatus;
ref.listen(extractViewModelProvider(childId), (prev, next) {
if (next.success && !(prev?.success ?? false)) {
@@ -57,139 +53,127 @@ class ExtractScreen extends ConsumerWidget {
return WalletManagementLayout(
childName: childName,
balance: availableBalance,
cardColors: _cardColors(viewState.device?.carrierGenre, theme),
cardColors: cardColorsFor(theme: theme, carrierGenre: viewState.device?.carrierGenre, cardStatus: cardStatus),
navigation: navigation,
children: [
Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24)),
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
child: Column(
spacing: 24,
children: [
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.extractTitle),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.watchInfo),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
),
],
),
CustomTextField(
controller: viewModel.amountController,
label: context.translate(I18n.extractAmountLabel),
hint: "2\u20AC",
keyboardType: TextInputType.number,
),
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.defaultMessagePrefix),
style: TextStyle(fontSize: 16, letterSpacing: 0),
),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.extractDefaultMessage),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
],
),
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.extractMessageLabel, args: {'name': childName}),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
),
CustomTextField(
controller: viewModel.messageController,
hint: context.translate(I18n.allowanceMessageHint),
lines: 4,
length: 150,
),
Row(
spacing: 4,
children: [
Icon(Icons.info_outline, size: 16),
Text(
context.translate(I18n.allowanceMaxChars, args: {'count': '150'}),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
],
),
],
),
],
),
),
],
footer: Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.only(
topRight: Radius.circular(24),
topLeft: Radius.circular(24),
),
),
child: Column(
spacing: 16,
SectionContainer(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
spacing: 24,
children: [
PrimaryButton(
onPressed: viewState.isSubmitting
? null
: () {
final amount = double.tryParse(viewModel.amountController.text.trim());
if (amount == null || amount <= 0) {
showTopSnackbar(context, message: context.translate(I18n.walletMoveAmountRequired), type: MessageType.warning);
return;
}
viewModel.submit();
},
text: viewState.isSubmitting
? context.translate(I18n.walletMoveProcessing)
: context.translate(I18n.sendMessageAndBlock),
color: theme.getColorFor(ThemeCode.buttonPrimary),
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.extractTitle),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.watchInfo),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
),
],
),
TextButton(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
CustomTextField(
controller: viewModel.amountController,
label: context.translate(I18n.extractAmountLabel),
hint: "2\u20AC",
keyboardType: TextInputType.number,
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(
I18n.allowanceBalanceAfter,
args: {
'amount': (availableBalance -
(double.tryParse(viewState.amount) ?? 0))
.toStringAsFixed(2),
},
),
),
onPressed: () => navigation.goBack(),
child: Text(context.translate(I18n.cancel), style: TextStyle(fontSize: 18)),
),
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.defaultMessagePrefix),
style: TextStyle(fontSize: 16, letterSpacing: 0),
),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.extractDefaultMessage),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
],
),
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.extractMessageLabel, args: {'name': childName}),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
),
CustomTextField(
controller: viewModel.messageController,
hint: context.translate(I18n.allowanceMessageHint),
lines: 4,
length: 150,
),
Row(
spacing: 4,
children: [
Icon(Icons.info_outline, size: 16),
Text(
context.translate(I18n.allowanceMaxChars, args: {'count': '150'}),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
],
),
],
),
],
),
],
footer: FooterContainer(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
primaryColor: theme.getColorFor(ThemeCode.buttonPrimary),
primaryText: viewState.isSubmitting
? context.translate(I18n.walletMoveProcessing)
: context.translate(I18n.sendMessageAndBlock),
onPrimaryPressed: viewState.isSubmitting
? null
: () {
final amount = double.tryParse(viewModel.amountController.text.trim());
if (amount == null || amount <= 0) {
showTopSnackbar(context, message: context.translate(I18n.walletMoveAmountRequired), type: MessageType.warning);
return;
}
viewModel.submit();
},
cancelText: context.translate(I18n.cancel),
onCancelPressed: () => navigation.goBack(),
),
);
}

View File

@@ -7,6 +7,8 @@ import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
import '../../card_colors.dart';
import '../child_wallet/child_data_provider.dart';
import 'goals_view_model.dart';
class GoalsScreen extends ConsumerWidget {
@@ -19,17 +21,11 @@ class GoalsScreen extends ConsumerWidget {
required this.navigation,
});
List<Color> _cardColors(String? carrierGenre, ThemePort theme) =>
switch (carrierGenre) {
'F' => theme.getCardColorFor(0),
'M' => theme.getCardColorFor(1),
_ => theme.getDisabledCardColors(),
};
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(goalsViewModelProvider(childId));
final cardStatus = ref.watch(childDataProvider(childId)).cardStatus;
if (viewState.isLoading) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
@@ -48,43 +44,37 @@ class GoalsScreen extends ConsumerWidget {
return WalletManagementLayout(
childName: childName,
balance: availableBalance,
cardColors: _cardColors(viewState.device?.carrierGenre, theme),
cardColors: cardColorsFor(theme: theme, carrierGenre: viewState.device?.carrierGenre, cardStatus: cardStatus),
navigation: navigation,
children: [
Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.all(Radius.circular(24)),
),
child: Column(
spacing: 24,
children: [
Row(
spacing: 8,
children: [
Text(
context.translate(I18n.goalsTitle),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
SectionContainer(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
spacing: 24,
children: [
Row(
spacing: 8,
children: [
Text(
context.translate(I18n.goalsTitle),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
Icon(Icons.workspace_premium),
Spacer(),
Text(
context.translate(I18n.goalsOnlyFullPlan),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
],
),
Text(
context.translate(I18n.goalsTeachSavings),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
],
),
),
Icon(Icons.workspace_premium),
Spacer(),
Text(
context.translate(I18n.goalsOnlyFullPlan),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
],
),
Text(
context.translate(I18n.goalsTeachSavings),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
],
),
SavingsBlock(
fullPlan: viewState.fullPlan,

View File

@@ -5,7 +5,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:home/src/presentation/wallet_management_layout.dart';
import 'package:sf_localizations/sf_localizations.dart';
import '../../card_colors.dart';
import '../child_wallet/child_data_provider.dart';
import 'limits_view_model.dart';
import 'limits_view_state.dart';
class LimitsScreen extends ConsumerWidget {
final String childId;
@@ -17,247 +20,202 @@ class LimitsScreen extends ConsumerWidget {
required this.navigation,
});
List<Color> _cardColors(String? carrierGenre, ThemePort theme) =>
switch (carrierGenre) {
'F' => theme.getCardColorFor(0),
'M' => theme.getCardColorFor(1),
_ => theme.getDisabledCardColors(),
};
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(limitsViewModelProvider(childId));
final viewModel = ref.read(limitsViewModelProvider(childId).notifier);
final cardStatus = ref.watch(childDataProvider(childId)).cardStatus;
if (viewState.isLoading) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
if (viewState.errorMessage.isNotEmpty) {
if (viewState.errorMessage.isNotEmpty && viewState.childProfile == null) {
return Scaffold(
body: Center(child: Text('Error: ${viewState.errorMessage}')),
body: Center(
child: Text(context.translate(I18n.errorLoadingData)),
),
);
}
final childProfile = viewState.childProfile!;
final childName = childProfile.firstName;
final availableBalance = viewState.childWallet?.authorizedBalance ?? 0;
return WalletManagementLayout(
childName: childName,
balance: availableBalance,
cardColors: _cardColors(viewState.device?.carrierGenre, theme),
childName: childProfile.firstName,
balance: viewState.childWallet?.authorizedBalance ?? 0,
cardColors: cardColorsFor(theme: theme, carrierGenre: viewState.device?.carrierGenre, cardStatus: cardStatus),
navigation: navigation,
footer: Container(
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
padding: EdgeInsets.all(24),
child: Column(
children: [
PrimaryButton(
onPressed: () => {},
text: context.translate(I18n.limitsSave),
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
TextButton(
onPressed: () => navigation.goBack(),
child: Text(
context.translate(I18n.cancel),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
),
],
),
footer: _LimitsFooter(
theme: theme,
isSubmitting: viewState.isSubmitting,
viewModel: viewModel,
navigation: navigation,
),
children: [
Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20)),
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
child: Column(
spacing: 10,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.limitsSpendingTitle),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
),
Text(context.translate(I18n.limitsSpendingSubtitle)),
...List<Widget>.generate(viewState.dailyLimits.length, (int index) {
return Column(
children: [
Row(
children: [
Text(
"${viewState.dailyLimits[index]["title"]}: ${viewState.dailyLimits[index]["limit"]} \u20AC",
),
Spacer(),
TextButton(
onPressed: () => viewModel.toggleDailyLimitEdit(index),
child: Text(context.translate(I18n.homeEdit)),
),
],
),
if (viewState.dailyLimits[index]["edit"]) CustomTextField(hint: "5\u20AC"),
],
);
}),
],
),
_SpendingLimitsSection(
theme: theme,
viewState: viewState,
viewModel: viewModel,
),
Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20)),
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
child: Column(
spacing: 10,
children: [
Text(
context.translate(I18n.limitsAllowedHours),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
Text(context.translate(I18n.limitsAllowedHoursSubtitle)),
...List<Widget>.generate(viewState.timeLimits.length, (int index) {
return Column(
children: [
Row(
children: [
Text(
"${viewState.timeLimits[index]["title"]}: ${viewState.timeLimits[index]["start"]} - ${viewState.timeLimits[index]["end"]}",
),
Spacer(),
TextButton(
onPressed: () => viewModel.toggleTimeLimitEdit(index),
child: Text(context.translate(I18n.homeEdit)),
),
],
),
if (viewState.timeLimits[index]["edit"]) CustomTextField(hint: "5\u20AC"),
],
);
}),
],
),
),
Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24)),
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
child: Column(
spacing: 24,
children: [
Text(
context.translate(I18n.allowanceConditions),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
Column(
spacing: 8,
children: List<Widget>.generate(
viewState.conditions.length,
(int index) => Column(
spacing: 8,
children: [
Row(
children: [
Expanded(
child: CheckboxListTile(
value: viewState.conditions[index]["active"],
onChanged: (_) => viewModel.toggleConditionActive(index),
title: Text(
"${viewState.conditions[index]["title"]}: ${viewState.conditions[index]["limit"]}\u20AC/sem",
style: TextStyle(
fontSize: 16,
letterSpacing: 0,
),
),
checkboxScaleFactor: 2,
controlAffinity: ListTileControlAffinity.leading,
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
contentPadding: EdgeInsets.zero,
),
),
TextButton(
onPressed: () => viewModel.toggleConditionEdit(index),
child: Text(
context.translate(I18n.homeEdit),
style: TextStyle(fontSize: 16, letterSpacing: 0),
),
),
],
),
if (viewState.conditions[index]["edit"])
CustomTextField(
hint: "5\u20AC",
keyboardType: TextInputType.number,
),
],
),
),
),
],
),
),
Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.all(Radius.circular(24)),
),
child: Column(
spacing: 24,
children: [
Text(
context.translate(I18n.limitsBlockedStores),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
Column(
spacing: 8,
children: List<Widget>.generate(
viewState.blocks.length,
(int index) => CheckboxListTile(
value: viewState.blocks[index]["active"],
onChanged: (_) => viewModel.toggleBlockActive(index),
title: Text(
viewState.blocks[index]["title"],
style: TextStyle(fontSize: 16, letterSpacing: 0),
),
checkboxScaleFactor: 2,
controlAffinity: ListTileControlAffinity.leading,
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
contentPadding: EdgeInsets.zero,
),
),
),
],
),
_BlockedStoresSection(
theme: theme,
viewState: viewState,
viewModel: viewModel,
),
],
);
}
}
class _LimitsFooter extends StatelessWidget {
final ThemePort theme;
final bool isSubmitting;
final LimitsViewModel viewModel;
final NavigationContract navigation;
const _LimitsFooter({
required this.theme,
required this.isSubmitting,
required this.viewModel,
required this.navigation,
});
@override
Widget build(BuildContext context) {
return FooterContainer(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
primaryColor: theme.getColorFor(ThemeCode.buttonPrimary),
primaryText: isSubmitting ? '...' : context.translate(I18n.limitsSave),
onPrimaryPressed: isSubmitting
? null
: () async {
final success = await viewModel.saveAll();
if (!context.mounted) return;
showTopSnackbar(
context,
message: success
? context.translate(I18n.limitsSaveSuccess)
: context.translate(I18n.limitsSaveError),
type: success ? MessageType.success : MessageType.error,
);
},
cancelText: context.translate(I18n.cancel),
onCancelPressed: () => navigation.goBack(),
);
}
}
class _SpendingLimitsSection extends StatelessWidget {
final ThemePort theme;
final LimitsViewState viewState;
final LimitsViewModel viewModel;
const _SpendingLimitsSection({
required this.theme,
required this.viewState,
required this.viewModel,
});
static const _limitFields = [
('dayLimit', I18n.limitsDayLimit),
('weekLimit', I18n.limitsWeekLimit),
('monthLimit', I18n.limitsMonthLimit),
('yearLimit', I18n.limitsYearLimit),
];
@override
Widget build(BuildContext context) {
return SectionContainer(
padding: 20,
borderRadius: 20,
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
title: context.translate(I18n.limitsSpendingTitle),
subtitle: context.translate(I18n.limitsSpendingSubtitle),
children: [
if (viewState.isLoadingLimits)
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
)
else
..._limitFields.map((entry) {
final field = entry.$1;
final i18nKey = entry.$2;
return EditableRow(
label: context.translate(i18nKey),
displayValue: _formatLimitValue(field),
isEditing: viewState.editingLimitFields.contains(field),
onToggleEdit: () => viewModel.toggleLimitEdit(field),
editButtonText: context.translate(I18n.homeEdit),
hint: "0\u20AC",
keyboardType: TextInputType.number,
onChanged: (text) {
viewModel.updateLimit(field, double.tryParse(text));
},
);
}),
],
);
}
String _formatLimitValue(String field) {
final limits = viewState.walletLimits;
if (limits == null) return '-- \u20AC';
final value = switch (field) {
'dayLimit' => limits.dayLimit,
'weekLimit' => limits.weekLimit,
'monthLimit' => limits.monthLimit,
'yearLimit' => limits.yearLimit,
_ => null,
};
return value != null ? '${value.toStringAsFixed(0)} \u20AC' : '-- \u20AC';
}
}
class _BlockedStoresSection extends StatelessWidget {
final ThemePort theme;
final LimitsViewState viewState;
final LimitsViewModel viewModel;
const _BlockedStoresSection({
required this.theme,
required this.viewState,
required this.viewModel,
});
@override
Widget build(BuildContext context) {
return SectionContainer(
padding: 20,
borderRadius: 20,
spacing: 24,
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
title: context.translate(I18n.limitsBlockedStores),
children: [
if (viewState.isLoadingMccGroups)
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
)
else
...viewState.mccGroups.map((group) {
final isActive = viewState.activeMccGroupIds.contains(group.id);
return CheckboxListTile(
value: isActive,
onChanged: (_) => viewModel.toggleMccGroup(group.id),
title: Text(
group.name,
style: const TextStyle(fontSize: 16, letterSpacing: 0),
),
checkboxScaleFactor: 2,
controlAffinity: ListTileControlAffinity.leading,
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
contentPadding: EdgeInsets.zero,
);
}),
],
);
}
}

View File

@@ -1,98 +1,28 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import '../child_wallet/child_data_provider.dart';
import 'limits_view_state.dart';
final limitsViewModelProvider =
NotifierProvider.autoDispose.family<LimitsViewModel, LimitsViewState, String>(
LimitsViewModel.new,
);
final limitsViewModelProvider = NotifierProvider.autoDispose
.family<LimitsViewModel, LimitsViewState, String>(LimitsViewModel.new);
class LimitsViewModel extends Notifier<LimitsViewState> {
final String childId;
LimitsViewModel(this.childId);
bool _limitsLoaded = false;
WalletLimitsEntity? _cachedLimits;
List<MccGroupEntity> _cachedMccGroups = [];
Set<String> _cachedActiveMccGroupIds = {};
@override
LimitsViewState build() {
ref.listen(childDataProvider(childId), (prev, next) {
if (!next.isLoading && next.childProfile != null && state.dailyLimits.isEmpty) {
state = state.copyWith(
isLoading: false,
childProfile: next.childProfile,
childWallet: next.childWallet,
device: next.device,
errorMessage: next.errorMessage,
dailyLimits: [
{"title": "Diario L-V", "limit": "5", "edit": false},
{"title": "Fines de semana", "limit": "8", "edit": false},
{"title": "Semanal", "limit": "30", "edit": false},
{"title": "Mensual", "limit": "1200", "edit": false},
],
timeLimits: [
{"title": "Lunes a Viernes", "start": "08:00", "end": "20:00", "edit": false},
{"title": "Fines de semana", "start": "10:00", "end": "21:00", "edit": false},
{"title": "Vacaciones", "start": "09:00", "end": "22:00", "edit": false},
],
conditions: [
{"title": "Alimentaci\u00F3n", "limit": "10", "active": true, "edit": false},
{"title": "Transporte", "limit": "10", "active": false, "edit": false},
{"title": "Alimentaci\u00F3n", "limit": "10", "active": false, "edit": false},
],
blocks: [
{"title": "Alojamiento y Hoteles", "active": true},
{"title": "Supermercados", "active": true},
{"title": "Gasolineras", "active": true},
{"title": "Restaurantes", "active": true},
{"title": "Bares y discotecas", "active": true},
{"title": "Licorer\u00EDas", "active": true},
{"title": "Estancos", "active": true},
],
);
} else {
state = state.copyWith(
isLoading: next.isLoading,
childProfile: next.childProfile,
childWallet: next.childWallet,
device: next.device,
errorMessage: next.errorMessage,
);
}
});
final data = ref.watch(childDataProvider(childId));
final data = ref.read(childDataProvider(childId));
if (!data.isLoading && data.childProfile != null) {
return LimitsViewState(
isLoading: false,
childProfile: data.childProfile,
childWallet: data.childWallet,
device: data.device,
errorMessage: data.errorMessage,
dailyLimits: [
{"title": "Diario L-V", "limit": "5", "edit": false},
{"title": "Fines de semana", "limit": "8", "edit": false},
{"title": "Semanal", "limit": "30", "edit": false},
{"title": "Mensual", "limit": "1200", "edit": false},
],
timeLimits: [
{"title": "Lunes a Viernes", "start": "08:00", "end": "20:00", "edit": false},
{"title": "Fines de semana", "start": "10:00", "end": "21:00", "edit": false},
{"title": "Vacaciones", "start": "09:00", "end": "22:00", "edit": false},
],
conditions: [
{"title": "Alimentaci\u00F3n", "limit": "10", "active": true, "edit": false},
{"title": "Transporte", "limit": "10", "active": false, "edit": false},
{"title": "Alimentaci\u00F3n", "limit": "10", "active": false, "edit": false},
],
blocks: [
{"title": "Alojamiento y Hoteles", "active": true},
{"title": "Supermercados", "active": true},
{"title": "Gasolineras", "active": true},
{"title": "Restaurantes", "active": true},
{"title": "Bares y discotecas", "active": true},
{"title": "Licorer\u00EDas", "active": true},
{"title": "Estancos", "active": true},
],
);
if (!data.isLoading && data.childProfile != null && !_limitsLoaded) {
_limitsLoaded = true;
Future.microtask(() => _loadLimitsData());
}
return LimitsViewState(
@@ -101,46 +31,132 @@ class LimitsViewModel extends Notifier<LimitsViewState> {
childWallet: data.childWallet,
device: data.device,
errorMessage: data.errorMessage,
walletLimits: _cachedLimits,
mccGroups: _cachedMccGroups,
activeMccGroupIds: _cachedActiveMccGroupIds,
);
}
void toggleDailyLimitEdit(int index) {
final updated = List<Map<String, dynamic>>.from(
state.dailyLimits.map((e) => Map<String, dynamic>.from(e)),
);
updated[index]["edit"] = !updated[index]["edit"];
state = state.copyWith(dailyLimits: updated);
Future<void> _loadLimitsData() async {
final walletId = state.childWallet?.walletId;
if (walletId == null) return;
final repo = ref.read(treezorRepositoryProvider);
await Future.wait([
_loadWalletLimits(repo, walletId),
_loadMccGroups(repo, walletId),
]);
}
void toggleTimeLimitEdit(int index) {
final updated = List<Map<String, dynamic>>.from(
state.timeLimits.map((e) => Map<String, dynamic>.from(e)),
);
updated[index]["edit"] = !updated[index]["edit"];
state = state.copyWith(timeLimits: updated);
Future<void> _loadWalletLimits(TreezorRepository repo, String walletId) async {
try {
final limits = await repo.getWalletLimits(walletId: walletId);
if (!ref.mounted) return;
_cachedLimits = limits;
state = state.copyWith(
walletLimits: limits,
isLoadingLimits: false,
errorMessage: '',
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoadingLimits: false, errorMessage: e.toString());
}
}
void toggleConditionActive(int index) {
final updated = List<Map<String, dynamic>>.from(
state.conditions.map((e) => Map<String, dynamic>.from(e)),
);
updated[index]["active"] = !updated[index]["active"];
state = state.copyWith(conditions: updated);
Future<void> _loadMccGroups(TreezorRepository repo, String walletId) async {
try {
final results = await Future.wait([
repo.getMccGroups(),
repo.getWalletMccLimits(walletId: walletId),
]);
if (!ref.mounted) return;
final groups = results[0] as List<MccGroupEntity>;
final activeIds = (results[1] as List<String>).toSet();
_cachedMccGroups = groups;
_cachedActiveMccGroupIds = activeIds;
state = state.copyWith(
mccGroups: groups,
activeMccGroupIds: activeIds,
isLoadingMccGroups: false,
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoadingMccGroups: false);
}
}
void toggleConditionEdit(int index) {
final updated = List<Map<String, dynamic>>.from(
state.conditions.map((e) => Map<String, dynamic>.from(e)),
);
updated[index]["edit"] = !updated[index]["edit"];
state = state.copyWith(conditions: updated);
void toggleLimitEdit(String field) {
final current = Set<String>.from(state.editingLimitFields);
if (current.contains(field)) {
current.remove(field);
} else {
current.add(field);
}
state = state.copyWith(editingLimitFields: current);
}
void toggleBlockActive(int index) {
final updated = List<Map<String, dynamic>>.from(
state.blocks.map((e) => Map<String, dynamic>.from(e)),
);
updated[index]["active"] = !updated[index]["active"];
state = state.copyWith(blocks: updated);
void updateLimit(String field, double? value) {
final current = state.walletLimits ?? const WalletLimitsEntity();
final updated = switch (field) {
'dayLimit' => current.copyWith(dayLimit: value),
'weekLimit' => current.copyWith(weekLimit: value),
'monthLimit' => current.copyWith(monthLimit: value),
'yearLimit' => current.copyWith(yearLimit: value),
_ => current,
};
state = state.copyWith(walletLimits: updated);
}
void toggleMccGroup(String groupId) {
final current = Set<String>.from(state.activeMccGroupIds);
if (current.contains(groupId)) {
current.remove(groupId);
} else {
current.add(groupId);
}
_cachedActiveMccGroupIds = current;
state = state.copyWith(activeMccGroupIds: current);
}
Future<bool> saveAll() async {
final walletId = state.childWallet?.walletId;
if (walletId == null) return false;
state = state.copyWith(isSubmitting: true, errorMessage: '');
try {
final repo = ref.read(treezorRepositoryProvider);
await Future.wait([
_saveLimits(repo, walletId),
_saveMccLimits(repo, walletId),
]);
if (!ref.mounted) return false;
state = state.copyWith(isSubmitting: false, editingLimitFields: {});
return true;
} catch (e) {
if (!ref.mounted) return false;
state = state.copyWith(isSubmitting: false, errorMessage: e.toString());
return false;
}
}
Future<void> _saveLimits(TreezorRepository repo, String walletId) {
return repo.setWalletLimits(
walletId: walletId,
limits: state.walletLimits ?? const WalletLimitsEntity(),
);
}
Future<void> _saveMccLimits(TreezorRepository repo, String walletId) {
return repo.setWalletMccLimits(
walletId: walletId,
mccGroupIds: state.activeMccGroupIds.toList(),
);
}
}

View File

@@ -10,10 +10,12 @@ abstract class LimitsViewState with _$LimitsViewState {
ChildProfileEntity? childProfile,
ChildWalletEntity? childWallet,
DeviceEntity? device,
@Default([]) List<Map<String, dynamic>> dailyLimits,
@Default([]) List<Map<String, dynamic>> timeLimits,
@Default([]) List<Map<String, dynamic>> conditions,
@Default([]) List<Map<String, dynamic>> blocks,
WalletLimitsEntity? walletLimits,
@Default(true) bool isLoadingLimits,
@Default({}) Set<String> editingLimitFields,
@Default([]) List<MccGroupEntity> mccGroups,
@Default({}) Set<String> activeMccGroupIds,
@Default(true) bool isLoadingMccGroups,
@Default(false) bool isSubmitting,
@Default('') String errorMessage,
}) = _LimitsViewState;

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LimitsViewState {
bool get isLoading; ChildProfileEntity? get childProfile; ChildWalletEntity? get childWallet; DeviceEntity? get device; List<Map<String, dynamic>> get dailyLimits; List<Map<String, dynamic>> get timeLimits; List<Map<String, dynamic>> get conditions; List<Map<String, dynamic>> get blocks; bool get isSubmitting; String get errorMessage;
bool get isLoading; ChildProfileEntity? get childProfile; ChildWalletEntity? get childWallet; DeviceEntity? get device; WalletLimitsEntity? get walletLimits; bool get isLoadingLimits; Set<String> get editingLimitFields; List<MccGroupEntity> get mccGroups; Set<String> get activeMccGroupIds; bool get isLoadingMccGroups; bool get isSubmitting; String get errorMessage;
/// Create a copy of LimitsViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $LimitsViewStateCopyWith<LimitsViewState> get copyWith => _$LimitsViewStateCopyW
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LimitsViewState&&(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)&&const DeepCollectionEquality().equals(other.dailyLimits, dailyLimits)&&const DeepCollectionEquality().equals(other.timeLimits, timeLimits)&&const DeepCollectionEquality().equals(other.conditions, conditions)&&const DeepCollectionEquality().equals(other.blocks, blocks)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is LimitsViewState&&(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.walletLimits, walletLimits) || other.walletLimits == walletLimits)&&(identical(other.isLoadingLimits, isLoadingLimits) || other.isLoadingLimits == isLoadingLimits)&&const DeepCollectionEquality().equals(other.editingLimitFields, editingLimitFields)&&const DeepCollectionEquality().equals(other.mccGroups, mccGroups)&&const DeepCollectionEquality().equals(other.activeMccGroupIds, activeMccGroupIds)&&(identical(other.isLoadingMccGroups, isLoadingMccGroups) || other.isLoadingMccGroups == isLoadingMccGroups)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,childProfile,childWallet,device,const DeepCollectionEquality().hash(dailyLimits),const DeepCollectionEquality().hash(timeLimits),const DeepCollectionEquality().hash(conditions),const DeepCollectionEquality().hash(blocks),isSubmitting,errorMessage);
int get hashCode => Object.hash(runtimeType,isLoading,childProfile,childWallet,device,walletLimits,isLoadingLimits,const DeepCollectionEquality().hash(editingLimitFields),const DeepCollectionEquality().hash(mccGroups),const DeepCollectionEquality().hash(activeMccGroupIds),isLoadingMccGroups,isSubmitting,errorMessage);
@override
String toString() {
return 'LimitsViewState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, dailyLimits: $dailyLimits, timeLimits: $timeLimits, conditions: $conditions, blocks: $blocks, isSubmitting: $isSubmitting, errorMessage: $errorMessage)';
return 'LimitsViewState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, walletLimits: $walletLimits, isLoadingLimits: $isLoadingLimits, editingLimitFields: $editingLimitFields, mccGroups: $mccGroups, activeMccGroupIds: $activeMccGroupIds, isLoadingMccGroups: $isLoadingMccGroups, isSubmitting: $isSubmitting, errorMessage: $errorMessage)';
}
@@ -45,11 +45,11 @@ abstract mixin class $LimitsViewStateCopyWith<$Res> {
factory $LimitsViewStateCopyWith(LimitsViewState value, $Res Function(LimitsViewState) _then) = _$LimitsViewStateCopyWithImpl;
@useResult
$Res call({
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, List<Map<String, dynamic>> dailyLimits, List<Map<String, dynamic>> timeLimits, List<Map<String, dynamic>> conditions, List<Map<String, dynamic>> blocks, bool isSubmitting, String errorMessage
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, WalletLimitsEntity? walletLimits, bool isLoadingLimits, Set<String> editingLimitFields, List<MccGroupEntity> mccGroups, Set<String> activeMccGroupIds, bool isLoadingMccGroups, bool isSubmitting, String errorMessage
});
$ChildProfileEntityCopyWith<$Res>? get childProfile;$ChildWalletEntityCopyWith<$Res>? get childWallet;$DeviceEntityCopyWith<$Res>? get device;
$ChildProfileEntityCopyWith<$Res>? get childProfile;$ChildWalletEntityCopyWith<$Res>? get childWallet;$DeviceEntityCopyWith<$Res>? get device;$WalletLimitsEntityCopyWith<$Res>? get walletLimits;
}
/// @nodoc
@@ -62,17 +62,19 @@ class _$LimitsViewStateCopyWithImpl<$Res>
/// Create a copy of LimitsViewState
/// 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? dailyLimits = null,Object? timeLimits = null,Object? conditions = null,Object? blocks = null,Object? isSubmitting = null,Object? errorMessage = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? childProfile = freezed,Object? childWallet = freezed,Object? device = freezed,Object? walletLimits = freezed,Object? isLoadingLimits = null,Object? editingLimitFields = null,Object? mccGroups = null,Object? activeMccGroupIds = null,Object? isLoadingMccGroups = null,Object? isSubmitting = null,Object? errorMessage = 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
as ChildProfileEntity?,childWallet: freezed == childWallet ? _self.childWallet : childWallet // ignore: cast_nullable_to_non_nullable
as ChildWalletEntity?,device: freezed == device ? _self.device : device // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,dailyLimits: null == dailyLimits ? _self.dailyLimits : dailyLimits // ignore: cast_nullable_to_non_nullable
as List<Map<String, dynamic>>,timeLimits: null == timeLimits ? _self.timeLimits : timeLimits // ignore: cast_nullable_to_non_nullable
as List<Map<String, dynamic>>,conditions: null == conditions ? _self.conditions : conditions // ignore: cast_nullable_to_non_nullable
as List<Map<String, dynamic>>,blocks: null == blocks ? _self.blocks : blocks // ignore: cast_nullable_to_non_nullable
as List<Map<String, dynamic>>,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,walletLimits: freezed == walletLimits ? _self.walletLimits : walletLimits // ignore: cast_nullable_to_non_nullable
as WalletLimitsEntity?,isLoadingLimits: null == isLoadingLimits ? _self.isLoadingLimits : isLoadingLimits // ignore: cast_nullable_to_non_nullable
as bool,editingLimitFields: null == editingLimitFields ? _self.editingLimitFields : editingLimitFields // ignore: cast_nullable_to_non_nullable
as Set<String>,mccGroups: null == mccGroups ? _self.mccGroups : mccGroups // ignore: cast_nullable_to_non_nullable
as List<MccGroupEntity>,activeMccGroupIds: null == activeMccGroupIds ? _self.activeMccGroupIds : activeMccGroupIds // ignore: cast_nullable_to_non_nullable
as Set<String>,isLoadingMccGroups: null == isLoadingMccGroups ? _self.isLoadingMccGroups : isLoadingMccGroups // ignore: cast_nullable_to_non_nullable
as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
));
@@ -113,6 +115,18 @@ $DeviceEntityCopyWith<$Res>? get device {
return $DeviceEntityCopyWith<$Res>(_self.device!, (value) {
return _then(_self.copyWith(device: value));
});
}/// Create a copy of LimitsViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$WalletLimitsEntityCopyWith<$Res>? get walletLimits {
if (_self.walletLimits == null) {
return null;
}
return $WalletLimitsEntityCopyWith<$Res>(_self.walletLimits!, (value) {
return _then(_self.copyWith(walletLimits: value));
});
}
}
@@ -195,10 +209,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, List<Map<String, dynamic>> dailyLimits, List<Map<String, dynamic>> timeLimits, List<Map<String, dynamic>> conditions, List<Map<String, dynamic>> blocks, bool isSubmitting, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, WalletLimitsEntity? walletLimits, bool isLoadingLimits, Set<String> editingLimitFields, List<MccGroupEntity> mccGroups, Set<String> activeMccGroupIds, bool isLoadingMccGroups, bool isSubmitting, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LimitsViewState() when $default != null:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.dailyLimits,_that.timeLimits,_that.conditions,_that.blocks,_that.isSubmitting,_that.errorMessage);case _:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.walletLimits,_that.isLoadingLimits,_that.editingLimitFields,_that.mccGroups,_that.activeMccGroupIds,_that.isLoadingMccGroups,_that.isSubmitting,_that.errorMessage);case _:
return orElse();
}
@@ -216,10 +230,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, List<Map<String, dynamic>> dailyLimits, List<Map<String, dynamic>> timeLimits, List<Map<String, dynamic>> conditions, List<Map<String, dynamic>> blocks, bool isSubmitting, String errorMessage) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, WalletLimitsEntity? walletLimits, bool isLoadingLimits, Set<String> editingLimitFields, List<MccGroupEntity> mccGroups, Set<String> activeMccGroupIds, bool isLoadingMccGroups, bool isSubmitting, String errorMessage) $default,) {final _that = this;
switch (_that) {
case _LimitsViewState():
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.dailyLimits,_that.timeLimits,_that.conditions,_that.blocks,_that.isSubmitting,_that.errorMessage);case _:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.walletLimits,_that.isLoadingLimits,_that.editingLimitFields,_that.mccGroups,_that.activeMccGroupIds,_that.isLoadingMccGroups,_that.isSubmitting,_that.errorMessage);case _:
throw StateError('Unexpected subclass');
}
@@ -236,10 +250,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, List<Map<String, dynamic>> dailyLimits, List<Map<String, dynamic>> timeLimits, List<Map<String, dynamic>> conditions, List<Map<String, dynamic>> blocks, bool isSubmitting, String errorMessage)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, WalletLimitsEntity? walletLimits, bool isLoadingLimits, Set<String> editingLimitFields, List<MccGroupEntity> mccGroups, Set<String> activeMccGroupIds, bool isLoadingMccGroups, bool isSubmitting, String errorMessage)? $default,) {final _that = this;
switch (_that) {
case _LimitsViewState() when $default != null:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.dailyLimits,_that.timeLimits,_that.conditions,_that.blocks,_that.isSubmitting,_that.errorMessage);case _:
return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.device,_that.walletLimits,_that.isLoadingLimits,_that.editingLimitFields,_that.mccGroups,_that.activeMccGroupIds,_that.isLoadingMccGroups,_that.isSubmitting,_that.errorMessage);case _:
return null;
}
@@ -251,41 +265,37 @@ return $default(_that.isLoading,_that.childProfile,_that.childWallet,_that.devic
class _LimitsViewState implements LimitsViewState {
const _LimitsViewState({this.isLoading = true, this.childProfile, this.childWallet, this.device, final List<Map<String, dynamic>> dailyLimits = const [], final List<Map<String, dynamic>> timeLimits = const [], final List<Map<String, dynamic>> conditions = const [], final List<Map<String, dynamic>> blocks = const [], this.isSubmitting = false, this.errorMessage = ''}): _dailyLimits = dailyLimits,_timeLimits = timeLimits,_conditions = conditions,_blocks = blocks;
const _LimitsViewState({this.isLoading = true, this.childProfile, this.childWallet, this.device, this.walletLimits, this.isLoadingLimits = true, final Set<String> editingLimitFields = const {}, final List<MccGroupEntity> mccGroups = const [], final Set<String> activeMccGroupIds = const {}, this.isLoadingMccGroups = true, this.isSubmitting = false, this.errorMessage = ''}): _editingLimitFields = editingLimitFields,_mccGroups = mccGroups,_activeMccGroupIds = activeMccGroupIds;
@override@JsonKey() final bool isLoading;
@override final ChildProfileEntity? childProfile;
@override final ChildWalletEntity? childWallet;
@override final DeviceEntity? device;
final List<Map<String, dynamic>> _dailyLimits;
@override@JsonKey() List<Map<String, dynamic>> get dailyLimits {
if (_dailyLimits is EqualUnmodifiableListView) return _dailyLimits;
@override final WalletLimitsEntity? walletLimits;
@override@JsonKey() final bool isLoadingLimits;
final Set<String> _editingLimitFields;
@override@JsonKey() Set<String> get editingLimitFields {
if (_editingLimitFields is EqualUnmodifiableSetView) return _editingLimitFields;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_dailyLimits);
return EqualUnmodifiableSetView(_editingLimitFields);
}
final List<Map<String, dynamic>> _timeLimits;
@override@JsonKey() List<Map<String, dynamic>> get timeLimits {
if (_timeLimits is EqualUnmodifiableListView) return _timeLimits;
final List<MccGroupEntity> _mccGroups;
@override@JsonKey() List<MccGroupEntity> get mccGroups {
if (_mccGroups is EqualUnmodifiableListView) return _mccGroups;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_timeLimits);
return EqualUnmodifiableListView(_mccGroups);
}
final List<Map<String, dynamic>> _conditions;
@override@JsonKey() List<Map<String, dynamic>> get conditions {
if (_conditions is EqualUnmodifiableListView) return _conditions;
final Set<String> _activeMccGroupIds;
@override@JsonKey() Set<String> get activeMccGroupIds {
if (_activeMccGroupIds is EqualUnmodifiableSetView) return _activeMccGroupIds;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_conditions);
}
final List<Map<String, dynamic>> _blocks;
@override@JsonKey() List<Map<String, dynamic>> get blocks {
if (_blocks is EqualUnmodifiableListView) return _blocks;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_blocks);
return EqualUnmodifiableSetView(_activeMccGroupIds);
}
@override@JsonKey() final bool isLoadingMccGroups;
@override@JsonKey() final bool isSubmitting;
@override@JsonKey() final String errorMessage;
@@ -299,16 +309,16 @@ _$LimitsViewStateCopyWith<_LimitsViewState> get copyWith => __$LimitsViewStateCo
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LimitsViewState&&(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)&&const DeepCollectionEquality().equals(other._dailyLimits, _dailyLimits)&&const DeepCollectionEquality().equals(other._timeLimits, _timeLimits)&&const DeepCollectionEquality().equals(other._conditions, _conditions)&&const DeepCollectionEquality().equals(other._blocks, _blocks)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LimitsViewState&&(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.walletLimits, walletLimits) || other.walletLimits == walletLimits)&&(identical(other.isLoadingLimits, isLoadingLimits) || other.isLoadingLimits == isLoadingLimits)&&const DeepCollectionEquality().equals(other._editingLimitFields, _editingLimitFields)&&const DeepCollectionEquality().equals(other._mccGroups, _mccGroups)&&const DeepCollectionEquality().equals(other._activeMccGroupIds, _activeMccGroupIds)&&(identical(other.isLoadingMccGroups, isLoadingMccGroups) || other.isLoadingMccGroups == isLoadingMccGroups)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,childProfile,childWallet,device,const DeepCollectionEquality().hash(_dailyLimits),const DeepCollectionEquality().hash(_timeLimits),const DeepCollectionEquality().hash(_conditions),const DeepCollectionEquality().hash(_blocks),isSubmitting,errorMessage);
int get hashCode => Object.hash(runtimeType,isLoading,childProfile,childWallet,device,walletLimits,isLoadingLimits,const DeepCollectionEquality().hash(_editingLimitFields),const DeepCollectionEquality().hash(_mccGroups),const DeepCollectionEquality().hash(_activeMccGroupIds),isLoadingMccGroups,isSubmitting,errorMessage);
@override
String toString() {
return 'LimitsViewState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, dailyLimits: $dailyLimits, timeLimits: $timeLimits, conditions: $conditions, blocks: $blocks, isSubmitting: $isSubmitting, errorMessage: $errorMessage)';
return 'LimitsViewState(isLoading: $isLoading, childProfile: $childProfile, childWallet: $childWallet, device: $device, walletLimits: $walletLimits, isLoadingLimits: $isLoadingLimits, editingLimitFields: $editingLimitFields, mccGroups: $mccGroups, activeMccGroupIds: $activeMccGroupIds, isLoadingMccGroups: $isLoadingMccGroups, isSubmitting: $isSubmitting, errorMessage: $errorMessage)';
}
@@ -319,11 +329,11 @@ abstract mixin class _$LimitsViewStateCopyWith<$Res> implements $LimitsViewState
factory _$LimitsViewStateCopyWith(_LimitsViewState value, $Res Function(_LimitsViewState) _then) = __$LimitsViewStateCopyWithImpl;
@override @useResult
$Res call({
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, List<Map<String, dynamic>> dailyLimits, List<Map<String, dynamic>> timeLimits, List<Map<String, dynamic>> conditions, List<Map<String, dynamic>> blocks, bool isSubmitting, String errorMessage
bool isLoading, ChildProfileEntity? childProfile, ChildWalletEntity? childWallet, DeviceEntity? device, WalletLimitsEntity? walletLimits, bool isLoadingLimits, Set<String> editingLimitFields, List<MccGroupEntity> mccGroups, Set<String> activeMccGroupIds, bool isLoadingMccGroups, bool isSubmitting, String errorMessage
});
@override $ChildProfileEntityCopyWith<$Res>? get childProfile;@override $ChildWalletEntityCopyWith<$Res>? get childWallet;@override $DeviceEntityCopyWith<$Res>? get device;
@override $ChildProfileEntityCopyWith<$Res>? get childProfile;@override $ChildWalletEntityCopyWith<$Res>? get childWallet;@override $DeviceEntityCopyWith<$Res>? get device;@override $WalletLimitsEntityCopyWith<$Res>? get walletLimits;
}
/// @nodoc
@@ -336,17 +346,19 @@ class __$LimitsViewStateCopyWithImpl<$Res>
/// Create a copy of LimitsViewState
/// 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? dailyLimits = null,Object? timeLimits = null,Object? conditions = null,Object? blocks = null,Object? isSubmitting = null,Object? errorMessage = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? childProfile = freezed,Object? childWallet = freezed,Object? device = freezed,Object? walletLimits = freezed,Object? isLoadingLimits = null,Object? editingLimitFields = null,Object? mccGroups = null,Object? activeMccGroupIds = null,Object? isLoadingMccGroups = null,Object? isSubmitting = null,Object? errorMessage = null,}) {
return _then(_LimitsViewState(
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
as ChildProfileEntity?,childWallet: freezed == childWallet ? _self.childWallet : childWallet // ignore: cast_nullable_to_non_nullable
as ChildWalletEntity?,device: freezed == device ? _self.device : device // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,dailyLimits: null == dailyLimits ? _self._dailyLimits : dailyLimits // ignore: cast_nullable_to_non_nullable
as List<Map<String, dynamic>>,timeLimits: null == timeLimits ? _self._timeLimits : timeLimits // ignore: cast_nullable_to_non_nullable
as List<Map<String, dynamic>>,conditions: null == conditions ? _self._conditions : conditions // ignore: cast_nullable_to_non_nullable
as List<Map<String, dynamic>>,blocks: null == blocks ? _self._blocks : blocks // ignore: cast_nullable_to_non_nullable
as List<Map<String, dynamic>>,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,walletLimits: freezed == walletLimits ? _self.walletLimits : walletLimits // ignore: cast_nullable_to_non_nullable
as WalletLimitsEntity?,isLoadingLimits: null == isLoadingLimits ? _self.isLoadingLimits : isLoadingLimits // ignore: cast_nullable_to_non_nullable
as bool,editingLimitFields: null == editingLimitFields ? _self._editingLimitFields : editingLimitFields // ignore: cast_nullable_to_non_nullable
as Set<String>,mccGroups: null == mccGroups ? _self._mccGroups : mccGroups // ignore: cast_nullable_to_non_nullable
as List<MccGroupEntity>,activeMccGroupIds: null == activeMccGroupIds ? _self._activeMccGroupIds : activeMccGroupIds // ignore: cast_nullable_to_non_nullable
as Set<String>,isLoadingMccGroups: null == isLoadingMccGroups ? _self.isLoadingMccGroups : isLoadingMccGroups // ignore: cast_nullable_to_non_nullable
as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
));
@@ -388,6 +400,18 @@ $DeviceEntityCopyWith<$Res>? get device {
return $DeviceEntityCopyWith<$Res>(_self.device!, (value) {
return _then(_self.copyWith(device: value));
});
}/// Create a copy of LimitsViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$WalletLimitsEntityCopyWith<$Res>? get walletLimits {
if (_self.walletLimits == null) {
return null;
}
return $WalletLimitsEntityCopyWith<$Res>(_self.walletLimits!, (value) {
return _then(_self.copyWith(walletLimits: value));
});
}
}

View File

@@ -6,6 +6,8 @@ import 'package:home/src/presentation/wallet_management_layout.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
import '../../card_colors.dart';
import '../child_wallet/child_data_provider.dart';
import 'lock_card_view_model.dart';
import 'lock_card_view_state.dart';
@@ -19,21 +21,17 @@ class LockCardScreen extends ConsumerWidget {
required this.navigation,
});
List<Color> _cardColors(String? carrierGenre, ThemePort theme) =>
switch (carrierGenre) {
'F' => theme.getCardColorFor(0),
'M' => theme.getCardColorFor(1),
_ => theme.getDisabledCardColors(),
};
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(lockCardViewModelProvider(childId));
final viewModel = ref.read(lockCardViewModelProvider(childId).notifier);
final cardStatus = ref.watch(childDataProvider(childId)).cardStatus;
ref.listen<LockCardViewState>(lockCardViewModelProvider(childId),
(prev, next) {
ref.listen<LockCardViewState>(lockCardViewModelProvider(childId), (
prev,
next,
) {
if (next.success && !(prev?.success ?? false)) {
showTopSnackbar(
context,
@@ -59,7 +57,8 @@ class LockCardScreen extends ConsumerWidget {
if (viewState.errorMessage.isNotEmpty && viewState.childProfile == null) {
return Scaffold(
body: Center(child: Text('Error: ${viewState.errorMessage}')));
body: Center(child: Text('Error: ${viewState.errorMessage}')),
);
}
if (viewState.showPin) {
@@ -109,125 +108,101 @@ class LockCardScreen extends ConsumerWidget {
return WalletManagementLayout(
childName: childName,
balance: availableBalance,
cardColors: _cardColors(viewState.device?.carrierGenre, theme),
cardColors: cardColorsFor(theme: theme, carrierGenre: viewState.device?.carrierGenre, cardStatus: cardStatus),
navigation: navigation,
footer: FooterContainer(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
primaryColor: theme.getColorFor(ThemeCode.buttonPrimary),
primaryText: context.translate(I18n.sendMessageAndBlock),
onPrimaryPressed: viewModel.showPinStep,
cancelText: context.translate(I18n.cancel),
onCancelPressed: () => navigation.goBack(),
),
children: [
Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24)),
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
child: Column(
spacing: 24,
children: [
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.lockCardTitle),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
SectionContainer(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
spacing: 24,
children: [
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.lockCardTitle),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.watchInfo),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
),
],
),
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.defaultMessagePrefix),
style: TextStyle(fontSize: 16, letterSpacing: 0),
),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.lockCardDefaultMessage),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
],
),
Column(
spacing: 8,
children: [
Text(
context.translate(I18n.lockCardMessageLabel,
args: {'name': childName}),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.watchInfo),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
CustomTextField(
controller: viewModel.messageController,
hint: context.translate(I18n.allowanceMessageHint),
lines: 4,
length: 150,
),
Row(
spacing: 4,
children: [
Icon(Icons.info_outline, size: 16),
Text(
context.translate(I18n.allowanceMaxChars,
args: {'count': '150'}),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
],
),
],
),
],
),
),
],
footer: Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.only(
topRight: Radius.circular(24),
topLeft: Radius.circular(24),
),
),
child: Column(
spacing: 16,
children: [
PrimaryButton(
onPressed: viewModel.showPinStep,
text: context.translate(I18n.sendMessageAndBlock),
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
],
),
TextButton(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
onPressed: () => navigation.goBack(),
child: Text(
context.translate(I18n.cancel),
style: TextStyle(fontSize: 18),
),
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.defaultMessagePrefix),
style: TextStyle(fontSize: 16, letterSpacing: 0),
),
),
Align(
alignment: Alignment.topLeft,
child: Text(
context.translate(I18n.lockCardDefaultMessage),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
],
),
Column(
spacing: 8,
children: [
Text(
context.translate(
I18n.lockCardMessageLabel,
args: {'name': childName},
),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
CustomTextField(
controller: viewModel.messageController,
hint: context.translate(I18n.allowanceMessageHint),
lines: 4,
length: 150,
),
Row(
spacing: 4,
children: [
Icon(Icons.info_outline, size: 16),
Text(
context.translate(
I18n.allowanceMaxChars,
args: {'count': '150'},
),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
],
),
],
),
],
),
),
],
);
}
}

View File

@@ -6,6 +6,7 @@ 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_wallet/child_data_provider.dart';
import 'lock_card_view_state.dart';
@@ -19,12 +20,14 @@ class LockCardViewModel extends Notifier<LockCardViewState> {
LockCardViewModel(this.childId);
late final TextEditingController messageController;
late final TreezorWalletConnectionService _connectionService;
late final TreezorWalletSignatureService _signatureService;
@override
LockCardViewState build() {
messageController = TextEditingController();
messageController.addListener(_onMessageChanged);
_connectionService = GetIt.I<TreezorWalletConnectionService>();
_signatureService = GetIt.I<TreezorWalletSignatureService>();
ref.onDispose(_disposeControllers);
@@ -74,7 +77,7 @@ class LockCardViewModel extends Notifier<LockCardViewState> {
final card =
await ref.read(treezorRepositoryProvider).getCard(walletId: walletId);
if (!ref.mounted) return;
state = state.copyWith(cardId: card.cardId);
state = state.copyWith(cardId: card.cardId.toString());
} catch (_) {}
}
@@ -110,6 +113,9 @@ class LockCardViewModel extends Notifier<LockCardViewState> {
state = state.copyWith(isSigning: true, errorMessage: '');
try {
await _connectionService.connectWithPin(loginPin: state.pin);
if (!ref.mounted) return;
final url =
'https://savefamily.sandbox.treezor.co/v1/cards/$cardId/LockUnlock';
final scaProof = await _signatureService.generateJwsWithPin(
@@ -126,7 +132,7 @@ class LockCardViewModel extends Notifier<LockCardViewState> {
await ref.read(treezorRepositoryProvider).updateCardStatus(
walletId: walletId,
status: 'LOCK',
status: CardStatus.lock.value,
scaProof: scaProof,
);

View File

@@ -38,12 +38,14 @@ class _ChildWalletsSliderState extends ConsumerState<ChildWalletsSlider> {
onPageChanged: viewModel.onPageChanged,
itemBuilder: (context, index) {
final child = children[index];
final cardStatus = viewState.childCardStatuses[child.id] ?? '';
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: WalletItem(
childProfile: child,
childWallet: viewState.childWallets[child.id],
device: viewState.childDevices[child.id],
cardStatus: cardStatus,
index: index,
),
);

View File

@@ -61,6 +61,7 @@ class HomeViewModel extends Notifier<HomeViewState> {
// Fetch child wallets and device info
final childWallets = <String, ChildWalletEntity>{};
final childDevices = <String, DeviceEntity>{};
final childCardStatuses = <String, String>{};
final treezorRepo = ref.read(treezorRepositoryProvider);
final userRepo = ref.read(userRepositoryProvider);
for (final child in children) {
@@ -75,6 +76,11 @@ class HomeViewModel extends Notifier<HomeViewState> {
} catch (e) {
debugPrint('Error fetching device for child ${child.id}: $e');
}
try {
final card =
await treezorRepo.getCard(walletId: child.walletId);
childCardStatuses[child.id] = card.status;
} catch (_) {}
}
if (!ref.mounted) return;
@@ -90,6 +96,7 @@ class HomeViewModel extends Notifier<HomeViewState> {
children: children,
childWallets: childWallets,
childDevices: childDevices,
childCardStatuses: childCardStatuses,
currentPage: clampedPage,
);
} catch (e) {
@@ -98,6 +105,28 @@ class HomeViewModel extends Notifier<HomeViewState> {
}
}
void removeChild(String childId) {
final children = state.children.where((c) => c.id != childId).toList();
final wallets = Map<String, ChildWalletEntity>.from(state.childWallets)..remove(childId);
final devices = Map<String, DeviceEntity>.from(state.childDevices)..remove(childId);
final statuses = Map<String, String>.from(state.childCardStatuses)..remove(childId);
final clampedPage = children.isEmpty ? 0 : _currentPage.clamp(0, children.length - 1);
_currentPage = clampedPage;
state = state.copyWith(
children: children,
childWallets: wallets,
childDevices: devices,
childCardStatuses: statuses,
currentPage: clampedPage,
);
}
void updateChildCardStatus(String childId, String status) {
final updated = Map<String, String>.from(state.childCardStatuses);
updated[childId] = status;
state = state.copyWith(childCardStatuses: updated);
}
void retry() {
ref.read(walletRefreshProvider.notifier).refresh();
}

View File

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

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$HomeViewState {
bool get isLoading; String get userName; List<ChildProfileEntity> get children; Map<String, ChildWalletEntity> get childWallets; Map<String, DeviceEntity> get childDevices; String get errorMessage; int get currentPage;
bool get isLoading; String get userName; List<ChildProfileEntity> get children; Map<String, ChildWalletEntity> get childWallets; Map<String, DeviceEntity> get childDevices; Map<String, String> get childCardStatuses; 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.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage));
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)&&const DeepCollectionEquality().equals(other.childCardStatuses, childCardStatuses)&&(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),errorMessage,currentPage);
int get hashCode => Object.hash(runtimeType,isLoading,userName,const DeepCollectionEquality().hash(children),const DeepCollectionEquality().hash(childWallets),const DeepCollectionEquality().hash(childDevices),const DeepCollectionEquality().hash(childCardStatuses),errorMessage,currentPage);
@override
String toString() {
return 'HomeViewState(isLoading: $isLoading, userName: $userName, children: $children, childWallets: $childWallets, childDevices: $childDevices, errorMessage: $errorMessage, currentPage: $currentPage)';
return 'HomeViewState(isLoading: $isLoading, userName: $userName, children: $children, childWallets: $childWallets, childDevices: $childDevices, childCardStatuses: $childCardStatuses, errorMessage: $errorMessage, currentPage: $currentPage)';
}
@@ -45,7 +45,7 @@ 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, String errorMessage, int currentPage
bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, Map<String, String> childCardStatuses, String errorMessage, int currentPage
});
@@ -62,14 +62,15 @@ 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? errorMessage = null,Object? currentPage = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? userName = null,Object? children = null,Object? childWallets = null,Object? childDevices = null,Object? childCardStatuses = 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>,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as Map<String, DeviceEntity>,childCardStatuses: null == childCardStatuses ? _self.childCardStatuses : childCardStatuses // ignore: cast_nullable_to_non_nullable
as Map<String, String>,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,
));
@@ -156,10 +157,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, String errorMessage, int currentPage)? $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, Map<String, String> childCardStatuses, 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.errorMessage,_that.currentPage);case _:
return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets,_that.childDevices,_that.childCardStatuses,_that.errorMessage,_that.currentPage);case _:
return orElse();
}
@@ -177,10 +178,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, String errorMessage, int currentPage) $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, Map<String, String> childCardStatuses, 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.errorMessage,_that.currentPage);case _:
return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets,_that.childDevices,_that.childCardStatuses,_that.errorMessage,_that.currentPage);case _:
throw StateError('Unexpected subclass');
}
@@ -197,10 +198,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, String errorMessage, int currentPage)? $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, Map<String, String> childCardStatuses, 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.errorMessage,_that.currentPage);case _:
return $default(_that.isLoading,_that.userName,_that.children,_that.childWallets,_that.childDevices,_that.childCardStatuses,_that.errorMessage,_that.currentPage);case _:
return null;
}
@@ -212,7 +213,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.errorMessage = '', this.currentPage = 0}): _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 {}, final Map<String, String> childCardStatuses = const {}, this.errorMessage = '', this.currentPage = 0}): _children = children,_childWallets = childWallets,_childDevices = childDevices,_childCardStatuses = childCardStatuses;
@override@JsonKey() final bool isLoading;
@@ -238,6 +239,13 @@ class _HomeViewState implements HomeViewState {
return EqualUnmodifiableMapView(_childDevices);
}
final Map<String, String> _childCardStatuses;
@override@JsonKey() Map<String, String> get childCardStatuses {
if (_childCardStatuses is EqualUnmodifiableMapView) return _childCardStatuses;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_childCardStatuses);
}
@override@JsonKey() final String errorMessage;
@override@JsonKey() final int currentPage;
@@ -251,16 +259,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.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage));
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)&&const DeepCollectionEquality().equals(other._childCardStatuses, _childCardStatuses)&&(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),errorMessage,currentPage);
int get hashCode => Object.hash(runtimeType,isLoading,userName,const DeepCollectionEquality().hash(_children),const DeepCollectionEquality().hash(_childWallets),const DeepCollectionEquality().hash(_childDevices),const DeepCollectionEquality().hash(_childCardStatuses),errorMessage,currentPage);
@override
String toString() {
return 'HomeViewState(isLoading: $isLoading, userName: $userName, children: $children, childWallets: $childWallets, childDevices: $childDevices, errorMessage: $errorMessage, currentPage: $currentPage)';
return 'HomeViewState(isLoading: $isLoading, userName: $userName, children: $children, childWallets: $childWallets, childDevices: $childDevices, childCardStatuses: $childCardStatuses, errorMessage: $errorMessage, currentPage: $currentPage)';
}
@@ -271,7 +279,7 @@ 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, String errorMessage, int currentPage
bool isLoading, String userName, List<ChildProfileEntity> children, Map<String, ChildWalletEntity> childWallets, Map<String, DeviceEntity> childDevices, Map<String, String> childCardStatuses, String errorMessage, int currentPage
});
@@ -288,14 +296,15 @@ 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? errorMessage = null,Object? currentPage = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? userName = null,Object? children = null,Object? childWallets = null,Object? childDevices = null,Object? childCardStatuses = 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>,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as Map<String, DeviceEntity>,childCardStatuses: null == childCardStatuses ? _self._childCardStatuses : childCardStatuses // ignore: cast_nullable_to_non_nullable
as Map<String, String>,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,
));

View File

@@ -8,12 +8,15 @@ import 'package:navigation/navigation.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_localizations/sf_localizations.dart';
import '../card_colors.dart';
class WalletItem extends ConsumerWidget {
static const double cardHeight = 227;
final ChildProfileEntity childProfile;
final ChildWalletEntity? childWallet;
final DeviceEntity? device;
final String cardStatus;
final int index;
const WalletItem({
@@ -21,15 +24,10 @@ class WalletItem extends ConsumerWidget {
required this.childProfile,
this.childWallet,
this.device,
this.cardStatus = '',
required this.index,
});
List<Color> _cardColors(ThemePort theme) => switch (device?.carrierGenre) {
'F' => theme.getCardColorFor(0),
'M' => theme.getCardColorFor(1),
_ => theme.getDisabledCardColors(),
};
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
@@ -44,7 +42,13 @@ class WalletItem extends ConsumerWidget {
child: Stack(
children: [
Positioned.fill(
child: CustomPaint(painter: WalletPainter(_cardColors(theme))),
child: CustomPaint(
painter: WalletPainter(cardColorsFor(
theme: theme,
carrierGenre: device?.carrierGenre,
cardStatus: cardStatus,
)),
),
),
Column(
children: [
@@ -57,16 +61,25 @@ class WalletItem extends ConsumerWidget {
),
child: Column(
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
childProfile.firstName,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 30,
color: theme.getColorFor(ThemeCode.textSecondary),
Row(
children: [
Expanded(
child: Text(
childProfile.firstName,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 30,
color: theme.getColorFor(ThemeCode.textSecondary),
),
),
),
),
if (isCardLocked(cardStatus))
Icon(
Icons.lock,
size: 24,
color: theme.getColorFor(ThemeCode.textSecondary),
),
],
),
Row(
children: [
@@ -153,9 +166,11 @@ class WalletItem extends ConsumerWidget {
width: 169,
height: 60,
child: PrimaryButton(
onPressed: () => GetIt.I<NavigationContract>().pushTo(
AppRoutes.deposit(childProfile.id),
),
onPressed: isCardLocked(cardStatus)
? null
: () => GetIt.I<NavigationContract>().pushTo(
AppRoutes.deposit(childProfile.id),
),
text: context.translate(I18n.homeAddMoney),
color: theme.getColorFor(ThemeCode.buttonSecondary),
radius: 12,

View File

@@ -26,8 +26,6 @@ class WalletManagementLayout extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final bool locked = false;
final content = [
Padding(
padding: EdgeInsets.only(top: 50),
@@ -95,9 +93,7 @@ class WalletManagementLayout extends ConsumerWidget {
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: locked
? theme.getDisabledCardColors()
: cardColors ?? theme.getCardColorFor(0),
colors: cardColors ?? theme.getCardColorFor(0),
),
),
child: SizedBox(width: double.infinity, height: 200),