payment methods with HiPay, payment profile edited, allowance step in device setup, tio snackbar added and logout from profile settings

This commit is contained in:
2026-02-15 14:05:08 +01:00
parent 5803286a3f
commit a221b7a71e
105 changed files with 1921 additions and 268 deletions

View File

@@ -40,6 +40,8 @@ abstract class AuthRemoteDatasource {
Future<void> createWallet();
Future<void> logout();
Future<ChildProfileResponseModel> createChildProfile({
required String id,
required String parentId,

View File

@@ -265,6 +265,15 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource {
}
}
@override
Future<void> logout() async {
try {
await _repository.post<void>('/auth/logout');
} on DioException catch (error) {
throw _mapDioError(error, defaultMessage: 'Error in logout');
}
}
@override
Future<ChildProfileResponseModel> createChildProfile({
required String id,

View File

@@ -103,6 +103,11 @@ class AuthRepositoryImpl implements AuthRepository {
return _remote.recoverPassword(newPassword: newPassword, token: token);
}
@override
Future<void> logout() {
return _remote.logout();
}
@override
Future<ChildProfileEntity> createChildProfile({
required String id,

View File

@@ -43,6 +43,8 @@ abstract class AuthRepository {
required String token,
});
Future<void> logout();
Future<ChildProfileEntity> createChildProfile({
required String id,
required String parentId,

View File

@@ -10,8 +10,8 @@ extension AddKidStepMapper on AddKidStep {
return AddKidMainStep.linkDevice;
case AddKidStep.profile:
return AddKidMainStep.profile;
case AddKidStep.success:
return AddKidMainStep.success;
case AddKidStep.allowance:
return AddKidMainStep.allowance;
case AddKidStep.intro:
return AddKidMainStep.linkDevice;
}

View File

@@ -3,6 +3,7 @@ import 'package:auth/src/features/device_setup/presentation/state/device_setup_v
import 'package:auth/src/features/device_setup/presentation/enums/add_kid_main_step.dart';
import 'package:auth/src/features/device_setup/presentation/enums/add_kid_step.dart';
import 'package:auth/src/features/device_setup/presentation/step_body.dart';
import 'package:auth/src/features/device_setup/presentation/success_screen.dart';
import 'package:auth/src/features/device_setup/presentation/widgets/flow_footer.dart';
import 'package:auth/src/features/sca_treezor/sca_pin_view.dart';
import 'package:design_system/design_system.dart';
@@ -10,6 +11,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/app_routes.dart';
import 'package:navigation/navigation_contract.dart';
import 'package:payments/payments.dart';
import 'package:sf_localizations/sf_localizations.dart';
class DeviceSetupScreen extends ConsumerWidget {
@@ -24,15 +26,15 @@ class DeviceSetupScreen extends ConsumerWidget {
final theme = ref.watch(themePortProvider);
final mainStep = state.step.mainStep;
final isIntroOrSuccess =
state.step == AddKidStep.intro || state.step == AddKidStep.success;
final isIntro = state.step == AddKidStep.intro;
final isAllowance = state.step == AddKidStep.allowance;
final canPopRoute = state.step == AddKidStep.intro;
return PopScope(
canPop: canPopRoute,
onPopInvokedWithResult: (didPop, result) {
if (didPop) return;
if (didPop || isAllowance) return;
vm.back();
},
child: Scaffold(
@@ -40,21 +42,24 @@ class DeviceSetupScreen extends ConsumerWidget {
body: SafeArea(
child: Column(
children: [
if (isIntroOrSuccess)
if (isIntro)
const SizedBox(height: 24)
else
Padding(
padding: const EdgeInsets.only(top: 12, left: 8, right: 8),
child: Row(
children: [
IconButton(
onPressed: vm.back,
icon: const Icon(Icons.arrow_back_ios_new_rounded),
color: theme.getColorFor(ThemeCode.textPrimary),
tooltip: MaterialLocalizations.of(
context,
).backButtonTooltip,
),
if (isAllowance)
const SizedBox(width: 48)
else
IconButton(
onPressed: vm.back,
icon: const Icon(Icons.arrow_back_ios_new_rounded),
color: theme.getColorFor(ThemeCode.textPrimary),
tooltip: MaterialLocalizations.of(
context,
).backButtonTooltip,
),
Expanded(
child: StepIndicator(
total: AddKidMainStep.values.length,
@@ -75,24 +80,25 @@ class DeviceSetupScreen extends ConsumerWidget {
),
FlowFooter(
error: context.translate(state.errorMessage),
primaryText: context.translate(primaryButtonText(state.step)),
secondaryText: state.step == AddKidStep.success
? context.translate(I18n.deviceSetup_addAnotherKid)
: null,
onPrimary: () {
if (state.step == AddKidStep.success) {
navigationContract.pushTo(AppRoutes.dashboardHome);
return;
}
if (state.step == AddKidStep.profile) {
if (!vm.validateProfile()) return;
_pushScaPinScreen(context, ref);
return;
}
if (state.step == AddKidStep.allowance) {
_pushHiPayScreen(context, ref);
return;
}
vm.next();
},
onSecondary: state.step == AddKidStep.success ? vm.resetForNewKid : null,
secondaryText: isAllowance
? context.translate(I18n.deviceSetup_skipAndConfigureLater)
: null,
onSecondary: isAllowance
? () => navigationContract.pushTo(AppRoutes.dashboardHome)
: null,
theme: theme,
),
],
@@ -102,6 +108,31 @@ class DeviceSetupScreen extends ConsumerWidget {
);
}
Future<void> _pushHiPayScreen(BuildContext context, WidgetRef ref) async {
final vm = ref.read(deviceSetupViewModelProvider.notifier);
final result = await Navigator.of(context).push<HiPayResult>(
MaterialPageRoute(
builder: (_) =>
HiPayWebViewScreen(navigationContract: navigationContract),
),
);
if (!context.mounted) return;
if (result == HiPayResult.success) {
vm.setError('');
showTopSnackbar(
context,
message: context.translate(I18n.deviceSetup_paymentSuccess),
type: MessageType.success,
);
} else {
showTopSnackbar(
context,
message: context.translate(I18n.deviceSetup_paymentCancelled),
type: MessageType.error,
);
}
}
void _pushScaPinScreen(BuildContext context, WidgetRef ref) {
Navigator.of(context).push(
MaterialPageRoute(
@@ -114,7 +145,7 @@ class DeviceSetupScreen extends ConsumerWidget {
switch (step) {
case AddKidStep.intro:
return I18n.deviceSetup_start;
case AddKidStep.success:
case AddKidStep.allowance:
return I18n.deviceSetup_giveFirstAllowance;
default:
return I18n.continueKey;
@@ -132,6 +163,19 @@ class _ScaPinScreen extends ConsumerWidget {
final state = ref.watch(deviceSetupViewModelProvider);
final vm = ref.read(deviceSetupViewModelProvider.notifier);
ref.listen(
deviceSetupViewModelProvider.select((s) => s.errorMessage),
(previous, next) {
if (next.isNotEmpty) {
showTopSnackbar(
context,
message: context.translate(next),
type: MessageType.error,
);
}
},
);
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
@@ -140,21 +184,22 @@ class _ScaPinScreen extends ConsumerWidget {
title: 'Introduce tu PIN para firmar',
pin: state.pin,
isProcessing: state.isSigning || state.isLoading,
processingText:
state.isSigning ? 'Firmando...' : 'Creando perfil...',
canSubmit:
vm.canSubmitPin && !state.isSigning && !state.isLoading,
processingText: state.isSigning
? 'Firmando...'
: 'Creando perfil...',
canSubmit: vm.canSubmitPin && !state.isSigning && !state.isLoading,
submitText: 'Confirmar',
errorMessage: state.errorMessage.isNotEmpty
? context.translate(state.errorMessage)
: null,
onDigitPressed: vm.onDigitPressed,
onBackspacePressed: vm.onBackspacePressed,
onClearPin: vm.clearPin,
onSubmit: () async {
await vm.createChildProfile(pin: state.pin);
final ok = await vm.createChildProfile(pin: state.pin);
if (!context.mounted) return;
Navigator.of(context).pop();
if (ok) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => SuccessScreen()),
);
}
},
),
),

View File

@@ -1 +1 @@
enum AddKidMainStep { linkDevice, profile, success }
enum AddKidMainStep { linkDevice, profile, allowance }

View File

@@ -1 +1 @@
enum AddKidStep { intro, linkInfo, scanStrap, scanWatch, profile, success }
enum AddKidStep { intro, linkInfo, scanStrap, scanWatch, profile, allowance }

View File

@@ -29,10 +29,11 @@ class DeviceSetupViewModel extends Notifier<DeviceSetupViewState> {
late final TextEditingController addressController;
late final TextEditingController strapCodeController;
late final TextEditingController watchCodeController;
late final TextEditingController allowanceAmountController;
@override
DeviceSetupViewState build() {
final initial = DeviceSetupViewState(id: const Uuid().v4());
final initial = DeviceSetupViewState(id: const Uuid().v4(), step: AddKidStep.allowance); // TODO: revert to default (intro)
_initControllers(initial);
_addListeners();
@@ -55,6 +56,7 @@ class DeviceSetupViewModel extends Notifier<DeviceSetupViewState> {
addressController = TextEditingController(text: s.address);
strapCodeController = TextEditingController(text: s.strapCode);
watchCodeController = TextEditingController(text: s.watchCode);
allowanceAmountController = TextEditingController(text: s.allowanceAmount);
}
void _addListeners() {
@@ -65,6 +67,7 @@ class DeviceSetupViewModel extends Notifier<DeviceSetupViewState> {
strapCodeController.addListener(_onStrapCodeChanged);
watchCodeController.addListener(_onWatchCodeChanged);
allowanceAmountController.addListener(_onAllowanceAmountChanged);
}
void next() {
@@ -93,7 +96,7 @@ class DeviceSetupViewModel extends Notifier<DeviceSetupViewState> {
return;
case AddKidStep.profile:
return;
case AddKidStep.success:
case AddKidStep.allowance:
return;
}
}
@@ -115,7 +118,7 @@ class DeviceSetupViewModel extends Notifier<DeviceSetupViewState> {
case AddKidStep.profile:
state = state.copyWith(step: AddKidStep.scanWatch);
return;
case AddKidStep.success:
case AddKidStep.allowance:
state = state.copyWith(step: AddKidStep.profile);
return;
}
@@ -193,7 +196,6 @@ class DeviceSetupViewModel extends Notifier<DeviceSetupViewState> {
state = state.copyWith(
isLoading: false,
isSuccess: true,
step: AddKidStep.success,
);
return true;
} catch (e) {
@@ -295,6 +297,20 @@ class DeviceSetupViewModel extends Notifier<DeviceSetupViewState> {
state = state.copyWith(watchCode: text, errorMessage: '');
}
void _onAllowanceAmountChanged() {
final text = allowanceAmountController.text;
if (text == state.allowanceAmount) return;
state = state.copyWith(allowanceAmount: text, errorMessage: '');
}
void setError(String message) {
state = state.copyWith(errorMessage: message);
}
void goToAllowance() {
state = state.copyWith(step: AddKidStep.allowance, errorMessage: '');
}
void onGenrerChanged(String? value) {
final v = value ?? '';
if (v == state.genrer) return;
@@ -403,6 +419,7 @@ class DeviceSetupViewModel extends Notifier<DeviceSetupViewState> {
addressController.clear();
strapCodeController.clear();
watchCodeController.clear();
allowanceAmountController.clear();
state = DeviceSetupViewState(id: const Uuid().v4());
}

View File

@@ -28,5 +28,7 @@ abstract class DeviceSetupViewState with _$DeviceSetupViewState {
@Default('') String pin,
@Default(false) bool isSigning,
@Default('') String lastSignature,
@Default('') String allowanceAmount,
}) = _AddKidFlowState;
}

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$DeviceSetupViewState {
AddKidStep get step; String get id; String get parentId; String get firstName; String get lastName; DateTime? get bornAt; String get address; String get genrer; String get relationType; String get strapQr; String get strapCode; String get watchQr; String get watchCode; bool get isLoading; String get errorMessage; bool get isSuccess; String get pin; bool get isSigning; String get lastSignature;
AddKidStep get step; String get id; String get parentId; String get firstName; String get lastName; DateTime? get bornAt; String get address; String get genrer; String get relationType; String get strapQr; String get strapCode; String get watchQr; String get watchCode; bool get isLoading; String get errorMessage; bool get isSuccess; String get pin; bool get isSigning; String get lastSignature; String get allowanceAmount;
/// Create a copy of DeviceSetupViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $DeviceSetupViewStateCopyWith<DeviceSetupViewState> get copyWith => _$DeviceSetu
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceSetupViewState&&(identical(other.step, step) || other.step == step)&&(identical(other.id, id) || other.id == id)&&(identical(other.parentId, parentId) || other.parentId == parentId)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bornAt, bornAt) || other.bornAt == bornAt)&&(identical(other.address, address) || other.address == address)&&(identical(other.genrer, genrer) || other.genrer == genrer)&&(identical(other.relationType, relationType) || other.relationType == relationType)&&(identical(other.strapQr, strapQr) || other.strapQr == strapQr)&&(identical(other.strapCode, strapCode) || other.strapCode == strapCode)&&(identical(other.watchQr, watchQr) || other.watchQr == watchQr)&&(identical(other.watchCode, watchCode) || other.watchCode == watchCode)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isSuccess, isSuccess) || other.isSuccess == isSuccess)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.isSigning, isSigning) || other.isSigning == isSigning)&&(identical(other.lastSignature, lastSignature) || other.lastSignature == lastSignature));
return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceSetupViewState&&(identical(other.step, step) || other.step == step)&&(identical(other.id, id) || other.id == id)&&(identical(other.parentId, parentId) || other.parentId == parentId)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bornAt, bornAt) || other.bornAt == bornAt)&&(identical(other.address, address) || other.address == address)&&(identical(other.genrer, genrer) || other.genrer == genrer)&&(identical(other.relationType, relationType) || other.relationType == relationType)&&(identical(other.strapQr, strapQr) || other.strapQr == strapQr)&&(identical(other.strapCode, strapCode) || other.strapCode == strapCode)&&(identical(other.watchQr, watchQr) || other.watchQr == watchQr)&&(identical(other.watchCode, watchCode) || other.watchCode == watchCode)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isSuccess, isSuccess) || other.isSuccess == isSuccess)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.isSigning, isSigning) || other.isSigning == isSigning)&&(identical(other.lastSignature, lastSignature) || other.lastSignature == lastSignature)&&(identical(other.allowanceAmount, allowanceAmount) || other.allowanceAmount == allowanceAmount));
}
@override
int get hashCode => Object.hashAll([runtimeType,step,id,parentId,firstName,lastName,bornAt,address,genrer,relationType,strapQr,strapCode,watchQr,watchCode,isLoading,errorMessage,isSuccess,pin,isSigning,lastSignature]);
int get hashCode => Object.hashAll([runtimeType,step,id,parentId,firstName,lastName,bornAt,address,genrer,relationType,strapQr,strapCode,watchQr,watchCode,isLoading,errorMessage,isSuccess,pin,isSigning,lastSignature,allowanceAmount]);
@override
String toString() {
return 'DeviceSetupViewState(step: $step, id: $id, parentId: $parentId, firstName: $firstName, lastName: $lastName, bornAt: $bornAt, address: $address, genrer: $genrer, relationType: $relationType, strapQr: $strapQr, strapCode: $strapCode, watchQr: $watchQr, watchCode: $watchCode, isLoading: $isLoading, errorMessage: $errorMessage, isSuccess: $isSuccess, pin: $pin, isSigning: $isSigning, lastSignature: $lastSignature)';
return 'DeviceSetupViewState(step: $step, id: $id, parentId: $parentId, firstName: $firstName, lastName: $lastName, bornAt: $bornAt, address: $address, genrer: $genrer, relationType: $relationType, strapQr: $strapQr, strapCode: $strapCode, watchQr: $watchQr, watchCode: $watchCode, isLoading: $isLoading, errorMessage: $errorMessage, isSuccess: $isSuccess, pin: $pin, isSigning: $isSigning, lastSignature: $lastSignature, allowanceAmount: $allowanceAmount)';
}
@@ -45,7 +45,7 @@ abstract mixin class $DeviceSetupViewStateCopyWith<$Res> {
factory $DeviceSetupViewStateCopyWith(DeviceSetupViewState value, $Res Function(DeviceSetupViewState) _then) = _$DeviceSetupViewStateCopyWithImpl;
@useResult
$Res call({
AddKidStep step, String id, String parentId, String firstName, String lastName, DateTime? bornAt, String address, String genrer, String relationType, String strapQr, String strapCode, String watchQr, String watchCode, bool isLoading, String errorMessage, bool isSuccess, String pin, bool isSigning, String lastSignature
AddKidStep step, String id, String parentId, String firstName, String lastName, DateTime? bornAt, String address, String genrer, String relationType, String strapQr, String strapCode, String watchQr, String watchCode, bool isLoading, String errorMessage, bool isSuccess, String pin, bool isSigning, String lastSignature, String allowanceAmount
});
@@ -62,7 +62,7 @@ class _$DeviceSetupViewStateCopyWithImpl<$Res>
/// Create a copy of DeviceSetupViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? step = null,Object? id = null,Object? parentId = null,Object? firstName = null,Object? lastName = null,Object? bornAt = freezed,Object? address = null,Object? genrer = null,Object? relationType = null,Object? strapQr = null,Object? strapCode = null,Object? watchQr = null,Object? watchCode = null,Object? isLoading = null,Object? errorMessage = null,Object? isSuccess = null,Object? pin = null,Object? isSigning = null,Object? lastSignature = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? step = null,Object? id = null,Object? parentId = null,Object? firstName = null,Object? lastName = null,Object? bornAt = freezed,Object? address = null,Object? genrer = null,Object? relationType = null,Object? strapQr = null,Object? strapCode = null,Object? watchQr = null,Object? watchCode = null,Object? isLoading = null,Object? errorMessage = null,Object? isSuccess = null,Object? pin = null,Object? isSigning = null,Object? lastSignature = null,Object? allowanceAmount = null,}) {
return _then(_self.copyWith(
step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable
as AddKidStep,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
@@ -83,6 +83,7 @@ as String,isSuccess: null == isSuccess ? _self.isSuccess : isSuccess // ignore:
as bool,pin: null == pin ? _self.pin : pin // ignore: cast_nullable_to_non_nullable
as String,isSigning: null == isSigning ? _self.isSigning : isSigning // ignore: cast_nullable_to_non_nullable
as bool,lastSignature: null == lastSignature ? _self.lastSignature : lastSignature // ignore: cast_nullable_to_non_nullable
as String,allowanceAmount: null == allowanceAmount ? _self.allowanceAmount : allowanceAmount // ignore: cast_nullable_to_non_nullable
as String,
));
}
@@ -168,10 +169,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( AddKidStep step, String id, String parentId, String firstName, String lastName, DateTime? bornAt, String address, String genrer, String relationType, String strapQr, String strapCode, String watchQr, String watchCode, bool isLoading, String errorMessage, bool isSuccess, String pin, bool isSigning, String lastSignature)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( AddKidStep step, String id, String parentId, String firstName, String lastName, DateTime? bornAt, String address, String genrer, String relationType, String strapQr, String strapCode, String watchQr, String watchCode, bool isLoading, String errorMessage, bool isSuccess, String pin, bool isSigning, String lastSignature, String allowanceAmount)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AddKidFlowState() when $default != null:
return $default(_that.step,_that.id,_that.parentId,_that.firstName,_that.lastName,_that.bornAt,_that.address,_that.genrer,_that.relationType,_that.strapQr,_that.strapCode,_that.watchQr,_that.watchCode,_that.isLoading,_that.errorMessage,_that.isSuccess,_that.pin,_that.isSigning,_that.lastSignature);case _:
return $default(_that.step,_that.id,_that.parentId,_that.firstName,_that.lastName,_that.bornAt,_that.address,_that.genrer,_that.relationType,_that.strapQr,_that.strapCode,_that.watchQr,_that.watchCode,_that.isLoading,_that.errorMessage,_that.isSuccess,_that.pin,_that.isSigning,_that.lastSignature,_that.allowanceAmount);case _:
return orElse();
}
@@ -189,10 +190,10 @@ return $default(_that.step,_that.id,_that.parentId,_that.firstName,_that.lastNam
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( AddKidStep step, String id, String parentId, String firstName, String lastName, DateTime? bornAt, String address, String genrer, String relationType, String strapQr, String strapCode, String watchQr, String watchCode, bool isLoading, String errorMessage, bool isSuccess, String pin, bool isSigning, String lastSignature) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( AddKidStep step, String id, String parentId, String firstName, String lastName, DateTime? bornAt, String address, String genrer, String relationType, String strapQr, String strapCode, String watchQr, String watchCode, bool isLoading, String errorMessage, bool isSuccess, String pin, bool isSigning, String lastSignature, String allowanceAmount) $default,) {final _that = this;
switch (_that) {
case _AddKidFlowState():
return $default(_that.step,_that.id,_that.parentId,_that.firstName,_that.lastName,_that.bornAt,_that.address,_that.genrer,_that.relationType,_that.strapQr,_that.strapCode,_that.watchQr,_that.watchCode,_that.isLoading,_that.errorMessage,_that.isSuccess,_that.pin,_that.isSigning,_that.lastSignature);case _:
return $default(_that.step,_that.id,_that.parentId,_that.firstName,_that.lastName,_that.bornAt,_that.address,_that.genrer,_that.relationType,_that.strapQr,_that.strapCode,_that.watchQr,_that.watchCode,_that.isLoading,_that.errorMessage,_that.isSuccess,_that.pin,_that.isSigning,_that.lastSignature,_that.allowanceAmount);case _:
throw StateError('Unexpected subclass');
}
@@ -209,10 +210,10 @@ return $default(_that.step,_that.id,_that.parentId,_that.firstName,_that.lastNam
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( AddKidStep step, String id, String parentId, String firstName, String lastName, DateTime? bornAt, String address, String genrer, String relationType, String strapQr, String strapCode, String watchQr, String watchCode, bool isLoading, String errorMessage, bool isSuccess, String pin, bool isSigning, String lastSignature)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( AddKidStep step, String id, String parentId, String firstName, String lastName, DateTime? bornAt, String address, String genrer, String relationType, String strapQr, String strapCode, String watchQr, String watchCode, bool isLoading, String errorMessage, bool isSuccess, String pin, bool isSigning, String lastSignature, String allowanceAmount)? $default,) {final _that = this;
switch (_that) {
case _AddKidFlowState() when $default != null:
return $default(_that.step,_that.id,_that.parentId,_that.firstName,_that.lastName,_that.bornAt,_that.address,_that.genrer,_that.relationType,_that.strapQr,_that.strapCode,_that.watchQr,_that.watchCode,_that.isLoading,_that.errorMessage,_that.isSuccess,_that.pin,_that.isSigning,_that.lastSignature);case _:
return $default(_that.step,_that.id,_that.parentId,_that.firstName,_that.lastName,_that.bornAt,_that.address,_that.genrer,_that.relationType,_that.strapQr,_that.strapCode,_that.watchQr,_that.watchCode,_that.isLoading,_that.errorMessage,_that.isSuccess,_that.pin,_that.isSigning,_that.lastSignature,_that.allowanceAmount);case _:
return null;
}
@@ -224,7 +225,7 @@ return $default(_that.step,_that.id,_that.parentId,_that.firstName,_that.lastNam
class _AddKidFlowState implements DeviceSetupViewState {
const _AddKidFlowState({this.step = AddKidStep.intro, this.id = '', this.parentId = '', this.firstName = '', this.lastName = '', this.bornAt, this.address = '', this.genrer = '', this.relationType = '', this.strapQr = '', this.strapCode = '', this.watchQr = '', this.watchCode = '', this.isLoading = false, this.errorMessage = '', this.isSuccess = false, this.pin = '', this.isSigning = false, this.lastSignature = ''});
const _AddKidFlowState({this.step = AddKidStep.intro, this.id = '', this.parentId = '', this.firstName = '', this.lastName = '', this.bornAt, this.address = '', this.genrer = '', this.relationType = '', this.strapQr = '', this.strapCode = '', this.watchQr = '', this.watchCode = '', this.isLoading = false, this.errorMessage = '', this.isSuccess = false, this.pin = '', this.isSigning = false, this.lastSignature = '', this.allowanceAmount = ''});
@override@JsonKey() final AddKidStep step;
@@ -246,6 +247,7 @@ class _AddKidFlowState implements DeviceSetupViewState {
@override@JsonKey() final String pin;
@override@JsonKey() final bool isSigning;
@override@JsonKey() final String lastSignature;
@override@JsonKey() final String allowanceAmount;
/// Create a copy of DeviceSetupViewState
/// with the given fields replaced by the non-null parameter values.
@@ -257,16 +259,16 @@ _$AddKidFlowStateCopyWith<_AddKidFlowState> get copyWith => __$AddKidFlowStateCo
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AddKidFlowState&&(identical(other.step, step) || other.step == step)&&(identical(other.id, id) || other.id == id)&&(identical(other.parentId, parentId) || other.parentId == parentId)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bornAt, bornAt) || other.bornAt == bornAt)&&(identical(other.address, address) || other.address == address)&&(identical(other.genrer, genrer) || other.genrer == genrer)&&(identical(other.relationType, relationType) || other.relationType == relationType)&&(identical(other.strapQr, strapQr) || other.strapQr == strapQr)&&(identical(other.strapCode, strapCode) || other.strapCode == strapCode)&&(identical(other.watchQr, watchQr) || other.watchQr == watchQr)&&(identical(other.watchCode, watchCode) || other.watchCode == watchCode)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isSuccess, isSuccess) || other.isSuccess == isSuccess)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.isSigning, isSigning) || other.isSigning == isSigning)&&(identical(other.lastSignature, lastSignature) || other.lastSignature == lastSignature));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AddKidFlowState&&(identical(other.step, step) || other.step == step)&&(identical(other.id, id) || other.id == id)&&(identical(other.parentId, parentId) || other.parentId == parentId)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bornAt, bornAt) || other.bornAt == bornAt)&&(identical(other.address, address) || other.address == address)&&(identical(other.genrer, genrer) || other.genrer == genrer)&&(identical(other.relationType, relationType) || other.relationType == relationType)&&(identical(other.strapQr, strapQr) || other.strapQr == strapQr)&&(identical(other.strapCode, strapCode) || other.strapCode == strapCode)&&(identical(other.watchQr, watchQr) || other.watchQr == watchQr)&&(identical(other.watchCode, watchCode) || other.watchCode == watchCode)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isSuccess, isSuccess) || other.isSuccess == isSuccess)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.isSigning, isSigning) || other.isSigning == isSigning)&&(identical(other.lastSignature, lastSignature) || other.lastSignature == lastSignature)&&(identical(other.allowanceAmount, allowanceAmount) || other.allowanceAmount == allowanceAmount));
}
@override
int get hashCode => Object.hashAll([runtimeType,step,id,parentId,firstName,lastName,bornAt,address,genrer,relationType,strapQr,strapCode,watchQr,watchCode,isLoading,errorMessage,isSuccess,pin,isSigning,lastSignature]);
int get hashCode => Object.hashAll([runtimeType,step,id,parentId,firstName,lastName,bornAt,address,genrer,relationType,strapQr,strapCode,watchQr,watchCode,isLoading,errorMessage,isSuccess,pin,isSigning,lastSignature,allowanceAmount]);
@override
String toString() {
return 'DeviceSetupViewState(step: $step, id: $id, parentId: $parentId, firstName: $firstName, lastName: $lastName, bornAt: $bornAt, address: $address, genrer: $genrer, relationType: $relationType, strapQr: $strapQr, strapCode: $strapCode, watchQr: $watchQr, watchCode: $watchCode, isLoading: $isLoading, errorMessage: $errorMessage, isSuccess: $isSuccess, pin: $pin, isSigning: $isSigning, lastSignature: $lastSignature)';
return 'DeviceSetupViewState(step: $step, id: $id, parentId: $parentId, firstName: $firstName, lastName: $lastName, bornAt: $bornAt, address: $address, genrer: $genrer, relationType: $relationType, strapQr: $strapQr, strapCode: $strapCode, watchQr: $watchQr, watchCode: $watchCode, isLoading: $isLoading, errorMessage: $errorMessage, isSuccess: $isSuccess, pin: $pin, isSigning: $isSigning, lastSignature: $lastSignature, allowanceAmount: $allowanceAmount)';
}
@@ -277,7 +279,7 @@ abstract mixin class _$AddKidFlowStateCopyWith<$Res> implements $DeviceSetupView
factory _$AddKidFlowStateCopyWith(_AddKidFlowState value, $Res Function(_AddKidFlowState) _then) = __$AddKidFlowStateCopyWithImpl;
@override @useResult
$Res call({
AddKidStep step, String id, String parentId, String firstName, String lastName, DateTime? bornAt, String address, String genrer, String relationType, String strapQr, String strapCode, String watchQr, String watchCode, bool isLoading, String errorMessage, bool isSuccess, String pin, bool isSigning, String lastSignature
AddKidStep step, String id, String parentId, String firstName, String lastName, DateTime? bornAt, String address, String genrer, String relationType, String strapQr, String strapCode, String watchQr, String watchCode, bool isLoading, String errorMessage, bool isSuccess, String pin, bool isSigning, String lastSignature, String allowanceAmount
});
@@ -294,7 +296,7 @@ class __$AddKidFlowStateCopyWithImpl<$Res>
/// Create a copy of DeviceSetupViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? step = null,Object? id = null,Object? parentId = null,Object? firstName = null,Object? lastName = null,Object? bornAt = freezed,Object? address = null,Object? genrer = null,Object? relationType = null,Object? strapQr = null,Object? strapCode = null,Object? watchQr = null,Object? watchCode = null,Object? isLoading = null,Object? errorMessage = null,Object? isSuccess = null,Object? pin = null,Object? isSigning = null,Object? lastSignature = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? step = null,Object? id = null,Object? parentId = null,Object? firstName = null,Object? lastName = null,Object? bornAt = freezed,Object? address = null,Object? genrer = null,Object? relationType = null,Object? strapQr = null,Object? strapCode = null,Object? watchQr = null,Object? watchCode = null,Object? isLoading = null,Object? errorMessage = null,Object? isSuccess = null,Object? pin = null,Object? isSigning = null,Object? lastSignature = null,Object? allowanceAmount = null,}) {
return _then(_AddKidFlowState(
step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable
as AddKidStep,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
@@ -315,6 +317,7 @@ as String,isSuccess: null == isSuccess ? _self.isSuccess : isSuccess // ignore:
as bool,pin: null == pin ? _self.pin : pin // ignore: cast_nullable_to_non_nullable
as String,isSigning: null == isSigning ? _self.isSigning : isSigning // ignore: cast_nullable_to_non_nullable
as bool,lastSignature: null == lastSignature ? _self.lastSignature : lastSignature // ignore: cast_nullable_to_non_nullable
as String,allowanceAmount: null == allowanceAmount ? _self.allowanceAmount : allowanceAmount // ignore: cast_nullable_to_non_nullable
as String,
));
}

View File

@@ -3,9 +3,9 @@ import 'package:auth/src/features/device_setup/presentation/enums/add_kid_step.d
import 'package:auth/src/features/device_setup/presentation/enums/scan_link_step.dart';
import 'package:auth/src/features/device_setup/presentation/steps/intro_step.dart';
import 'package:auth/src/features/device_setup/presentation/steps/link_info_step.dart';
import 'package:auth/src/features/device_setup/presentation/steps/allowance_step.dart';
import 'package:auth/src/features/device_setup/presentation/steps/profile_step.dart';
import 'package:auth/src/features/device_setup/presentation/steps/scan_strap_and_watch_step.dart';
import 'package:auth/src/features/device_setup/presentation/steps/success_step.dart';
import 'package:flutter/material.dart';
class StepBody extends StatelessWidget {
@@ -25,8 +25,8 @@ class StepBody extends StatelessWidget {
return ScanStrapAndWatchStepScreen(step: ScanLinkStep.watch);
case AddKidStep.profile:
return ProfileStepScreen();
case AddKidStep.success:
return SuccessStepScreen();
case AddKidStep.allowance:
return AllowanceStepScreen();
}
}
}

View File

@@ -0,0 +1,55 @@
import 'package:auth/src/features/device_setup/presentation/state/device_setup_view_model.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class AllowanceStepScreen extends ConsumerWidget {
const AllowanceStepScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(deviceSetupViewModelProvider.notifier);
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 30),
Text(
'¡Dale su primera paga!',
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Text(
'Enséñales a gestionar su dinero recargando su reloj',
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
CustomTextField(
label: 'Cantidad de dinero de la paga',
hint: '0',
controller: vm.allowanceAmountController,
keyboardType: TextInputType.number,
),
const SizedBox(height: 12),
Text(
'Cantidad mínima: 10 euros',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 24),
Text(
'Por seguridad sólo se puede disponer de un máximo de 150€ por wallet',
style: const TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
],
),
),
);
}
}

