diff --git a/lib/core/routes/route_name.dart b/lib/core/routes/route_name.dart index bfa8175..13b4df3 100644 --- a/lib/core/routes/route_name.dart +++ b/lib/core/routes/route_name.dart @@ -37,4 +37,8 @@ class RouteName { //Biometric static const String otpScreen = 'otpScreen'; + + //Pin Screen + static const String pinScreen = 'pinScreen'; + static const String confirmPinScreen = 'confirmPinScreen'; } diff --git a/lib/core/routes/routes.dart b/lib/core/routes/routes.dart index 452779c..0fbd531 100644 --- a/lib/core/routes/routes.dart +++ b/lib/core/routes/routes.dart @@ -13,10 +13,12 @@ import 'package:tanami_app/features/countrySelection/presentation/pages/choose_c import 'package:tanami_app/features/otpVerification/presentation/pages/otp_screen.dart'; import 'package:tanami_app/features/register/presentation/pages/register_screen.dart'; import 'package:tanami_app/features/register/presentation/pages/register_step_screen.dart'; +import 'package:tanami_app/features/securePin/presentation/pages/pin_screen.dart'; import 'package:tanami_app/features/welcome/presentation/pages/weclome_screen.dart'; import '../../features/login/presentation/pages/login_screen.dart'; import '../../features/register/presentation/pages/register_user_details_screen.dart'; +import '../../features/securePin/presentation/pages/confirm_pin_screen.dart'; import '../../features/splash/presentation/pages/splash_screen.dart'; /* CREATED BY - JAYESH JAIN @@ -88,6 +90,13 @@ final goRouter = GoRouter( return const RegisterScreen(); }, ), + GoRoute( + name: RouteName.registerUserDetailsScreen, + path: RouteName.registerUserDetailsScreen, + builder: (context, state) { + return const RegisterUserDetailsScreen(); + }, + ), GoRoute( name: RouteName.otpScreen, path: RouteName.otpScreen, @@ -109,6 +118,22 @@ final goRouter = GoRouter( return const AcademyDetailsScreen(); }, ), + GoRoute( + name: RouteName.pinScreen, + path: "${RouteName.pinScreen}/:fromScreen", + builder: (context, state) { + return PinScreen( + fromScreen: state.pathParameters["fromScreen"]!, + ); + }, + ), + GoRoute( + name: RouteName.confirmPinScreen, + path: RouteName.confirmPinScreen, + builder: (context, state) { + return const ConfirmPinScreen(); + }, + ), ], ), // GoRoute( diff --git a/lib/core/styles/app_color.dart b/lib/core/styles/app_color.dart index 22e7280..3a7cd4a 100644 --- a/lib/core/styles/app_color.dart +++ b/lib/core/styles/app_color.dart @@ -44,4 +44,12 @@ class AppColor { //CheckBox Color static const Color checkBoxActiveColor = Color(0xFF09622E); + + //Selectable Text Color + static const Color selectableTextColor = Color(0xFF015BA2); + + //Pin Code Color + static const Color pinFillColor = Color(0xFFC9D9CB); + static const Color pinFillBorderColor = Color(0xFF648774); + static const Color pinInActiveBorderColor = Color(0xFFDFDFE3); } diff --git a/lib/core/styles/app_text.dart b/lib/core/styles/app_text.dart index 7b8de3b..4c1ffb6 100644 --- a/lib/core/styles/app_text.dart +++ b/lib/core/styles/app_text.dart @@ -61,6 +61,10 @@ class AppText { static const String enterEmail = "Enter Email"; static const String cantBeEmptyText = "Can't Be Empty"; static const String passwordMismatch = "Password Mismatch"; + static const String iAgreeToThe = "I agree to the "; + static const String termsAndCondition = "Terms & Conditions"; + static const String andThe = " and the"; + static const String privacyPolicy = "Privacy Policy"; //Country Name static const String bahrainCountryText = "Bahrain"; @@ -98,4 +102,20 @@ class AppText { static const String resendSms = "Resend SMS"; static const String otpVerifiedSucessfully = "OTP Verified Successfully!"; static const String otpVerifiedFailed = "OTP Verification Failed:"; + + //Pin Code + static const String pinCode = "Pin Code"; + static const String createPinCode = "Create Pin Code"; + static const String confirmPinCode = "Confirm Pin Code"; + static const String incorrectPinCode = "Incorrect PIN. Please try again."; + static const String welcomeBackText = "Welcome back"; + static const String userYourAppPinToLoginEnterTanami = + "Use your app PIN to login to enter Tanami"; + static const String forgotPinCode = "Forgot Pin Code"; + + static const String notificationText = "Notification"; + static const String toRestorePinYouWillBeLoggedOut = + "To restore PIN you will be Logged out"; + static const String allowText = "Allow"; + static const String declineText = "Decline"; } diff --git a/lib/core/utils/secure/secure_storage_service.dart b/lib/core/utils/secure/secure_storage_service.dart new file mode 100644 index 0000000..e0dc670 --- /dev/null +++ b/lib/core/utils/secure/secure_storage_service.dart @@ -0,0 +1,21 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class SecureStorageService { + final FlutterSecureStorage _storage = const FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + ), + ); + + Future write(String key, String value) async { + await _storage.write(key: key, value: value); + } + + Future read(String key) async { + return await _storage.read(key: key); + } + + Future delete(String key) async { + await _storage.delete(key: key); + } +} diff --git a/lib/features/MainScreens/Academy/presentation/pages/academyScreen.dart b/lib/features/MainScreens/Academy/presentation/pages/academyScreen.dart index 24fb96b..ad343c5 100644 --- a/lib/features/MainScreens/Academy/presentation/pages/academyScreen.dart +++ b/lib/features/MainScreens/Academy/presentation/pages/academyScreen.dart @@ -1,7 +1,4 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:tanami_app/core/routes/route_name.dart'; @@ -93,7 +90,7 @@ class _AcademyScreenState extends State { title: Text( 'Academy', style: GoogleFonts.dmSans( - color: Color(0xFF272727), + color: const Color(0xFF272727), fontSize: 22.sp, fontWeight: FontWeight.w700, ), diff --git a/lib/features/MainScreens/Invest/presentation/pages/investScreen.dart b/lib/features/MainScreens/Invest/presentation/pages/investScreen.dart index 3d5570e..36d8b2a 100644 --- a/lib/features/MainScreens/Invest/presentation/pages/investScreen.dart +++ b/lib/features/MainScreens/Invest/presentation/pages/investScreen.dart @@ -10,6 +10,8 @@ class InvestScreen extends StatefulWidget { class _InvestScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold(body: Text('Invest'),); + return const Scaffold( + body: Text('Invest'), + ); } -} \ No newline at end of file +} diff --git a/lib/features/MainScreens/MainScreen.dart b/lib/features/MainScreens/MainScreen.dart index 8e8a26e..14b577f 100644 --- a/lib/features/MainScreens/MainScreen.dart +++ b/lib/features/MainScreens/MainScreen.dart @@ -6,6 +6,8 @@ import 'package:tanami_app/features/MainScreens/Settings/presentation/pages/sett import 'package:tanami_app/features/MainScreens/Wallet/presentation/pages/walletScreen.dart'; import 'package:tanami_app/shared/components/common_bottom_navigation.dart'; +import '../../shared/components/exit_app_dialog.dart'; + var currentTab = [ const WalletScreen(), const PortfolioScreen(), @@ -24,10 +26,16 @@ class MainScreen extends StatelessWidget { const MainScreen({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: currentTab[selectedIndex], - bottomNavigationBar: bottomnavigationbar(selectedIndex), + return WillPopScope( + onWillPop: () async { + exitAppDialog(context); + return false; + }, + child: Scaffold( + backgroundColor: Colors.white, + body: currentTab[selectedIndex], + bottomNavigationBar: bottomnavigationbar(selectedIndex), + ), ); } } diff --git a/lib/features/MainScreens/Portfolio/presentation/pages/detailsScreen.dart b/lib/features/MainScreens/Portfolio/presentation/pages/detailsScreen.dart index ea2de62..db54ad3 100644 --- a/lib/features/MainScreens/Portfolio/presentation/pages/detailsScreen.dart +++ b/lib/features/MainScreens/Portfolio/presentation/pages/detailsScreen.dart @@ -37,13 +37,13 @@ class _DetailsScreenState extends State { Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(20.0)), + borderRadius: const BorderRadius.all(Radius.circular(20.0)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), spreadRadius: 2, blurRadius: 10, - offset: Offset(0, 3), // changes position of shadow + offset: const Offset(0, 3), // changes position of shadow ), ], ), @@ -230,7 +230,7 @@ class _DetailsScreenState extends State { child: Text( AppText.investmentamount, style: GoogleFonts.dmSans( - color: Color(0xFF535353), + color: const Color(0xFF535353), fontSize: 14.sp, fontWeight: FontWeight.w500, ), @@ -271,7 +271,7 @@ class _DetailsScreenState extends State { child: Text( AppText.currentval, style: GoogleFonts.dmSans( - color: Color(0xFF535353), + color: const Color(0xFF535353), fontSize: 14.sp, fontWeight: FontWeight.w500, ), @@ -312,7 +312,7 @@ class _DetailsScreenState extends State { child: Text( AppText.disttodate, style: GoogleFonts.dmSans( - color: Color(0xFF535353), + color: const Color(0xFF535353), fontSize: 14.sp, fontWeight: FontWeight.w500, ), @@ -338,7 +338,7 @@ class _DetailsScreenState extends State { Text( AppText.totalreturn, style: GoogleFonts.dmSans( - color: Color(0xFF535353), + color: const Color(0xFF535353), fontSize: 14.sp, fontWeight: FontWeight.w500, ), @@ -346,7 +346,7 @@ class _DetailsScreenState extends State { Text( '+ 20.0%', style: GoogleFonts.dmSans( - color: Color(0xFF066123), + color: const Color(0xFF066123), fontSize: 14.sp, fontWeight: FontWeight.w700, ), @@ -373,7 +373,7 @@ class _DetailsScreenState extends State { color: Colors.black.withOpacity(0.15), spreadRadius: 2, blurRadius: 10, - offset: Offset(0, 3), // changes position of shadow + offset: const Offset(0, 3), // changes position of shadow ), ], ), @@ -404,12 +404,12 @@ class _DetailsScreenState extends State { (index) { return Center( child: Container( - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Color(0xFFF4F4F4), borderRadius: BorderRadius.all(Radius.circular(10.0)), ), - padding: EdgeInsets.all(20.0), + padding: const EdgeInsets.all(20.0), child: Column( children: [ Row( @@ -443,7 +443,7 @@ class _DetailsScreenState extends State { Text( "512 Mb", style: GoogleFonts.dmSans( - color: Color(0xFF535353), + color: const Color(0xFF535353), fontSize: 12.sp, fontWeight: FontWeight.w700, ), diff --git a/lib/features/MainScreens/Portfolio/presentation/pages/portfolioScreen.dart b/lib/features/MainScreens/Portfolio/presentation/pages/portfolioScreen.dart index bd9773d..3f2aebb 100644 --- a/lib/features/MainScreens/Portfolio/presentation/pages/portfolioScreen.dart +++ b/lib/features/MainScreens/Portfolio/presentation/pages/portfolioScreen.dart @@ -36,7 +36,8 @@ class _PortfolioScreenState extends State { var opacity = (1 - percentage).clamp(0.0, 1.0); return FlexibleSpaceBar( // centerTitle: true, - titlePadding: EdgeInsets.only(left: 30, top: 0, bottom: 15), + titlePadding: + const EdgeInsets.only(left: 30, top: 0, bottom: 15), title: Opacity( opacity: opacity, child: Row( @@ -44,7 +45,7 @@ class _PortfolioScreenState extends State { Text( AppText.portfolio, style: GoogleFonts.dmSans( - color: Color(0xFF888888), + color: const Color(0xFF888888), fontSize: 12.sp, fontWeight: FontWeight.w700, ), @@ -78,7 +79,7 @@ class _PortfolioScreenState extends State { Text( AppText.portfolio, style: GoogleFonts.dmSans( - color: Color(0xFFC9D9CB), + color: const Color(0xFFC9D9CB), fontSize: 14.sp, fontWeight: FontWeight.w700, ), @@ -115,13 +116,16 @@ class _PortfolioScreenState extends State { child: Container( decoration: BoxDecoration( color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(20.0)), + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), spreadRadius: 2, blurRadius: 10, - offset: Offset(0, 3), // changes position of shadow + offset: + const Offset(0, 3), // changes position of shadow ), ], ), @@ -152,7 +156,7 @@ class _PortfolioScreenState extends State { Text( 'Mar 01 2024', style: GoogleFonts.dmSans( - color: Color(0xFF004717), + color: const Color(0xFF004717), fontSize: 12.sp, fontWeight: FontWeight.w500, ), @@ -163,8 +167,8 @@ class _PortfolioScreenState extends State { (index == 2) ? 'Exited' : 'Pending', style: GoogleFonts.dmSans( color: (index == 2) - ? Color(0xFF8D8D8D) - : Color(0xFF0FA4A4), + ? const Color(0xFF8D8D8D) + : const Color(0xFF0FA4A4), fontSize: 14.sp, fontWeight: FontWeight.w700, ), @@ -209,8 +213,8 @@ class _PortfolioScreenState extends State { AppText.investmentamount, style: GoogleFonts.dmSans( color: (index == 2) - ? Color(0xFF8D8D8D) - : Color(0xFF535353), + ? const Color(0xFF8D8D8D) + : const Color(0xFF535353), fontSize: 14.sp, fontWeight: FontWeight.w500, ), @@ -223,7 +227,7 @@ class _PortfolioScreenState extends State { 'SAR 100,000', style: GoogleFonts.dmSans( color: (index == 2) - ? Color(0xFF8D8D8D) + ? const Color(0xFF8D8D8D) : Colors.black, fontSize: 14.sp, fontWeight: FontWeight.w700, @@ -233,7 +237,7 @@ class _PortfolioScreenState extends State { ' \$ 26,700', style: GoogleFonts.dmSans( color: (index == 2) - ? Color(0xFF8D8D8D) + ? const Color(0xFF8D8D8D) : Colors.black, fontSize: 11.sp, fontWeight: FontWeight.w400, @@ -255,8 +259,8 @@ class _PortfolioScreenState extends State { AppText.currentval, style: GoogleFonts.dmSans( color: (index == 2) - ? Color(0xFF8D8D8D) - : Color(0xFF535353), + ? const Color(0xFF8D8D8D) + : const Color(0xFF535353), fontSize: 14.sp, fontWeight: FontWeight.w500, ), @@ -269,7 +273,7 @@ class _PortfolioScreenState extends State { 'SAR 100,000', style: GoogleFonts.dmSans( color: (index == 2) - ? Color(0xFF8D8D8D) + ? const Color(0xFF8D8D8D) : Colors.black, fontSize: 14.sp, fontWeight: FontWeight.w700, @@ -279,7 +283,7 @@ class _PortfolioScreenState extends State { ' \$ 26,700', style: GoogleFonts.dmSans( color: (index == 2) - ? Color(0xFF8D8D8D) + ? const Color(0xFF8D8D8D) : Colors.black, fontSize: 11.sp, fontWeight: FontWeight.w400, @@ -301,8 +305,8 @@ class _PortfolioScreenState extends State { AppText.totalreturn, style: GoogleFonts.dmSans( color: (index == 2) - ? Color(0xFF8D8D8D) - : Color(0xFF535353), + ? const Color(0xFF8D8D8D) + : const Color(0xFF535353), fontSize: 14.sp, fontWeight: FontWeight.w500, ), @@ -311,8 +315,8 @@ class _PortfolioScreenState extends State { (index == 2) ? '- 20.0%' : '+ 20.0%', style: GoogleFonts.dmSans( color: (index == 2) - ? Color(0xFFde9595) - : Color(0xFF066123), + ? const Color(0xFFde9595) + : const Color(0xFF066123), fontSize: 14.sp, fontWeight: FontWeight.w700, ), diff --git a/lib/features/MainScreens/Settings/presentation/pages/settingsScreen.dart b/lib/features/MainScreens/Settings/presentation/pages/settingsScreen.dart index ec3e19c..1eff811 100644 --- a/lib/features/MainScreens/Settings/presentation/pages/settingsScreen.dart +++ b/lib/features/MainScreens/Settings/presentation/pages/settingsScreen.dart @@ -10,7 +10,7 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return const Scaffold( body: Text('Settings'), ); } diff --git a/lib/features/MainScreens/Wallet/presentation/pages/walletScreen.dart b/lib/features/MainScreens/Wallet/presentation/pages/walletScreen.dart index d2e8a7a..be6b9f8 100644 --- a/lib/features/MainScreens/Wallet/presentation/pages/walletScreen.dart +++ b/lib/features/MainScreens/Wallet/presentation/pages/walletScreen.dart @@ -14,6 +14,7 @@ class WalletScreen extends StatefulWidget { class _WalletScreenState extends State { @override Widget build(BuildContext context) { + return Scaffold( backgroundColor: Colors.white, body: CustomScrollView( @@ -156,6 +157,7 @@ class _WalletScreenState extends State { ), ], ), + ); } } diff --git a/lib/features/login/presentation/widgets/bottom_section.dart b/lib/features/login/presentation/widgets/bottom_section.dart index 93842d4..1940cc4 100644 --- a/lib/features/login/presentation/widgets/bottom_section.dart +++ b/lib/features/login/presentation/widgets/bottom_section.dart @@ -45,8 +45,9 @@ class BottomSection extends StatelessWidget { successToastMessage(context, "login successful !"); goRouter.pop(); - // goRouter.goNamed(RouteName.biometricScreen); - goRouter.goNamed(RouteName.mainScreen); + goRouter.goNamed(RouteName.pinScreen, pathParameters: { + "fromScreen": "login", + }); } else if (state is LoginFailure) { goRouter.pop(); errorToastMessage( diff --git a/lib/features/otpVerification/presentation/bloc/timer/timer_state.dart b/lib/features/otpVerification/presentation/bloc/timer/timer_state.dart index 5410e76..a56bbc9 100644 --- a/lib/features/otpVerification/presentation/bloc/timer/timer_state.dart +++ b/lib/features/otpVerification/presentation/bloc/timer/timer_state.dart @@ -20,7 +20,7 @@ class TimerInitial extends TimerState { } class TimerRunInProgress extends TimerState { - const TimerRunInProgress(int duration) : super(duration); + const TimerRunInProgress(super.duration); } class TimerRunComplete extends TimerState { diff --git a/lib/features/register/presentation/widgets/register_user_bottom_section.dart b/lib/features/register/presentation/widgets/register_user_bottom_section.dart index 64f8beb..33cef91 100644 --- a/lib/features/register/presentation/widgets/register_user_bottom_section.dart +++ b/lib/features/register/presentation/widgets/register_user_bottom_section.dart @@ -1,8 +1,13 @@ +import 'dart:developer'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:gap/gap.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:tanami_app/core/styles/app_images.dart'; import 'package:tanami_app/shared/components/checkbox_widget.dart'; import 'package:tanami_app/shared/components/loader.dart'; @@ -12,6 +17,8 @@ import '../../../../core/routes/route_name.dart'; import '../../../../core/routes/routes.dart'; import '../../../../core/styles/app_color.dart'; import '../../../../core/styles/app_text.dart'; +import '../../../../shared/components/bloc/checkbox/checkbox_bloc.dart'; +import '../../../../shared/components/bloc/checkbox/checkbox_state.dart'; import '../../../../shared/components/button_widget.dart'; import '../../../../shared/components/text_widget.dart'; import '../bloc/register_user_bloc.dart'; @@ -27,41 +34,52 @@ class RegisterUserBottomSection extends StatelessWidget { children: [ const Gap(40), Row( + crossAxisAlignment: CrossAxisAlignment.end, children: [ const CheckBoxWidget(), - Container( - width: 0.8.sw, - child: Row( - children: [ - const Text("I agree to the "), - InkWell( - onTap: () { - // Handle Terms & Conditions tap - }, - child: const Text( - "Terms & Conditions", - style: TextStyle( - color: Colors.blue, - decoration: TextDecoration.underline, - ), + Expanded( + child: RichText( + text: TextSpan( + text: AppText.iAgreeToThe, + style: GoogleFonts.dmSans( + color: AppColor.charcoalColor, + fontSize: 14, + fontWeight: FontWeight.w500, ), - ), - const Text(" and the "), - InkWell( - onTap: () { - // Handle Privacy Policy tap - }, - child: const Text( - "Privacy Policy", - style: TextStyle( - color: Colors.blue, - decoration: TextDecoration.underline, + children: [ + TextSpan( + style: GoogleFonts.dmSans( + color: AppColor.selectableTextColor, + fontSize: 14, + fontWeight: FontWeight.w500, + decoration: TextDecoration.underline, + ), + text: AppText.termsAndCondition, + recognizer: TapGestureRecognizer() + ..onTap = () => log('Tap Here onTap'), ), - ), - ), - ], + const TextSpan( + text: AppText.andThe, + style: TextStyle( + color: AppColor.charcoalColor, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + TextSpan( + text: AppText.privacyPolicy, + recognizer: TapGestureRecognizer() + ..onTap = () => log('Tap Here onTap'), + style: const TextStyle( + color: AppColor.selectableTextColor, + fontSize: 14, + fontWeight: FontWeight.w500, + decoration: TextDecoration.underline, + ), + ), + ]), ), - ) + ), ], ), const Gap(10), @@ -79,7 +97,9 @@ class RegisterUserBottomSection extends StatelessWidget { successToastMessage(context, "successful !"); goRouter.pop(); - goRouter.pushNamed(RouteName.otpScreen); + goRouter.goNamed(RouteName.pinScreen, pathParameters: { + "fromScreen": "register", + }); } else if (state is RegisterUserFailure) { goRouter.pop(); errorToastMessage( @@ -90,10 +110,13 @@ class RegisterUserBottomSection extends StatelessWidget { }, builder: (context, state) { bool isButtonEnabled = false; - if (state is RegisterUserFieldsState) { + final checkboxState = context.watch().state; + if (state is RegisterUserFieldsState && + checkboxState is CheckboxChecked) { isButtonEnabled = state.areFieldsFilled; - } else if (state is RegisterUserSuccess || - state is RegisterUserFailure) { + } else if ((state is RegisterUserSuccess || + state is RegisterUserFailure) && + checkboxState is CheckboxChecked) { isButtonEnabled = true; } return Container( diff --git a/lib/features/securePin/presentation/bloc/pin_bloc.dart b/lib/features/securePin/presentation/bloc/pin_bloc.dart new file mode 100644 index 0000000..1b3039c --- /dev/null +++ b/lib/features/securePin/presentation/bloc/pin_bloc.dart @@ -0,0 +1,54 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:tanami_app/core/styles/app_text.dart'; + +import '../../../../core/utils/secure/secure_storage_service.dart'; + +part 'pin_event.dart'; +part 'pin_state.dart'; + +class PinBloc extends Bloc { + final SecureStorageService secureStorageService; + + PinBloc({required this.secureStorageService}) + : super(const PinState( + pin: '', pinComplete: false, isVerified: false, error: '')) { + on((event, emit) { + final newPin = state.pin + event.number; + + if (newPin.length <= 6) { + emit(state.copyWith( + pin: newPin, pinComplete: newPin.length == 6, error: '')); + + if (newPin.length == 6) { + add(VerifyPinPressed(newPin)); + } + } + }); + + on((event, emit) { + if (state.pin.isNotEmpty) { + final newPin = state.pin.substring(0, state.pin.length - 1); + emit(state.copyWith( + pin: newPin, pinComplete: newPin.length == 6, error: '')); + } + }); + + on((event, emit) async { + await secureStorageService.write('pin_code', state.pin); + }); + + on((event, emit) async { + final storedPin = await secureStorageService.read('pin_code'); + + if (storedPin == event.pin) { + emit(state.copyWith(isVerified: true, error: '')); + } else { + emit(state.copyWith( + isVerified: false, + error: AppText.incorrectPinCode, + )); + } + }); + } +} diff --git a/lib/features/securePin/presentation/bloc/pin_event.dart b/lib/features/securePin/presentation/bloc/pin_event.dart new file mode 100644 index 0000000..ba3bc49 --- /dev/null +++ b/lib/features/securePin/presentation/bloc/pin_event.dart @@ -0,0 +1,30 @@ +part of 'pin_bloc.dart'; + +abstract class PinEvent extends Equatable { + const PinEvent(); + + @override + List get props => []; +} + +class NumberPressed extends PinEvent { + final String number; + + const NumberPressed(this.number); + + @override + List get props => [number]; +} + +class BackspacePressed extends PinEvent {} + +class SavePinPressed extends PinEvent {} + +class VerifyPinPressed extends PinEvent { + final String pin; + + const VerifyPinPressed(this.pin); + + @override + List get props => [pin]; +} diff --git a/lib/features/securePin/presentation/bloc/pin_state.dart b/lib/features/securePin/presentation/bloc/pin_state.dart new file mode 100644 index 0000000..6245a9a --- /dev/null +++ b/lib/features/securePin/presentation/bloc/pin_state.dart @@ -0,0 +1,32 @@ +part of 'pin_bloc.dart'; + +class PinState extends Equatable { + final String pin; + final bool pinComplete; + final bool isVerified; + final String error; + + const PinState({ + required this.pin, + required this.pinComplete, + required this.isVerified, + required this.error, + }); + + PinState copyWith({ + String? pin, + bool? pinComplete, + bool? isVerified, + String? error, + }) { + return PinState( + pin: pin ?? this.pin, + pinComplete: pinComplete ?? this.pinComplete, + isVerified: isVerified ?? this.isVerified, + error: error ?? this.error, + ); + } + + @override + List get props => [pin, pinComplete, isVerified, error]; +} diff --git a/lib/features/securePin/presentation/pages/confirm_pin_layout.dart b/lib/features/securePin/presentation/pages/confirm_pin_layout.dart new file mode 100644 index 0000000..feda010 --- /dev/null +++ b/lib/features/securePin/presentation/pages/confirm_pin_layout.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +import '../widgets/confirm_pin_keypad_section.dart'; +import '../widgets/confirm_pin_top_section.dart'; + +class ConfirmPinLayout extends StatelessWidget { + const ConfirmPinLayout({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: ListView( + children: const [ + ConfirmPinTopSection(), + ConfirmPinKey(), + ], + )); + } +} diff --git a/lib/features/securePin/presentation/pages/confirm_pin_screen.dart b/lib/features/securePin/presentation/pages/confirm_pin_screen.dart new file mode 100644 index 0000000..237de02 --- /dev/null +++ b/lib/features/securePin/presentation/pages/confirm_pin_screen.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../core/styles/app_text.dart'; +import '../../../../core/utils/secure/secure_storage_service.dart'; +import '../../../../shared/components/appbar_widget.dart'; +import '../bloc/pin_bloc.dart'; +import 'confirm_pin_layout.dart'; + +class ConfirmPinScreen extends StatelessWidget { + const ConfirmPinScreen({super.key}); + + @override + Widget build(BuildContext context) { + final secureStorageService = SecureStorageService(); + return Scaffold( + appBar: const AppBarWidget( + height: 75, + titleTxt: AppText.createPinCode, + showLeading: true, + ), + resizeToAvoidBottomInset: true, + body: MultiBlocProvider( + providers: [ + BlocProvider( + // Create an instance of the OnboardingBloc + create: (context) => + PinBloc(secureStorageService: secureStorageService), + ), + ], + child: const ConfirmPinLayout(), + ), + ); + } +} diff --git a/lib/features/securePin/presentation/pages/pin_layout.dart b/lib/features/securePin/presentation/pages/pin_layout.dart new file mode 100644 index 0000000..66ec129 --- /dev/null +++ b/lib/features/securePin/presentation/pages/pin_layout.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:tanami_app/features/securePin/presentation/widgets/pin_keypad_section.dart'; +import 'package:tanami_app/features/securePin/presentation/widgets/pin_top_section.dart'; + +class PinLayout extends StatelessWidget { + final String fromScreen; + const PinLayout({super.key, required this.fromScreen}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: ListView( + children: [ + PinTopSection(fromScreen: fromScreen), + PinKey( + fromScreen: fromScreen, + ), + ], + )); + } +} diff --git a/lib/features/securePin/presentation/pages/pin_screen.dart b/lib/features/securePin/presentation/pages/pin_screen.dart new file mode 100644 index 0000000..b9f464d --- /dev/null +++ b/lib/features/securePin/presentation/pages/pin_screen.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../core/styles/app_text.dart'; +import '../../../../core/utils/secure/secure_storage_service.dart'; +import '../../../../shared/components/appbar_widget.dart'; +import '../bloc/pin_bloc.dart'; +import 'pin_layout.dart'; + +class PinScreen extends StatelessWidget { + final String fromScreen; + const PinScreen({super.key, required this.fromScreen}); + + @override + Widget build(BuildContext context) { + final secureStorageService = SecureStorageService(); + return Scaffold( + appBar: fromScreen == "register" + ? const AppBarWidget( + height: 75, + titleTxt: AppText.createPinCode, + showLeading: false, + ) + : null, + resizeToAvoidBottomInset: true, + body: MultiBlocProvider( + providers: [ + BlocProvider( + // Create an instance of the OnboardingBloc + create: (context) => + PinBloc(secureStorageService: secureStorageService), + ), + ], + child: PinLayout( + fromScreen: fromScreen, + ), + ), + ); + } +} diff --git a/lib/features/securePin/presentation/widgets/confirm_pin_keypad_section.dart b/lib/features/securePin/presentation/widgets/confirm_pin_keypad_section.dart new file mode 100644 index 0000000..b1d7c45 --- /dev/null +++ b/lib/features/securePin/presentation/widgets/confirm_pin_keypad_section.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; +import 'package:tanami_app/core/routes/route_name.dart'; +import 'package:tanami_app/core/routes/routes.dart'; +import 'package:tanami_app/core/styles/app_color.dart'; +import 'package:tanami_app/shared/components/text_widget.dart'; + +import '../bloc/pin_bloc.dart'; + +class ConfirmPinKey extends StatelessWidget { + const ConfirmPinKey({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Gap(20), + BlocConsumer( + listener: (context, state) { + if (state.pinComplete && state.isVerified) { + // successToastMessage(context, "Pin verified successfully"); + goRouter.goNamed(RouteName.mainScreen); + } + }, + builder: (context, state) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(6, (index) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 10), + width: 32, + height: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: state.error.isNotEmpty + ? AppColor.txtErrorColor + : index < state.pin.length + ? AppColor.pinFillBorderColor + : AppColor.pinInActiveBorderColor), + color: index < state.pin.length + ? AppColor.pinFillColor + : Colors.transparent, + ), + ); + }), + ); + }, + ), + BlocBuilder( + builder: (context, state) { + if (state.error.isNotEmpty) { + return Container( + margin: const EdgeInsets.only(top: 5), + child: TextWidget() + .text14W500(state.error, clr: AppColor.txtErrorColor), + ); + } + return const SizedBox.shrink(); + }, + ), + const Gap(50), + GridView.builder( + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 1.3, + ), + itemCount: 12, + itemBuilder: (context, index) { + if (index == 9) { + return const SizedBox.shrink(); + } else if (index == 11) { + return GestureDetector( + onTap: () { + context.read().add(BackspacePressed()); + }, + child: Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black12), + ), + child: const Icon( + Icons.arrow_back_rounded, + ), + ), + ); + } else { + final number = index == 10 ? '0' : '${index + 1}'; + return GestureDetector( + onTap: () { + context.read().add(NumberPressed(number)); + }, + child: Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black12), + ), + child: Center( + child: TextWidget().text20W700( + number, + clr: AppColor.plainBlack, + )), + ), + ); + } + }, + ), + ], + ); + } +} diff --git a/lib/features/securePin/presentation/widgets/confirm_pin_top_section.dart b/lib/features/securePin/presentation/widgets/confirm_pin_top_section.dart new file mode 100644 index 0000000..28d92ea --- /dev/null +++ b/lib/features/securePin/presentation/widgets/confirm_pin_top_section.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:gap/gap.dart'; +import 'package:tanami_app/core/styles/app_color.dart'; +import 'package:tanami_app/shared/components/text_widget.dart'; + +import '../../../../core/styles/app_images.dart'; +import '../../../../core/styles/app_text.dart'; + +class ConfirmPinTopSection extends StatelessWidget { + const ConfirmPinTopSection({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Gap(85), + Center( + child: SvgPicture.asset( + AppImages.weclomeLogo, + ), + ), + const Gap(60), + TextWidget().text14W500( + AppText.confirmPinCode, + clr: AppColor.textLabelColor, + ) + ], + ); + } +} diff --git a/lib/features/securePin/presentation/widgets/forgot_pin_dialog.dart b/lib/features/securePin/presentation/widgets/forgot_pin_dialog.dart new file mode 100644 index 0000000..87b93ff --- /dev/null +++ b/lib/features/securePin/presentation/widgets/forgot_pin_dialog.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:gap/gap.dart'; +import 'package:tanami_app/core/routes/routes.dart'; +import 'package:tanami_app/core/styles/app_color.dart'; +import 'package:tanami_app/core/styles/app_text.dart'; +import 'package:tanami_app/shared/components/button_widget.dart'; +import 'package:tanami_app/shared/components/text_widget.dart'; + +void forgotPinDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + backgroundColor: AppColor.plainWhite, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(22), + color: AppColor.plainWhite, + ), + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextWidget().text17W700(AppText.notificationText, + clr: AppColor.plainBlack), + const Gap(25), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + ), + child: TextWidget().text15W500( + AppText.toRestorePinYouWillBeLoggedOut, + clr: AppColor.hintTextColor, + ), + ), + const Gap(40), + SizedBox( + width: 0.9.sw, + height: 55.h, + child: ButtonWidget().elevatedBtn( + txtClr: AppColor.plainWhite, + text: AppText.allowText, + clr: AppColor.primaryColor2, + function: () { + goRouter.pop(); + }, + ), + ), + const Gap(10), + ButtonWidget().textBtn( + text: TextWidget().text14W700( + AppText.declineText, + textDecoration: TextDecoration.underline, + clr: AppColor.textLabelColor, + ), + function: () { + goRouter.pop(); + }, + ), + ], + ), + ), + ); + }, + ); +} diff --git a/lib/features/securePin/presentation/widgets/pin_keypad_section.dart b/lib/features/securePin/presentation/widgets/pin_keypad_section.dart new file mode 100644 index 0000000..5ebfc9d --- /dev/null +++ b/lib/features/securePin/presentation/widgets/pin_keypad_section.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; +import 'package:tanami_app/core/routes/route_name.dart'; +import 'package:tanami_app/core/routes/routes.dart'; +import 'package:tanami_app/core/styles/app_color.dart'; +import 'package:tanami_app/core/styles/app_text.dart'; +import 'package:tanami_app/features/securePin/presentation/widgets/forgot_pin_dialog.dart'; +import 'package:tanami_app/shared/components/text_widget.dart'; + +import '../bloc/pin_bloc.dart'; + +class PinKey extends StatelessWidget { + final String fromScreen; + const PinKey({ + super.key, + required this.fromScreen, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const Gap(20), + BlocConsumer( + listener: (context, state) { + if (state.pinComplete) { + if (fromScreen == "login") { + goRouter.pushNamed(RouteName.mainScreen); + } else { + context.read().add(SavePinPressed()); + goRouter.pushNamed(RouteName.confirmPinScreen); + } + } + }, + builder: (context, state) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(6, (index) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 10), + width: 32, + height: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: index < state.pin.length + ? AppColor.pinFillBorderColor + : AppColor.pinInActiveBorderColor, + ), + color: index < state.pin.length + ? AppColor.pinFillColor + : Colors.transparent, + ), + ); + }), + ); + }, + ), + fromScreen == "login" ? const Gap(20) : const Gap(0), + fromScreen == "login" + ? InkWell( + onTap: () { + forgotPinDialog(context); + }, + child: Padding( + padding: const EdgeInsets.only( + right: 50, + ), + child: TextWidget().text15W500( + AppText.forgotPinCode, + clr: AppColor.hintTextColor, + textDecoration: TextDecoration.underline, + ), + ), + ) + : const SizedBox(), + const Gap(50), + GridView.builder( + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 1.3, + ), + itemCount: 12, + itemBuilder: (context, index) { + if (index == 9) { + return const SizedBox.shrink(); + } else if (index == 11) { + return GestureDetector( + onTap: () { + context.read().add(BackspacePressed()); + }, + child: Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black12), + ), + child: const Icon( + Icons.arrow_back_rounded, + ), + ), + ); + } else { + final number = index == 10 ? '0' : '${index + 1}'; + return GestureDetector( + onTap: () { + context.read().add(NumberPressed(number)); + }, + child: Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black12), + ), + child: Center( + child: TextWidget().text20W700( + number, + clr: AppColor.plainBlack, + )), + ), + ); + } + }, + ), + ], + ); + } +} diff --git a/lib/features/securePin/presentation/widgets/pin_top_section.dart b/lib/features/securePin/presentation/widgets/pin_top_section.dart new file mode 100644 index 0000000..6c3b7a7 --- /dev/null +++ b/lib/features/securePin/presentation/widgets/pin_top_section.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:gap/gap.dart'; +import 'package:tanami_app/core/styles/app_color.dart'; +import 'package:tanami_app/core/styles/app_text.dart'; +import 'package:tanami_app/shared/components/text_widget.dart'; + +import '../../../../core/styles/app_images.dart'; + +class PinTopSection extends StatelessWidget { + final String fromScreen; + const PinTopSection({ + super.key, + required this.fromScreen, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Gap(85), + Center( + child: SvgPicture.asset( + AppImages.weclomeLogo, + ), + ), + const Gap(60), + fromScreen == "login" + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TextWidget().text14W500(AppText.welcomeBackText, + clr: AppColor.smokeGrayColor), + TextWidget() + .text14W700(", Jayesh", clr: AppColor.plainBlack), + ], + ), + TextWidget().text14W500( + AppText.userYourAppPinToLoginEnterTanami, + clr: AppColor.smokeGrayColor), + const Gap(25), + TextWidget().text14W400( + AppText.pinCode, + clr: AppColor.textLabelColor, + ) + ], + ) + : TextWidget().text14W500( + AppText.createPinCode, + clr: AppColor.textLabelColor, + ) + ], + ); + } +} diff --git a/lib/shared/components/appbar_widget.dart b/lib/shared/components/appbar_widget.dart index 445b197..62eb1d7 100644 --- a/lib/shared/components/appbar_widget.dart +++ b/lib/shared/components/appbar_widget.dart @@ -41,21 +41,23 @@ class AppBarWidget extends StatelessWidget implements PreferredSizeWidget { padding: EdgeInsets.only( left: 16.w, ), - child: GestureDetector( - onTap: () { - customBack ?? false - ? goRouter.goNamed(backPageName!) - : goRouter.pop(); - }, - child: Padding( - padding: EdgeInsets.only(left: 8.w), - child: Icon( - Icons.arrow_back_rounded, - color: AppColor.appBarIconColor, - size: 25.r, - ), - ), - ), + child: !showLeading! + ? null + : GestureDetector( + onTap: () { + customBack ?? false + ? goRouter.goNamed(backPageName!) + : goRouter.pop(); + }, + child: Padding( + padding: EdgeInsets.only(left: 8.w), + child: Icon( + Icons.arrow_back_rounded, + color: AppColor.appBarIconColor, + size: 25.r, + ), + ), + ), ), actions: [ if (customActionWidget != null) diff --git a/lib/shared/components/checkbox_widget.dart b/lib/shared/components/checkbox_widget.dart index 81b853d..9dd002d 100644 --- a/lib/shared/components/checkbox_widget.dart +++ b/lib/shared/components/checkbox_widget.dart @@ -29,7 +29,7 @@ class CheckBoxWidget extends StatelessWidget { }, ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), // Custom radius + borderRadius: BorderRadius.circular(6), ), value: state is CheckboxChecked, onChanged: (value) { diff --git a/lib/shared/components/text_widget.dart b/lib/shared/components/text_widget.dart index b54fd6f..4b259a3 100644 --- a/lib/shared/components/text_widget.dart +++ b/lib/shared/components/text_widget.dart @@ -14,6 +14,21 @@ class TextWidget { //Text Size 14 + Widget text14W400( + String text, { + Color? clr, + TextDecoration? textDecoration, + TextAlign? txtAlign, + }) { + return Text(text, + textAlign: txtAlign ?? TextAlign.center, + style: GoogleFonts.dmSans( + fontSize: 14, + fontWeight: FontWeight.w400, + decoration: textDecoration ?? TextDecoration.none, + color: clr ?? AppColor.plainWhite)); + } + Widget text14W500( String text, { Color? clr, @@ -44,11 +59,14 @@ class TextWidget { Widget text15W500( String text, { Color? clr, + TextDecoration? textDecoration, }) { return Text(text, textAlign: TextAlign.center, style: GoogleFonts.dmSans( fontSize: 15, + decorationColor: AppColor.hintTextColor, + decoration: textDecoration ?? TextDecoration.none, fontWeight: FontWeight.w500, color: clr ?? AppColor.plainWhite)); } @@ -69,6 +87,22 @@ class TextWidget { color: clr ?? AppColor.plainWhite)); } + //Text Size 17 + Widget text17W700( + String text, { + Color? clr, + TextDecoration? textDecoration, + }) { + return Text(text, + textAlign: TextAlign.center, + style: GoogleFonts.dmSans( + fontSize: 17, + decorationColor: AppColor.hintTextColor, + decoration: textDecoration ?? TextDecoration.none, + fontWeight: FontWeight.w700, + color: clr ?? AppColor.plainWhite)); + } + //Text Size 22 Widget text22W700(String text, {Color? clr}) { return Text(text,