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:
44
modules/home/lib/src/card_colors.dart
Normal file
44
modules/home/lib/src/card_colors.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,6 +10,7 @@ abstract class ChildDataState with _$ChildDataState {
|
||||
ChildProfileEntity? childProfile,
|
||||
ChildWalletEntity? childWallet,
|
||||
DeviceEntity? device,
|
||||
@Default('') String cardStatus,
|
||||
@Default('') String errorMessage,
|
||||
}) = _ChildDataState;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user