View File

@@ -21,7 +21,6 @@ class ProfileStepScreen extends ConsumerWidget {
'OTHER': 'Otro',
};
final theme = ref.watch(themePortProvider);
final state = ref.watch(deviceSetupViewModelProvider);
final vm = ref.read(deviceSetupViewModelProvider.notifier);

View File

@@ -0,0 +1,44 @@
import 'package:auth/src/features/device_setup/presentation/state/device_setup_view_model.dart';
import 'package:auth/src/features/device_setup/presentation/steps/success_step.dart';
import 'package:auth/src/features/device_setup/presentation/widgets/flow_footer.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
class SuccessScreen extends ConsumerWidget {
const SuccessScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(deviceSetupViewModelProvider.notifier);
final theme = ref.watch(themePortProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: SafeArea(
child: Column(
children: [
const SizedBox(height: 20),
Expanded(child: SuccessStepScreen()),
FlowFooter(
primaryText: context.translate(
I18n.deviceSetup_giveFirstAllowance,
),
onPrimary: () {
vm.goToAllowance();
Navigator.of(context).pop();
},
secondaryText: context.translate(I18n.deviceSetup_addAnotherKid),
onSecondary: () {
vm.resetForNewKid();
Navigator.of(context).pop();
},
theme: theme,
),
],
),
),
);
}
}

