From 1ca1a451263d25adfdf7d0c8fa1ceccaa86539de Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 18 Sep 2025 08:01:49 +0700 Subject: [PATCH] check phone impl --- .../check_phone_form_bloc.dart | 3 +- lib/common/extension/extension.dart | 2 +- lib/common/function/app_function.dart | 7 + .../datasources/remote_data_provider.dart | 7 + lib/injection.config.dart | 20 +-- .../components/button/button.dart | 1 + .../components/button/elevated_button.dart | 32 +++- .../components/field/text_form_field.dart | 3 + .../components/toast/flushbar.dart | 54 ++++++ .../pages/auth/login/login_page.dart | 159 ++++++++++++------ .../pages/auth/login/widgets/phone_field.dart | 13 +- .../pages/auth/password/password_page.dart | 5 +- lib/presentation/router/app_router.gr.dart | 31 +++- pubspec.lock | 16 ++ pubspec.yaml | 2 + 15 files changed, 275 insertions(+), 80 deletions(-) create mode 100644 lib/presentation/components/toast/flushbar.dart diff --git a/lib/application/auth/check_phone_form/check_phone_form_bloc.dart b/lib/application/auth/check_phone_form/check_phone_form_bloc.dart index 8dab0e8..e559b46 100644 --- a/lib/application/auth/check_phone_form/check_phone_form_bloc.dart +++ b/lib/application/auth/check_phone_form/check_phone_form_bloc.dart @@ -3,6 +3,7 @@ import 'package:dartz/dartz.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; +import '../../../common/function/app_function.dart'; import '../../../domain/auth/auth.dart'; part 'check_phone_form_event.dart'; @@ -40,7 +41,7 @@ class CheckPhoneFormBloc if (phoneNumberValid) { failureOrCheckPhone = await _repository.checkPhone( - phoneNumber: state.phoneNumber, + phoneNumber: getNormalizePhone(state.phoneNumber), ); emit( state.copyWith( diff --git a/lib/common/extension/extension.dart b/lib/common/extension/extension.dart index 922c90a..a950fef 100644 --- a/lib/common/extension/extension.dart +++ b/lib/common/extension/extension.dart @@ -7,7 +7,7 @@ part 'date_extension.dart'; extension StringExt on String { CheckPhoneStatus toCheckPhoneStatus() { switch (this) { - case 'NO_REGISTERED': + case 'NOT_REGISTERED': return CheckPhoneStatus.notRegistered; case 'PASSWORD_REQUIRED': return CheckPhoneStatus.passwordRequired; diff --git a/lib/common/function/app_function.dart b/lib/common/function/app_function.dart index ddffdda..3b11f3d 100644 --- a/lib/common/function/app_function.dart +++ b/lib/common/function/app_function.dart @@ -6,3 +6,10 @@ void dismissKeyboard(BuildContext context) { FocusManager.instance.primaryFocus?.unfocus(); } } + +String getNormalizePhone(String phoneNumber) { + final normalizedPhone = phoneNumber.startsWith('08') + ? phoneNumber.replaceFirst('0', '') + : phoneNumber; + return '62$normalizedPhone'; +} diff --git a/lib/infrastructure/auth/datasources/remote_data_provider.dart b/lib/infrastructure/auth/datasources/remote_data_provider.dart index 259d96a..ffa8d6c 100644 --- a/lib/infrastructure/auth/datasources/remote_data_provider.dart +++ b/lib/infrastructure/auth/datasources/remote_data_provider.dart @@ -41,6 +41,13 @@ class AuthRemoteDataProvider { AuthFailure.dynamicErrorMessage('No. Telepon Tidak Boleh Kosong'), ); } + if (response.data['errors'][0]['code'] == 304) { + return DC.error( + AuthFailure.dynamicErrorMessage( + response.data['errors'][0]['cause'], + ), + ); + } } } diff --git a/lib/injection.config.dart b/lib/injection.config.dart index cfa831f..3f17f41 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -78,20 +78,20 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i510.LoginFormBloc>( () => _i510.LoginFormBloc(gh<_i995.IAuthRepository>()), ); - gh.factory<_i869.CheckPhoneFormBloc>( - () => _i869.CheckPhoneFormBloc(gh<_i995.IAuthRepository>()), - ); - gh.factory<_i260.RegisterFormBloc>( - () => _i260.RegisterFormBloc(gh<_i995.IAuthRepository>()), - ); - gh.factory<_i521.VerifyFormBloc>( - () => _i521.VerifyFormBloc(gh<_i995.IAuthRepository>()), + gh.factory<_i627.ResendFormBloc>( + () => _i627.ResendFormBloc(gh<_i995.IAuthRepository>()), ); gh.factory<_i174.SetPasswordFormBloc>( () => _i174.SetPasswordFormBloc(gh<_i995.IAuthRepository>()), ); - gh.factory<_i627.ResendFormBloc>( - () => _i627.ResendFormBloc(gh<_i995.IAuthRepository>()), + gh.factory<_i260.RegisterFormBloc>( + () => _i260.RegisterFormBloc(gh<_i995.IAuthRepository>()), + ); + gh.factory<_i869.CheckPhoneFormBloc>( + () => _i869.CheckPhoneFormBloc(gh<_i995.IAuthRepository>()), + ); + gh.factory<_i521.VerifyFormBloc>( + () => _i521.VerifyFormBloc(gh<_i995.IAuthRepository>()), ); return this; } diff --git a/lib/presentation/components/button/button.dart b/lib/presentation/components/button/button.dart index 739165b..2cdbda2 100644 --- a/lib/presentation/components/button/button.dart +++ b/lib/presentation/components/button/button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; import '../../../common/theme/theme.dart'; diff --git a/lib/presentation/components/button/elevated_button.dart b/lib/presentation/components/button/elevated_button.dart index 40ca12d..917fed1 100644 --- a/lib/presentation/components/button/elevated_button.dart +++ b/lib/presentation/components/button/elevated_button.dart @@ -7,12 +7,14 @@ class AppElevatedButton extends StatelessWidget { required this.title, this.width = double.infinity, this.height = 48.0, + this.isLoading = false, }); final Function()? onPressed; final String title; final double width; final double height; + final bool isLoading; @override Widget build(BuildContext context) { @@ -21,13 +23,29 @@ class AppElevatedButton extends StatelessWidget { height: height, child: ElevatedButton( onPressed: onPressed, - child: Text( - title, - style: AppStyle.lg.copyWith( - color: AppColor.white, - fontWeight: FontWeight.w700, - ), - ), + child: isLoading + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SpinKitFadingCircle(color: AppColor.white, size: 24), + SizedBox(width: 8), + Text( + 'Loading', + style: AppStyle.lg.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w700, + ), + ), + ], + ) + : Text( + title, + style: AppStyle.lg.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w700, + ), + ), ), ); } diff --git a/lib/presentation/components/field/text_form_field.dart b/lib/presentation/components/field/text_form_field.dart index 17328b4..dafa5e2 100644 --- a/lib/presentation/components/field/text_form_field.dart +++ b/lib/presentation/components/field/text_form_field.dart @@ -11,6 +11,7 @@ class AppTextFormField extends StatelessWidget { this.suffixIcon, this.keyboardType, this.onChanged, + this.validator, }); final String? hintText; @@ -21,6 +22,7 @@ class AppTextFormField extends StatelessWidget { final Widget? suffixIcon; final TextInputType? keyboardType; final ValueChanged? onChanged; + final String? Function(String?)? validator; @override Widget build(BuildContext context) { @@ -39,6 +41,7 @@ class AppTextFormField extends StatelessWidget { color: AppColor.textPrimary, fontWeight: FontWeight.w500, ), + validator: validator, decoration: InputDecoration( hintText: hintText, prefixIcon: prefixIcon, diff --git a/lib/presentation/components/toast/flushbar.dart b/lib/presentation/components/toast/flushbar.dart new file mode 100644 index 0000000..845967c --- /dev/null +++ b/lib/presentation/components/toast/flushbar.dart @@ -0,0 +1,54 @@ +import 'package:another_flushbar/flushbar.dart'; +import 'package:flutter/material.dart'; + +import '../../../common/theme/theme.dart'; +import '../../../domain/auth/auth.dart'; + +class AppFlushbar { + static void showSuccess(BuildContext context, String message) { + Flushbar( + messageText: Text( + message, + style: AppStyle.lg.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + icon: const Icon(Icons.check_circle, color: Colors.white), + duration: const Duration(seconds: 2), + flushbarPosition: FlushbarPosition.BOTTOM, + backgroundColor: AppColor.secondary, + borderRadius: BorderRadius.circular(12), + margin: const EdgeInsets.all(12), + ).show(context); + } + + static void showError(BuildContext context, String message) { + Flushbar( + messageText: Text( + message, + style: AppStyle.lg.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + icon: const Icon(Icons.error, color: Colors.white), + duration: const Duration(seconds: 3), + flushbarPosition: FlushbarPosition.BOTTOM, + backgroundColor: AppColor.error, + borderRadius: BorderRadius.circular(12), + margin: const EdgeInsets.all(12), + ).show(context); + } + + static void showAuthFailureToast(BuildContext context, AuthFailure failure) => + showError( + context, + failure.map( + serverError: (value) => value.failure.toStringFormatted(context), + dynamicErrorMessage: (value) => value.erroMessage, + unexpectedError: (value) => 'Terjadi kesalahan, silahkan coba lagi', + ), + ); +} diff --git a/lib/presentation/pages/auth/login/login_page.dart b/lib/presentation/pages/auth/login/login_page.dart index f50ce9d..ce827f2 100644 --- a/lib/presentation/pages/auth/login/login_page.dart +++ b/lib/presentation/pages/auth/login/login_page.dart @@ -1,78 +1,131 @@ +import 'dart:developer'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../application/auth/check_phone_form/check_phone_form_bloc.dart'; import '../../../../common/theme/theme.dart'; +import '../../../../domain/auth/auth.dart'; +import '../../../../injection.dart'; import '../../../components/button/button.dart'; +import '../../../components/toast/flushbar.dart'; import '../../../router/app_router.gr.dart'; import 'widgets/phone_field.dart'; @RoutePage() -class LoginPage extends StatelessWidget { +class LoginPage extends StatelessWidget implements AutoRouteWrapper { const LoginPage({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Masuk')), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 40), - - // Title - LoginPhoneField(), - - const SizedBox(height: 50), - - // Continue Button - AppElevatedButton( - onPressed: () { - context.router.push(RegisterRoute()); - }, - title: 'Lanjutkan', - ), - - const SizedBox(height: 24), - - // Terms and Conditions - Center( - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: AppStyle.md.copyWith( - color: AppColor.textSecondary, - height: 1.4, - ), + return BlocListener( + listenWhen: (p, c) => + p.failureOrCheckPhoneOption != c.failureOrCheckPhoneOption, + listener: (context, state) { + state.failureOrCheckPhoneOption.fold( + () => null, + (either) => either.fold( + (f) => AppFlushbar.showAuthFailureToast(context, f), + (data) { + AppFlushbar.showSuccess(context, data.message); + Future.delayed(Duration(milliseconds: 1000), () { + log(data.toString()); + if (data.status.isNotRegistered) { + context.router.push(RegisterRoute()); + } else if (data.status.isPasswordRequired) { + context.router.push( + PasswordRoute(phoneNumber: data.phoneNumber), + ); + } + }); + }, + ), + ); + }, + child: Scaffold( + appBar: AppBar(title: const Text('Masuk')), + body: BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Form( + autovalidateMode: state.showErrorMessages + ? AutovalidateMode.always + : AutovalidateMode.disabled, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const TextSpan( - text: 'Dengan masuk Enaklo, kamu telah\nmenyetujui ', + const SizedBox(height: 40), + + // Title + LoginPhoneField(), + + const SizedBox(height: 50), + + // Continue Button + AppElevatedButton( + onPressed: state.isSubmitting + ? null + : () { + context.read().add( + CheckPhoneFormEvent.submitted(), + ); + }, + isLoading: state.isSubmitting, + title: 'Lanjutkan', ), - TextSpan( - text: 'Syarat & Ketentuan', - style: AppStyle.md.copyWith( - color: AppColor.primary, - fontWeight: FontWeight.w600, - ), - ), - const TextSpan(text: ' dan\n'), - TextSpan( - text: 'Kebijakan Privasi', - style: AppStyle.md.copyWith( - color: AppColor.primary, - fontWeight: FontWeight.w600, + + const SizedBox(height: 24), + + // Terms and Conditions + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: AppStyle.md.copyWith( + color: AppColor.textSecondary, + height: 1.4, + ), + children: [ + const TextSpan( + text: + 'Dengan masuk Enaklo, kamu telah\nmenyetujui ', + ), + TextSpan( + text: 'Syarat & Ketentuan', + style: AppStyle.md.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + ), + const TextSpan(text: ' dan\n'), + TextSpan( + text: 'Kebijakan Privasi', + style: AppStyle.md.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), ), ), + + const SizedBox(height: 40), ], ), ), - ), - - const SizedBox(height: 40), - ], + ); + }, ), ), ); } + + @override + Widget wrappedRoute(BuildContext context) => BlocProvider( + create: (context) => getIt(), + child: this, + ); } diff --git a/lib/presentation/pages/auth/login/widgets/phone_field.dart b/lib/presentation/pages/auth/login/widgets/phone_field.dart index b649983..7caa40b 100644 --- a/lib/presentation/pages/auth/login/widgets/phone_field.dart +++ b/lib/presentation/pages/auth/login/widgets/phone_field.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../../application/auth/check_phone_form/check_phone_form_bloc.dart'; import '../../../../../common/theme/theme.dart'; import '../../../../components/field/field.dart'; @@ -45,6 +47,13 @@ class _LoginPhoneFieldState extends State { controller: _controller, focusNode: _focusNode, keyboardType: TextInputType.phone, + validator: (value) { + if (context.read().state.phoneNumber.isEmpty) { + return 'Masukkan no telepon'; + } + + return null; + }, prefixIcon: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( @@ -64,7 +73,9 @@ class _LoginPhoneFieldState extends State { ) : null, onChanged: (value) { - setState(() {}); + context.read().add( + CheckPhoneFormEvent.phoneNumberChanged(value), + ); }, ); } diff --git a/lib/presentation/pages/auth/password/password_page.dart b/lib/presentation/pages/auth/password/password_page.dart index 820212b..cfed788 100644 --- a/lib/presentation/pages/auth/password/password_page.dart +++ b/lib/presentation/pages/auth/password/password_page.dart @@ -7,12 +7,13 @@ import '../../../router/app_router.gr.dart'; @RoutePage() class PasswordPage extends StatelessWidget { - const PasswordPage({super.key}); + final String phoneNumber; + const PasswordPage({super.key, required this.phoneNumber}); @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Buat Kata Sandi')), + appBar: AppBar(title: const Text('Kata Sandi')), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Column( diff --git a/lib/presentation/router/app_router.gr.dart b/lib/presentation/router/app_router.gr.dart index dd0ae48..24083cd 100644 --- a/lib/presentation/router/app_router.gr.dart +++ b/lib/presentation/router/app_router.gr.dart @@ -255,7 +255,7 @@ class LoginRoute extends _i33.PageRouteInfo { static _i33.PageInfo page = _i33.PageInfo( name, builder: (data) { - return const _i12.LoginPage(); + return _i33.WrappedRoute(child: const _i12.LoginPage()); }, ); } @@ -432,20 +432,41 @@ class OtpRoute extends _i33.PageRouteInfo { /// generated route for /// [_i21.PasswordPage] -class PasswordRoute extends _i33.PageRouteInfo { - const PasswordRoute({List<_i33.PageRouteInfo>? children}) - : super(PasswordRoute.name, initialChildren: children); +class PasswordRoute extends _i33.PageRouteInfo { + PasswordRoute({ + _i34.Key? key, + required String phoneNumber, + List<_i33.PageRouteInfo>? children, + }) : super( + PasswordRoute.name, + args: PasswordRouteArgs(key: key, phoneNumber: phoneNumber), + initialChildren: children, + ); static const String name = 'PasswordRoute'; static _i33.PageInfo page = _i33.PageInfo( name, builder: (data) { - return const _i21.PasswordPage(); + final args = data.argsAs(); + return _i21.PasswordPage(key: args.key, phoneNumber: args.phoneNumber); }, ); } +class PasswordRouteArgs { + const PasswordRouteArgs({this.key, required this.phoneNumber}); + + final _i34.Key? key; + + final String phoneNumber; + + @override + String toString() { + return 'PasswordRouteArgs{key: $key, phoneNumber: $phoneNumber}'; + } +} + /// generated route for /// [_i22.PaymentPage] class PaymentRoute extends _i33.PageRouteInfo { diff --git a/pubspec.lock b/pubspec.lock index 03bd051..7ee8410 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.7.1" + another_flushbar: + dependency: "direct main" + description: + name: another_flushbar + sha256: "2b99671c010a7d5770acf5cb24c9f508b919c3a7948b6af9646e773e7da7b757" + url: "https://pub.dev" + source: hosted + version: "1.12.32" archive: dependency: transitive description: @@ -478,6 +486,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_spinkit: + dependency: "direct main" + description: + name: flutter_spinkit + sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f" + url: "https://pub.dev" + source: hosted + version: "5.2.2" flutter_svg: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 9b191b6..7afb99d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,8 @@ dependencies: audioplayers: ^6.5.1 flutter_bloc: ^9.1.1 bloc: ^9.0.0 + another_flushbar: ^1.12.32 + flutter_spinkit: ^5.2.2 dev_dependencies: flutter_test: