Merge pull request 'Added linkPhone use cases, auth repository providers and data folder' (#9) from feature/auth-link-phone into develop

Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
2025-12-09 09:10:01 +00:00
34 changed files with 470 additions and 183 deletions

View File

@@ -0,0 +1,31 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@@ -0,0 +1 @@
{"version":2,"entries":[{"package":"sf_app_platform_mono_repo","rootUri":"../","packageUri":"lib/"}]}

View File

@@ -1 +1,2 @@
API_BASE_URL=https://api-neki-b2b.neki.es/gateway/api/
API_ORIGIN =https://neki-b2b.neki.es

View File

@@ -2,6 +2,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
class Env {
static String get apiBaseUrl => dotenv.env['API_BASE_URL'] ?? '';
static String get apiOrigin => dotenv.env['API_ORIGIN'] ?? '';
// static String get apiKey => dotenv.env['API_KEY'] ?? '';
}

View File

@@ -4,6 +4,8 @@ import 'env.dart';
class QuestiaEnvConfig implements EnvConfig {
@override
String get apiBaseUrl => Env.apiBaseUrl;
@override
String get apiOrigin => Env.apiOrigin;
// @override
// String get apiKey => Env.apiKey;

View File

@@ -15,7 +15,7 @@ late final GoRouter appRouter;
void configureAppRouter() {
appRouter = GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: AppRoutes.linkPhone,
initialLocation: AppRoutes.login,
debugLogDiagnostics: true,
routes: [
GoRoute(
@@ -35,13 +35,13 @@ void configureAppRouter() {
),
GoRoute(
path: AppRoutes.linkPhone,
name: 'link_phone',
pageBuilder: LinkPhoneBuilder().buildPage,
name: 'request_link_phone',
pageBuilder: RequestLinkPhoneBuilder().buildPage,
),
GoRoute(
path: AppRoutes.phoneCode,
name: 'phone_code',
pageBuilder: PhoneCodeBuilder().buildPage,
name: 'Verify_link_phone_code',
pageBuilder: VerifyLinkPhoneCodeBuilder().buildPage,
),
GoRoute(
path: AppRoutes.recoverPassword,

View File

@@ -448,14 +448,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.20.2"
intl_phone_field_v2:
dependency: transitive
description:
name: intl_phone_field_v2
sha256: b1e5077e31cc8705639a69b2e0410a8ecc858c3e518726d99b378b6c35adfefb
url: "https://pub.dev"
source: hosted
version: "4.0.5"
io:
dependency: transitive
description:

View File

@@ -1,7 +1,7 @@
export 'src/features/device_sign_up/link_watch/create_profile_screen.dart';
export 'src/features/onboarding/onboarding_builder.dart';
export 'src/features/link_phone/link_phone_builder.dart';
export 'src/features/login/phone_code_builder.dart';
export 'src/features/link_phone/presentation/request_phone/request_link_phone_builder.dart';
export 'src/features/link_phone/presentation/verify_code/verify_link_phone_code_builder.dart';
export 'src/features/login/login_builder.dart';
export 'src/features/recover_password/recover_password_builder.dart';
export 'src/features/device_sign_up/device_signup_builder.dart';

View File

@@ -16,7 +16,10 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource {
body: <String, dynamic>{'phone': phone},
);
} on DioException catch (error) {
throw _mapDioError(error, defaultMessage: 'Error al solicitar el código');
throw _mapDioError(
error,
defaultMessage: error.response?.data ?? 'Error to request phone code',
);
}
}
@@ -31,7 +34,7 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource {
body: <String, dynamic>{'phone': phone, 'code': code},
);
} on DioException catch (error) {
throw _mapDioError(error, defaultMessage: 'Error al verificar el código');
throw _mapDioError(error, defaultMessage: 'Error in verification code');
}
}

View File

@@ -7,12 +7,14 @@ class LinkPhoneUseCaseImpl implements LinkPhoneUseCase {
final AuthRepository _repository;
@override
Future<void> requestCode({required String phone}) {
return _repository.requestPhoneCode(phone: phone);
Future<void> requestCode({required String phone}) async {
// return _repository.requestPhoneCode(phone: phone);
await Future<void>.delayed(const Duration(milliseconds: 500));
}
@override
Future<void> verifyCode({required String phone, required String code}) {
return _repository.verifyPhoneCode(phone: phone, code: code);
Future<void> verifyCode({required String phone, required String code}) async {
// return _repository.verifyPhoneCode(phone: phone, code: code);
await Future<void>.delayed(const Duration(milliseconds: 500));
}
}

View File

@@ -1,18 +1,18 @@
import 'package:auth/src/features/link_phone/presentation/link_phone_screen.dart';
import 'package:auth/src/features/link_phone/presentation/request_phone/request_link_phone_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class LinkPhoneBuilder {
const LinkPhoneBuilder();
class RequestLinkPhoneBuilder {
const RequestLinkPhoneBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: LinkPhoneScreen(navigationContract: navigationContract),
child: RequestLinkPhoneScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -1,15 +1,14 @@
import 'package:auth/src/features/link_phone/presentation/link_phone_view_model.dart';
import 'package:design_system/src/dropdowns/country_prefix_picker.dart';
import 'package:auth/src/features/link_phone/presentation/state/link_phone_view_model.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
class LinkPhoneScreen extends ConsumerWidget {
class RequestLinkPhoneScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const LinkPhoneScreen({super.key, required this.navigationContract});
const RequestLinkPhoneScreen({super.key, required this.navigationContract});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -56,6 +55,7 @@ class LinkPhoneScreen extends ConsumerWidget {
spacing: 10,
children: [
CountryPrefixPicker(
headerText: context.translate(I18n.selectYourCountry),
initialCountryCode: viewState.dialCode,
onChanged: (country) {
viewModel.updateDialCode(

View File

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:auth/src/features/link_phone/domain/use_cases/link_phone_use_case.dart';
import 'package:auth/src/features/link_phone/presentation/link_phone_view_state.dart';
import 'package:auth/src/features/link_phone/presentation/state/link_phone_view_state.dart';
final linkPhoneViewModelProvider =
NotifierProvider.autoDispose<LinkPhoneViewModel, LinkPhoneViewState>(
@@ -13,6 +13,7 @@ final linkPhoneViewModelProvider =
class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
late final LinkPhoneUseCase _linkPhoneUseCase;
late final TextEditingController phoneNumberController;
late final TextEditingController codeController;
@override
LinkPhoneViewState build() {
@@ -21,6 +22,8 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
phoneNumberController = TextEditingController();
phoneNumberController.addListener(_onPhoneNumberChanged);
codeController = TextEditingController();
ref.onDispose(disposeControllers);
return const LinkPhoneViewState();
@@ -28,18 +31,34 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
void _onPhoneNumberChanged() {
final raw = phoneNumberController.text;
state = state.copyWith(phoneNumber: raw, errorMessage: '');
state = state.copyWith(
phoneNumber: raw,
errorMessage: '',
codeVerified: false,
);
}
void updateDialCode(String dialCode) {
state = state.copyWith(dialCode: dialCode, errorMessage: '');
state = state.copyWith(
dialCode: dialCode,
errorMessage: '',
codeVerified: false,
);
}
void updateCode(String code) {
codeController.text = code;
state = state.copyWith(errorMessage: '', codeVerified: false);
}
Future<void> requestCode() async {
final trimmedNumber = state.phoneNumber.trim();
if (trimmedNumber.isEmpty) {
state = state.copyWith(errorMessage: 'El teléfono no puede estar vacío');
state = state.copyWith(
errorMessage: 'errorMessagePhoneIsEmpty',
codeVerified: false,
);
return;
}
@@ -49,6 +68,7 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
isLoading: true,
errorMessage: '',
codeRequested: false,
codeVerified: false,
);
try {
@@ -67,6 +87,55 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
isLoading: false,
errorMessage: e.toString(),
codeRequested: false,
codeVerified: false,
);
}
}
Future<void> verifyCode() async {
final dialCode = state.dialCode;
final phoneNumber = state.phoneNumber.trim();
final code = codeController.text.trim();
final fullPhone = '$dialCode$phoneNumber';
if (phoneNumber.isEmpty) {
state = state.copyWith(
errorMessage: 'errorMessagePhoneIsEmpty',
codeVerified: false,
);
return;
}
if (code.isEmpty) {
state = state.copyWith(
errorMessage: 'errorMessageCodeIsEmpty',
codeVerified: false,
);
return;
}
state = state.copyWith(
isLoading: true,
errorMessage: '',
codeVerified: false,
);
try {
await _linkPhoneUseCase.verifyCode(phone: fullPhone, code: code);
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorMessage: '',
codeVerified: true,
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorMessage: e.toString(),
codeVerified: false,
);
}
}
@@ -74,5 +143,6 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
void disposeControllers() {
phoneNumberController.removeListener(_onPhoneNumberChanged);
phoneNumberController.dispose();
codeController.dispose();
}
}

View File

@@ -10,5 +10,6 @@ abstract class LinkPhoneViewState with _$LinkPhoneViewState {
@Default('') String errorMessage,
@Default(false) bool isLoading,
@Default(false) bool codeRequested,
@Default(false) bool codeVerified,
}) = _LinkPhoneViewState;
}

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LinkPhoneViewState {
String get phoneNumber; String get dialCode; String get errorMessage; bool get isLoading; bool get codeRequested;
String get phoneNumber; String get dialCode; String get errorMessage; bool get isLoading; bool get codeRequested; bool get codeVerified;
/// Create a copy of LinkPhoneViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $LinkPhoneViewStateCopyWith<LinkPhoneViewState> get copyWith => _$LinkPhoneViewS
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LinkPhoneViewState&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.codeRequested, codeRequested) || other.codeRequested == codeRequested));
return identical(this, other) || (other.runtimeType == runtimeType&&other is LinkPhoneViewState&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.codeRequested, codeRequested) || other.codeRequested == codeRequested)&&(identical(other.codeVerified, codeVerified) || other.codeVerified == codeVerified));
}
@override
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,errorMessage,isLoading,codeRequested);
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,errorMessage,isLoading,codeRequested,codeVerified);
@override
String toString() {
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested)';
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested, codeVerified: $codeVerified)';
}
@@ -45,7 +45,7 @@ abstract mixin class $LinkPhoneViewStateCopyWith<$Res> {
factory $LinkPhoneViewStateCopyWith(LinkPhoneViewState value, $Res Function(LinkPhoneViewState) _then) = _$LinkPhoneViewStateCopyWithImpl;
@useResult
$Res call({
String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested
String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested, bool codeVerified
});
@@ -62,13 +62,14 @@ class _$LinkPhoneViewStateCopyWithImpl<$Res>
/// Create a copy of LinkPhoneViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? phoneNumber = null,Object? dialCode = null,Object? errorMessage = null,Object? isLoading = null,Object? codeRequested = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? phoneNumber = null,Object? dialCode = null,Object? errorMessage = null,Object? isLoading = null,Object? codeRequested = null,Object? codeVerified = null,}) {
return _then(_self.copyWith(
phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // ignore: cast_nullable_to_non_nullable
as String,dialCode: null == dialCode ? _self.dialCode : dialCode // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,codeRequested: null == codeRequested ? _self.codeRequested : codeRequested // ignore: cast_nullable_to_non_nullable
as bool,codeVerified: null == codeVerified ? _self.codeVerified : codeVerified // ignore: cast_nullable_to_non_nullable
as bool,
));
}
@@ -154,10 +155,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested, bool codeVerified)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LinkPhoneViewState() when $default != null:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested,_that.codeVerified);case _:
return orElse();
}
@@ -175,10 +176,10 @@ return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoad
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested, bool codeVerified) $default,) {final _that = this;
switch (_that) {
case _LinkPhoneViewState():
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested,_that.codeVerified);case _:
throw StateError('Unexpected subclass');
}
@@ -195,10 +196,10 @@ return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoad
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested, bool codeVerified)? $default,) {final _that = this;
switch (_that) {
case _LinkPhoneViewState() when $default != null:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested,_that.codeVerified);case _:
return null;
}
@@ -210,7 +211,7 @@ return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoad
class _LinkPhoneViewState implements LinkPhoneViewState {
const _LinkPhoneViewState({this.phoneNumber = '', this.dialCode = '+34', this.errorMessage = '', this.isLoading = false, this.codeRequested = false});
const _LinkPhoneViewState({this.phoneNumber = '', this.dialCode = '+34', this.errorMessage = '', this.isLoading = false, this.codeRequested = false, this.codeVerified = false});
@override@JsonKey() final String phoneNumber;
@@ -218,6 +219,7 @@ class _LinkPhoneViewState implements LinkPhoneViewState {
@override@JsonKey() final String errorMessage;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool codeRequested;
@override@JsonKey() final bool codeVerified;
/// Create a copy of LinkPhoneViewState
/// with the given fields replaced by the non-null parameter values.
@@ -229,16 +231,16 @@ _$LinkPhoneViewStateCopyWith<_LinkPhoneViewState> get copyWith => __$LinkPhoneVi
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LinkPhoneViewState&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.codeRequested, codeRequested) || other.codeRequested == codeRequested));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LinkPhoneViewState&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.codeRequested, codeRequested) || other.codeRequested == codeRequested)&&(identical(other.codeVerified, codeVerified) || other.codeVerified == codeVerified));
}
@override
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,errorMessage,isLoading,codeRequested);
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,errorMessage,isLoading,codeRequested,codeVerified);
@override
String toString() {
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested)';
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested, codeVerified: $codeVerified)';
}
@@ -249,7 +251,7 @@ abstract mixin class _$LinkPhoneViewStateCopyWith<$Res> implements $LinkPhoneVie
factory _$LinkPhoneViewStateCopyWith(_LinkPhoneViewState value, $Res Function(_LinkPhoneViewState) _then) = __$LinkPhoneViewStateCopyWithImpl;
@override @useResult
$Res call({
String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested
String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested, bool codeVerified
});
@@ -266,13 +268,14 @@ class __$LinkPhoneViewStateCopyWithImpl<$Res>
/// Create a copy of LinkPhoneViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? phoneNumber = null,Object? dialCode = null,Object? errorMessage = null,Object? isLoading = null,Object? codeRequested = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? phoneNumber = null,Object? dialCode = null,Object? errorMessage = null,Object? isLoading = null,Object? codeRequested = null,Object? codeVerified = null,}) {
return _then(_LinkPhoneViewState(
phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // ignore: cast_nullable_to_non_nullable
as String,dialCode: null == dialCode ? _self.dialCode : dialCode // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,codeRequested: null == codeRequested ? _self.codeRequested : codeRequested // ignore: cast_nullable_to_non_nullable
as bool,codeVerified: null == codeVerified ? _self.codeVerified : codeVerified // ignore: cast_nullable_to_non_nullable
as bool,
));
}

View File

@@ -1,18 +1,18 @@
import 'package:auth/src/features/login/presentation/phone_code_screen.dart';
import 'package:auth/src/features/link_phone/presentation/verify_code/verify_link_phone_code_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class PhoneCodeBuilder {
const PhoneCodeBuilder();
class VerifyLinkPhoneCodeBuilder {
const VerifyLinkPhoneCodeBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: PhoneCodeScreen(navigationContract: navigationContract),
child: VerifyLinkPhoneCodeScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,118 @@
import 'package:auth/src/features/link_phone/presentation/state/link_phone_view_model.dart';
import 'package:auth/src/features/link_phone/presentation/widgets/link_phone_code_input.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:navigation/navigation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
class VerifyLinkPhoneCodeScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const VerifyLinkPhoneCodeScreen({
super.key,
required this.navigationContract,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewModel = ref.read(linkPhoneViewModelProvider.notifier);
final viewState = ref.watch(linkPhoneViewModelProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 48,
children: [
Column(
children: [
Text(
context.translate(I18n.connect),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30),
),
const SizedBox(height: 24),
Text.rich(
TextSpan(
text: context.translate(I18n.verificationCodeSentTo),
children: [
TextSpan(
text: '${viewState.dialCode}${viewState.phoneNumber}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
Text(
context.translate(I18n.enterCodeHere),
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
LinkPhoneCodeInput(
length: 6,
onCodeChanged: viewModel.updateCode,
),
if (viewState.errorMessage.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
viewState.errorMessage,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color.fromRGBO(239, 17, 17, 1),
fontSize: 12,
),
),
],
],
),
Column(
children: [
PrimaryButton(
onPressed: () async {
await viewModel.verifyCode();
final updatedState = ref.read(linkPhoneViewModelProvider);
if (updatedState.codeVerified) {
navigationContract.pushTo(AppRoutes.login);
}
},
text: context.translate(I18n.enter),
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
const SizedBox(height: 24),
Text(
context.translate(I18n.didNotReceiveIt),
style: TextStyle(
fontSize: 18,
letterSpacing: 0,
height: 1.5,
),
),
const SizedBox(height: 8),
CustomTextButton(
onPressed: () => navigationContract.goBack(),
text: context.translate(I18n.tryAgain),
size: 18,
weight: FontWeight.w500,
),
],
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class LinkPhoneCodeInput extends StatefulWidget {
const LinkPhoneCodeInput({
super.key,
this.length = 6,
required this.onCodeChanged,
});
final int length;
final ValueChanged<String> onCodeChanged;
@override
State<LinkPhoneCodeInput> createState() => _LinkPhoneCodeInputState();
}
class _LinkPhoneCodeInputState extends State<LinkPhoneCodeInput> {
late final List<TextEditingController> _controllers;
late final List<FocusNode> _focusNodes;
@override
void initState() {
super.initState();
_controllers = List<TextEditingController>.generate(
widget.length,
(_) => TextEditingController(),
);
_focusNodes = List<FocusNode>.generate(widget.length, (_) => FocusNode());
}
@override
void dispose() {
for (final controller in _controllers) {
controller.dispose();
}
for (final node in _focusNodes) {
node.dispose();
}
super.dispose();
}
void _onDigitChanged(int index, String value) {
if (value.length > 1) {
final single = value.characters.last;
_controllers[index].text = single;
_controllers[index].selection = TextSelection.fromPosition(
TextPosition(offset: single.length),
);
}
if (value.isNotEmpty && index < widget.length - 1) {
_focusNodes[index + 1].requestFocus();
} else if (value.isEmpty && index > 0) {
_focusNodes[index - 1].requestFocus();
}
final code = _controllers.map((c) => c.text).join();
widget.onCodeChanged(code);
}
@override
Widget build(BuildContext context) {
return Row(
spacing: 8,
children: List<Widget>.generate(widget.length, (int i) {
return Expanded(
child: TextField(
controller: _controllers[i],
focusNode: _focusNodes[i],
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
decoration: const InputDecoration(
hintText: '0',
counterText: '',
border: OutlineInputBorder(),
),
maxLength: 1,
onChanged: (value) => _onDigitChanged(i, value),
),
);
}),
);
}
}

View File

@@ -1,119 +0,0 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:navigation/navigation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PhoneCodeScreen extends ConsumerWidget {
final NavigationContract navigationContract;
PhoneCodeScreen({super.key, required this.navigationContract});
final focusNodes = List<FocusNode>.generate(6, (int i) {
return FocusNode();
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: EdgeInsets.all(30),
child: Expanded(
child: Center(
child: Column(
spacing: 48,
children: [
Spacer(flex: 8),
Column(
spacing: 24,
children: [
Text(
"Conéctate",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 30,
),
),
Text.rich(
TextSpan(
text: "Hemos enviado el código al ",
children: [
TextSpan(
// text: widget.phone,
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
Text("Introduce el código aquí"),
Row(
spacing: 8,
children: List<Widget>.generate(6, (int i) {
return Expanded(
child: TextField(
focusNode: focusNodes[i],
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: "0",
counterText: "",
border: OutlineInputBorder(),
),
maxLength: 1,
onChanged: (String value) => {
value != ""
? focusNodes[i + 1].requestFocus()
: focusNodes[i - 1].requestFocus(),
},
),
);
}),
),
],
),
Column(
spacing: 24,
children: [
PrimaryButton(
onPressed: () => {
navigationContract.pushTo(AppRoutes.login),
},
text: "Entrar",
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
Column(
spacing: 8,
children: [
Text(
"¿No lo has recibido?",
style: TextStyle(
fontSize: 18,
letterSpacing: 0,
height: 1.5,
),
),
CustomTextButton(
onPressed: () => {},
text: "Volver a intentarlo",
size: 18,
weight: FontWeight.w500,
),
],
),
],
),
Spacer(flex: 10),
],
),
),
),
),
);
}
}

View File

@@ -5,6 +5,7 @@ class CountryPrefixPicker extends StatelessWidget {
const CountryPrefixPicker({
super.key,
required this.onChanged,
required this.headerText,
this.initialCountryCode = '+34',
this.radius = 12,
this.width = 90,
@@ -15,7 +16,7 @@ class CountryPrefixPicker extends StatelessWidget {
final ValueChanged<CountryCode> onChanged;
final String initialCountryCode;
final String headerText;
final double radius;
final double width;
final double height;
@@ -28,6 +29,7 @@ class CountryPrefixPicker extends StatelessWidget {
width: width,
height: height,
child: CountryCodePicker(
headerText: headerText,
onChanged: onChanged,
initialSelection: initialCountryCode,
showFlag: false,

View File

@@ -49,6 +49,7 @@ class CustomTextFieldState extends State<CustomTextField> {
),
),
TextFormField(
controller: widget.controller,
keyboardType: widget.numeric
? TextInputType.number
: TextInputType.text,

View File

@@ -0,0 +1,31 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@@ -0,0 +1 @@
{"version":2,"entries":[{"package":"fonts","rootUri":"../","packageUri":"lib/"}]}

View File

@@ -2,8 +2,8 @@ class AppRoutes {
static const login = '/login';
static const signup = '/signup';
static const onboarding = '/onboarding';
static const linkPhone = '/link_phone';
static const phoneCode = '/phone_code';
static const linkPhone = '/request_link_phone';
static const phoneCode = '/verify_link_phone_code';
static const deviceSignup = '/device_signup';
static const recoverPassword = '/recover_password';

View File

@@ -15,6 +15,7 @@ Future<void> configureDependencies(EnvConfig env, {bool log = false}) async {
getIt.registerLazySingleton<Dio>(
() => buildDioClient(
baseUrl: env.apiBaseUrl,
origin: env.apiOrigin,
// apiKey: env.apiKey,
log: log,
),

View File

@@ -1,4 +1,5 @@
abstract class EnvConfig {
String get apiBaseUrl;
String get apiOrigin;
// String get apiKey;
}

View File

@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
Dio buildDioClient({
required String baseUrl,
required String origin,
// required String apiKey,
bool log = false,
}) {
@@ -15,6 +16,7 @@ Dio buildDioClient({
// if (apiKey.isNotEmpty) 'x-api-key': apiKey,
'accept': 'application/json',
'content-type': 'application/json',
'origin': origin,
},
),
);

View File

@@ -12,5 +12,13 @@
"linkPhoneTitle": "Wir freuen uns sehr, dass du hier bist!",
"linkPhoneSubtitle": "Um dich sicher anzumelden, senden wir dir einen Code an deine Telefonnummer",
"mobilePhone": "Mobiltelefon",
"phoneNumber": "Telefonnummer"
"phoneNumber": "Telefonnummer",
"selectYourCountry": "Wähle dein Land",
"errorMessagePhoneIsEmpty": "Die Telefonnummer darf nicht leer sein",
"connect": "Verbinden",
"verificationCodeSentTo": "Wir haben den Code an ",
"enterCodeHere": "Gib den Code hier ein",
"enter": "Weiter",
"didNotReceiveIt": "Hast du es nicht erhalten?",
"tryAgain": "Erneut versuchen"
}

View File

@@ -12,5 +12,13 @@
"linkPhoneTitle": "We're really happy to have you here!",
"linkPhoneSubtitle": "To sign in securely, we'll send a code to your phone number",
"mobilePhone": "Mobile phone",
"phoneNumber": "Phone number"
"phoneNumber": "Phone number",
"selectYourCountry": "Select your country",
"errorMessagePhoneIsEmpty": "Phone number cannot be empty",
"connect": "Connect",
"verificationCodeSentTo": "We have sent the code to ",
"enterCodeHere": "Enter the code here",
"enter": "Enter",
"didNotReceiveIt": "Didn't receive it?",
"tryAgain": "Try again"
}

View File

@@ -12,5 +12,13 @@
"linkPhoneTitle": "¡Nos alegra mucho tenerte por aquí!",
"linkPhoneSubtitle": "Para poder entrar de forma segura, te vamos a enviar un código al teléfono",
"mobilePhone": "Teléfono móvil",
"phoneNumber": "Teléfono"
"phoneNumber": "Teléfono",
"selectYourCountry": "Selecciona tu país",
"errorMessagePhoneIsEmpty": "El número de teléfono no puede estar vacío",
"connect": "Conéctate",
"verificationCodeSentTo": "Hemos enviado el código al ",
"enterCodeHere": "Introduce el código aquí",
"enter": "enter",
"didNotReceiveIt": "¿No lo has recibido?",
"tryAgain": "Volver a intentarlo"
}

View File

@@ -12,5 +12,13 @@
"linkPhoneTitle": "Nous sommes ravis de te compter parmi nous !",
"linkPhoneSubtitle": "Pour te connecter en toute sécurité, nous allons envoyer un code sur ton téléphone",
"mobilePhone": "Téléphone portable",
"phoneNumber": "Numéro de téléphone"
"phoneNumber": "Numéro de téléphone",
"selectYourCountry": "Sélectionne ton pays",
"errorMessagePhoneIsEmpty": "Le numéro de téléphone ne peut pas être vide",
"connect": "Connecter",
"verificationCodeSentTo": "Nous avons envoyé le code à ",
"enterCodeHere": "Saisissez le code ici",
"enter": "Entrer",
"didNotReceiveIt": "Tu ne l'as pas reçu ?",
"tryAgain": "Réessayer"
}

View File

@@ -12,5 +12,13 @@
"linkPhoneTitle": "Siamo molto felici di averti qui!",
"linkPhoneSubtitle": "Per accedere in modo sicuro, ti invieremo un codice al tuo telefono",
"mobilePhone": "Telefono cellulare",
"phoneNumber": "Numero di telefono"
"phoneNumber": "Numero di telefono",
"selectYourCountry": "Seleziona il tuo paese",
"errorMessagePhoneIsEmpty": "Il numero di telefono non può essere vuoto",
"connect": "Collegare",
"verificationCodeSentTo": "Abbiamo inviato il codice a ",
"enterCodeHere": "Inserisci il codice qui",
"enter": "Entra",
"didNotReceiveIt": "Non lo hai ricevuto?",
"tryAgain": "Riprova"
}

View File

@@ -12,5 +12,13 @@
"linkPhoneTitle": "Ficamos muito felizes em ter você aqui!",
"linkPhoneSubtitle": "Para entrar com segurança, vamos enviar um código para o seu telefone",
"mobilePhone": "Telefone celular",
"phoneNumber": "Número de telefone"
"phoneNumber": "Número de telefone",
"selectYourCountry": "Selecione seu país",
"errorMessagePhoneIsEmpty": "O número de telefone não pode estar vazio",
"connect": "Conectar",
"verificationCodeSentTo": "Enviamos o código para ",
"enterCodeHere": "Insira o código aqui",
"enter": "Entrar",
"didNotReceiveIt": "Você não recebeu?",
"tryAgain": "Tentar novamente"
}

View File

@@ -17,4 +17,12 @@ class I18n {
static const String linkPhoneSubtitle = 'linkPhoneSubtitle';
static const String mobilePhone = 'mobilePhone';
static const String phoneNumber = 'phoneNumber';
static const String selectYourCountry = 'selectYourCountry';
static const String errorMessagePhoneIsEmpty = 'errorMessagePhoneIsEmpty';
static const String connect = "connect";
static const String verificationCodeSentTo = "verificationCodeSentTo";
static const String enterCodeHere = "enterCodeHere";
static const String enter = "enter";
static const String didNotReceiveIt = "didNotReceiveIt";
static const String tryAgain = "tryAgain";
}