View File

@@ -30,7 +30,7 @@ class FlowFooter extends StatelessWidget {
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [

View File

@@ -23,9 +23,11 @@ class LoginScreen extends ConsumerWidget {
final state = ref.read(loginViewModelProvider);
if (state.errorMessage.isNotEmpty) {
ScaffoldMessenger.of(
showTopSnackbar(
context,
).showSnackBar(SnackBar(content: Text(state.errorMessage)));
message: state.errorMessage,
type: MessageType.error,
);
return;
}
@@ -74,9 +76,11 @@ class LoginScreen extends ConsumerWidget {
if (!ok) {
final state = ref.read(loginViewModelProvider);
if (state.errorMessage.isNotEmpty) {
ScaffoldMessenger.of(
showTopSnackbar(
context,
).showSnackBar(SnackBar(content: Text(state.errorMessage)));
message: state.errorMessage,
type: MessageType.error,
);
}
return;
}
@@ -87,14 +91,12 @@ class LoginScreen extends ConsumerWidget {
if (user == null) {
final state = ref.read(loginViewModelProvider);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
state.errorMessage.isEmpty
? 'Error getting user info'
: state.errorMessage,
),
),
showTopSnackbar(
context,
message: state.errorMessage.isEmpty
? 'Error getting user info'
: state.errorMessage,
type: MessageType.error,
);
return;
}
@@ -286,9 +288,6 @@ class _SignInSection extends ConsumerWidget {
final bool isLoading = ref.watch(
loginViewModelProvider.select((s) => s.isLoading),
);
final String errorMessage = ref.watch(
loginViewModelProvider.select((s) => s.errorMessage),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -308,18 +307,6 @@ class _SignInSection extends ConsumerWidget {
)
: null,
),
if (errorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
errorMessage,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 13,
),
),
),
],
);
}

View File

@@ -1,3 +1,4 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/app_routes.dart';
@@ -18,6 +19,19 @@ class SCATreezorScreen extends ConsumerWidget {
final SCATreezorViewState state = ref.watch(scaTreezorViewModelProvider);
final vm = ref.read(scaTreezorViewModelProvider.notifier);
ref.listen(
scaTreezorViewModelProvider.select((s) => s.errorMessage),
(previous, next) {
if (next.isNotEmpty) {
showTopSnackbar(
context,
message: context.translate(next),
type: MessageType.error,
);
}
},
);
return Scaffold(
backgroundColor: Colors.white,
body: Center(
@@ -37,9 +51,6 @@ class SCATreezorScreen extends ConsumerWidget {
: 'Firmando...',
canSubmit: vm.canSubmitPin && !state.isConnecting,
submitText: 'Conectar',
errorMessage: state.errorMessage.isNotEmpty
? context.translate(state.errorMessage)
: null,
onDigitPressed: vm.onDigitPressed,
onBackspacePressed: vm.onBackspacePressed,
onClearPin: vm.clearPin,
@@ -70,14 +81,6 @@ class _ProvisioningBody extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 16),
if (state.errorMessage.isNotEmpty) ...[
Text(
context.translate(state.errorMessage),
style: const TextStyle(color: Colors.red, fontSize: 13),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
],
if (state.isProvisioning) ...[
const CircularProgressIndicator(),
const SizedBox(height: 8),

View File

@@ -40,6 +40,19 @@ class SignupScreen extends ConsumerWidget {
final vm = ref.read(signUpViewModelProvider.notifier);
final state = ref.watch(signUpViewModelProvider);
ref.listen(
signUpViewModelProvider.select((s) => s.errorMessage),
(previous, next) {
if (next.isNotEmpty) {
showTopSnackbar(
context,
message: context.translate(next),
type: MessageType.error,
);
}
},
);
final steps = signUpSteps(context);
final index = state.currentIndex.clamp(0, steps.length - 1);
final step = steps[index];
@@ -55,7 +68,6 @@ class SignupScreen extends ConsumerWidget {
currentStep: index + 1,
numSteps: steps.length,
body: step.bodyBuilder(context, ref),
errorMessage: context.translate(state.errorMessage),
onBackPressed: state.currentIndex == 0
? navigationContract.goBack
: vm.back,

View File

@@ -1,4 +1,3 @@
import 'package:auth/src/widgets/form_error_banner.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
@@ -15,8 +14,6 @@ class SignUpLayout extends StatelessWidget {
final VoidCallback onBackPressed;
final VoidCallback onNextPressed;
final String errorMessage;
const SignUpLayout({
super.key,
required this.theme,
@@ -28,7 +25,6 @@ class SignUpLayout extends StatelessWidget {
required this.body,
required this.onBackPressed,
required this.onNextPressed,
this.errorMessage = '',
});
@override
@@ -75,12 +71,7 @@ class SignUpLayout extends StatelessWidget {
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
children: [
FormErrorBanner(message: errorMessage),
body,
],
),
child: body,
),
),

View File

@@ -30,6 +30,8 @@ dependencies:
path: ../../packages/sf_shared
sca_treezor:
path: ../../packages/sca_treezor
payments:
path: ../../packages/payments
#dependencies go here
flutter_svg: ^2.2.1
get_it: ^9.0.5

View File

@@ -1,4 +1,4 @@
# melos_managed_dependency_overrides: dashboard_shell,design_system,home,notifications,profile,sf_shared,navigation,utils,sf_localizations,fonts,sf_infrastructure,flutter_treezor_entrust_sdk_bridge,sca_treezor
# melos_managed_dependency_overrides: dashboard_shell,design_system,home,notifications,profile,sf_shared,navigation,utils,sf_localizations,fonts,sf_infrastructure,flutter_treezor_entrust_sdk_bridge,sca_treezor,payments
dependency_overrides:
dashboard_shell:
path: ../dashboard_shell
@@ -14,6 +14,8 @@ dependency_overrides:
path: ../../packages/navigation
notifications:
path: ../notifications
payments:
path: ../../packages/payments
profile:
path: ../profile
sca_treezor:

View File

@@ -1,4 +1,4 @@
# melos_managed_dependency_overrides: auth,design_system,home,notifications,profile,sf_shared,navigation,utils,sf_localizations,fonts,sf_infrastructure,flutter_treezor_entrust_sdk_bridge,sca_treezor
# melos_managed_dependency_overrides: auth,design_system,home,notifications,profile,sf_shared,navigation,utils,sf_localizations,fonts,sf_infrastructure,flutter_treezor_entrust_sdk_bridge,sca_treezor,payments
dependency_overrides:
auth:
path: ../auth
@@ -14,6 +14,8 @@ dependency_overrides:
path: ../../packages/navigation
notifications:
path: ../notifications
payments:
path: ../../packages/payments
profile:
path: ../profile
sca_treezor:

View File

@@ -1,4 +1,4 @@
# melos_managed_dependency_overrides: auth,dashboard_shell,design_system,notifications,profile,sf_shared,navigation,utils,sf_localizations,fonts,sf_infrastructure,flutter_treezor_entrust_sdk_bridge,sca_treezor
# melos_managed_dependency_overrides: auth,dashboard_shell,design_system,notifications,profile,sf_shared,navigation,utils,sf_localizations,fonts,sf_infrastructure,flutter_treezor_entrust_sdk_bridge,sca_treezor,payments
dependency_overrides:
auth:
path: ../auth
@@ -14,6 +14,8 @@ dependency_overrides:
path: ../../packages/navigation
notifications:
path: ../notifications
payments:
path: ../../packages/payments
profile:
path: ../profile
sca_treezor:

View File

@@ -1,3 +1,4 @@
export 'src/presentation/profile_screen.dart';
export 'src/profile_builder.dart';
export 'src/profile_settings_builder.dart';
export 'src/features/payment_methods/presentation/payment_methods_builder.dart';

View File

@@ -0,0 +1,16 @@
import 'package:payments/payments.dart';
abstract class DeletePaymentCardUseCase {
Future<void> call(String topupCardId);
}
class DeletePaymentCardUseCaseImpl implements DeletePaymentCardUseCase {
DeletePaymentCardUseCaseImpl(this._repository);
final HiPayRepository _repository;
@override
Future<void> call(String topupCardId) {
return _repository.deleteTopupCard(topupCardId);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:payments/payments.dart';
abstract class GetPaymentCardsUseCase {
Future<List<PaymentCardEntity>> call();
}
class GetPaymentCardsUseCaseImpl implements GetPaymentCardsUseCase {
GetPaymentCardsUseCaseImpl(this._repository);
final HiPayRepository _repository;
@override
Future<List<PaymentCardEntity>> call() {
return _repository.getTopupCards();
}
}

View File

@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:navigation/navigation.dart';
import 'package:profile/src/features/payment_methods/presentation/payment_methods_screen.dart';
class PaymentMethodsBuilder {
const PaymentMethodsBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final navigationContract = GetIt.I<NavigationContract>();
return MaterialPage(
key: state.pageKey,
child: PaymentMethodsScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,299 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:navigation/navigation.dart';
import 'package:payments/payments.dart';
import 'package:profile/src/features/payment_methods/presentation/payment_methods_view_model.dart';
import 'package:profile/src/features/payment_methods/presentation/payment_methods_view_state.dart';
class PaymentMethodsScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const PaymentMethodsScreen({super.key, required this.navigationContract});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(paymentMethodsViewModelProvider);
return Stack(
children: [
Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
appBar: AppBar(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
title: Text(
'Métodos de pago',
style: TextStyle(
color: theme.getColorFor(ThemeCode.textPrimary),
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: theme.getColorFor(ThemeCode.textPrimary),
),
onPressed: () => navigationContract.goBack(),
),
),
body: _buildBody(context, ref, theme, viewState),
),
if (viewState.isDeleting)
Container(
color: Colors.black26,
child: const Center(child: CircularProgressIndicator()),
),
],
);
}
Widget _buildBody(
BuildContext context,
WidgetRef ref,
ThemePort theme,
PaymentMethodsViewState viewState,
) {
if (viewState.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (viewState.errorMessage.isNotEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Error al cargar las tarjetas',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
const SizedBox(height: 8),
Text(
viewState.errorMessage,
textAlign: TextAlign.center,
style: TextStyle(
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
const SizedBox(height: 16),
TextButton(
onPressed: () => ref
.read(paymentMethodsViewModelProvider.notifier)
.loadCards(),
child: const Text('Reintentar'),
),
],
),
),
);
}
return _buildContent(context, ref, theme, viewState.cards);
}
Widget _buildContent(
BuildContext context,
WidgetRef ref,
ThemePort theme,
List<PaymentCardEntity> cards,
) {
return Column(
children: [
Expanded(
child: cards.isEmpty
? Center(
child: Text(
'No tienes tarjetas registradas',
style: TextStyle(
fontSize: 16,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
)
: ListView.separated(
padding: const EdgeInsets.all(20),
itemCount: cards.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) =>
_buildCardTile(context, ref, theme, cards[index]),
),
),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
child: PrimaryButton(
onPressed: () => _addCard(context, ref),
text: 'Agregar tarjeta',
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
),
],
);
}
Widget _buildCardTile(
BuildContext context,
WidgetRef ref,
ThemePort theme,
PaymentCardEntity card,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
child: Row(
children: [
_brandIcon(card.brand),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
card.maskedPan,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
const SizedBox(height: 4),
Text(
card.cardHolder,
style: TextStyle(
fontSize: 14,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
const SizedBox(height: 2),
Text(
'${card.cardExpiryMonth}/${card.cardExpiryYear}',
style: TextStyle(
fontSize: 12,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
],
),
),
IconButton(
icon: Icon(
Icons.delete_outline,
color: Theme.of(context).colorScheme.error,
),
onPressed: () => _confirmDelete(context, ref, card),
),
],
),
);
}
Widget _brandIcon(String brand) {
switch (brand.toLowerCase()) {
case 'visa':
return SvgPicture.asset(
'assets/images/ui/visa.svg',
width: 28,
height: 12,
);
case 'mastercard':
return SvgPicture.asset(
'assets/images/ui/mastercard.svg',
width: 40,
height: 28,
);
default:
return const Icon(Icons.credit_card, size: 36);
}
}
Future<void> _confirmDelete(
BuildContext context,
WidgetRef ref,
PaymentCardEntity card,
) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Eliminar tarjeta'),
content: Text('¿Deseas eliminar la tarjeta ${card.maskedPan}?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Eliminar'),
),
],
),
);
if (confirmed == true) {
if (!context.mounted) return;
final success = await ref
.read(paymentMethodsViewModelProvider.notifier)
.deleteCard(card.topupCardId);
if (!context.mounted) return;
if (success) {
showTopSnackbar(
context,
message: 'Tarjeta eliminada correctamente',
type: MessageType.success,
);
} else {
showTopSnackbar(
context,
message: 'Error al eliminar la tarjeta',
type: MessageType.error,
);
}
}
}
Future<void> _addCard(BuildContext context, WidgetRef ref) async {
final result = await Navigator.of(context).push<HiPayResult>(
MaterialPageRoute(
builder: (_) =>
HiPayWebViewScreen(navigationContract: navigationContract),
),
);
if (!context.mounted) return;
if (result == HiPayResult.success) {
ref.read(paymentMethodsViewModelProvider.notifier).loadCards();
showTopSnackbar(
context,
message: 'Tarjeta agregada correctamente',
type: MessageType.success,
);
} else if (result == HiPayResult.cancelled) {
showTopSnackbar(
context,
message: 'Se canceló el registro de la tarjeta',
type: MessageType.error,
);
}
}
}

View File

@@ -0,0 +1,66 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:profile/src/features/payment_methods/domain/use_cases/delete_payment_card_use_case.dart';
import 'package:profile/src/features/payment_methods/domain/use_cases/get_payment_cards_use_case.dart';
import 'package:profile/src/features/payment_methods/presentation/payment_methods_view_state.dart';
import 'package:profile/src/features/payment_methods/providers/payment_methods_providers.dart';
final paymentMethodsViewModelProvider =
NotifierProvider.autoDispose<
PaymentMethodsViewModel,
PaymentMethodsViewState
>(PaymentMethodsViewModel.new);
class PaymentMethodsViewModel extends Notifier<PaymentMethodsViewState> {
late final GetPaymentCardsUseCase _getPaymentCardsUseCase;
late final DeletePaymentCardUseCase _deletePaymentCardUseCase;
@override
PaymentMethodsViewState build() {
_getPaymentCardsUseCase = ref.read(getPaymentCardsUseCaseProvider);
_deletePaymentCardUseCase = ref.read(deletePaymentCardUseCaseProvider);
Future.microtask(() => loadCards());
return const PaymentMethodsViewState(isLoading: true);
}
Future<void> loadCards() async {
state = state.copyWith(isLoading: true, errorMessage: '');
try {
final cards = await _getPaymentCardsUseCase();
if (!ref.mounted) return;
final validated = cards
.where((c) => c.status.toLowerCase() == 'validated')
.toList();
state = state.copyWith(isLoading: false, cards: validated);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoading: false, errorMessage: e.toString());
}
}
Future<bool> deleteCard(String topupCardId) async {
state = state.copyWith(isDeleting: true);
try {
await _deletePaymentCardUseCase(topupCardId);
if (!ref.mounted) return false;
final updatedCards = state.cards
.where((c) => c.topupCardId != topupCardId)
.toList();
state = state.copyWith(isDeleting: false, cards: updatedCards);
return true;
} catch (e) {
if (!ref.mounted) return false;
state = state.copyWith(isDeleting: false, errorMessage: e.toString());
return false;
}
}
}

View File

@@ -0,0 +1,29 @@
import 'package:payments/payments.dart';
class PaymentMethodsViewState {
final bool isLoading;
final bool isDeleting;
final List<PaymentCardEntity> cards;
final String errorMessage;
const PaymentMethodsViewState({
this.isLoading = false,
this.isDeleting = false,
this.cards = const [],
this.errorMessage = '',
});
PaymentMethodsViewState copyWith({
bool? isLoading,
bool? isDeleting,
List<PaymentCardEntity>? cards,
String? errorMessage,
}) {
return PaymentMethodsViewState(
isLoading: isLoading ?? this.isLoading,
isDeleting: isDeleting ?? this.isDeleting,
cards: cards ?? this.cards,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:payments/payments.dart';
import 'package:profile/src/features/payment_methods/domain/use_cases/delete_payment_card_use_case.dart';
import 'package:profile/src/features/payment_methods/domain/use_cases/get_payment_cards_use_case.dart';
final getPaymentCardsUseCaseProvider =
Provider.autoDispose<GetPaymentCardsUseCase>((ref) {
final repository = ref.read(hipayRepositoryProvider);
return GetPaymentCardsUseCaseImpl(repository);
});
final deletePaymentCardUseCaseProvider =
Provider.autoDispose<DeletePaymentCardUseCase>((ref) {
final repository = ref.read(hipayRepositoryProvider);
return DeletePaymentCardUseCaseImpl(repository);
});

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
import 'package:sealed_countries/sealed_countries.dart';
import '../providers/logout_provider.dart';
import '../providers/payment_profile_provider.dart';
class ProfileSettingsScreen extends ConsumerWidget {
@@ -143,10 +144,6 @@ class ProfileSettingsScreen extends ConsumerWidget {
_labelValue("Fecha de nacimiento", birthDate),
_labelValue("Nacionalidad", nationality),
_labelValue("Lugar de nacimiento", profile.placeOfBirth),
_labelValue(
"Documento (${profile.documentType})",
profile.document.toUpperCase(),
),
],
),
),
@@ -229,7 +226,10 @@ class ProfileSettingsScreen extends ConsumerWidget {
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
Spacer(),
TextButton(onPressed: () => {}, child: Text("Editar")),
TextButton(
onPressed: () => navigationContract.goTo(AppRoutes.dashboardProfilePaymentMethods),
child: Text("Editar"),
),
],
),
Text("Puedes cambiar el método de pago en cualquier momento"),
@@ -354,6 +354,25 @@ class ProfileSettingsScreen extends ConsumerWidget {
),
],
),
Align(
alignment: Alignment.topLeft,
child: TextButton(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
onPressed: () => _logout(context, ref),
child: Row(
spacing: 4,
children: [
Icon(Icons.logout, size: 24, color: Colors.red),
Text(
"Cerrar sesión",
style: TextStyle(color: Colors.red),
),
],
),
),
),
];
return Stack(
@@ -413,6 +432,41 @@ class ProfileSettingsScreen extends ConsumerWidget {
);
}
Future<void> _logout(BuildContext context, WidgetRef ref) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Cerrar sesión'),
content: Text('¿Estás seguro de que deseas cerrar sesión?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text('Cancelar'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text('Cerrar sesión'),
),
],
),
);
if (confirmed == true) {
try {
await ref.read(logoutProvider.future);
if (!context.mounted) return;
navigationContract.goTo(AppRoutes.login);
} catch (e) {
if (!context.mounted) return;
showTopSnackbar(
context,
message: 'Error al cerrar sesión',
type: MessageType.error,
);
}
}
}
Widget _labelValue(String label, String value) {
return Align(
alignment: Alignment.centerLeft,

View File

@@ -0,0 +1,13 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
final logoutProvider = FutureProvider.autoDispose<void>((ref) async {
final repository = getIt<QuestiaRepository>();
try {
await repository.post<void>('/auth/logout');
} on DioException catch (error) {
throw Exception(error.message ?? 'Error in logout');
}
await clearSessionData();
});

View File

@@ -22,7 +22,12 @@ dependencies:
path: ../../packages/sf_shared
navigation:
path: ../../packages/navigation
sf_infrastructure:
path: ../../packages/sf_infrastructure
payments:
path: ../../packages/payments
dio: ^5.9.0
#dependencies go here
sealed_countries: ^2.8.0
flutter_riverpod: ^3.0.3