diff --git a/recipients_app/assets/earth_animation.gif b/recipients_app/assets/earth_animation.gif new file mode 100644 index 000000000..8cbb00526 Binary files /dev/null and b/recipients_app/assets/earth_animation.gif differ diff --git a/recipients_app/ios/Podfile.lock b/recipients_app/ios/Podfile.lock index 184fbcac3..75f7c7313 100644 --- a/recipients_app/ios/Podfile.lock +++ b/recipients_app/ios/Podfile.lock @@ -179,7 +179,7 @@ SPEC CHECKSUMS: FirebaseCoreExtension: f17247ba8c61e4d3c8d136b5e2de3cb4ac6a85b6 FirebaseCoreInternal: 8845798510aae74703467480f71ac613788d0696 FirebaseCrashlytics: 35fdd1a433b31e28adcf5c8933f4c526691a1e0b - FirebaseFirestore: 712ff268d746fc65efb361e9ce2cfeb458f10643 + FirebaseFirestore: 7b7d95b74637ed61b76996077a2b292167dae7e5 FirebaseInstallations: 59c0e4c7a816a0f76710d83f77e5369b3e45eb96 FirebaseSessions: 34e5c084da010ef3802cbc062b822e513c9e6318 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 diff --git a/recipients_app/lib/core/cubits/signup/signup_cubit.dart b/recipients_app/lib/core/cubits/signup/signup_cubit.dart index c99a0d5c1..82bd5b588 100644 --- a/recipients_app/lib/core/cubits/signup/signup_cubit.dart +++ b/recipients_app/lib/core/cubits/signup/signup_cubit.dart @@ -34,11 +34,13 @@ class SignupCubit extends Cubit { try { await userRepository.verifyPhoneNumber( phoneNumber: phoneNumber, - onCodeSend: (verificationId) { + forceResendingToken: state.forceResendingToken, + onCodeSend: (verificationId, forceResendingToken) { emit( state.copyWith( status: SignupStatus.enterVerificationCode, phoneNumber: phoneNumber, + forceResendingToken: forceResendingToken, verificationId: verificationId, ), ); diff --git a/recipients_app/lib/core/cubits/signup/signup_state.dart b/recipients_app/lib/core/cubits/signup/signup_state.dart index d92fe28cc..ee8e0b88f 100644 --- a/recipients_app/lib/core/cubits/signup/signup_state.dart +++ b/recipients_app/lib/core/cubits/signup/signup_state.dart @@ -14,28 +14,32 @@ class SignupState extends Equatable { final SignupStatus status; final String? phoneNumber; final String? verificationId; + final int? forceResendingToken; final Exception? exception; const SignupState({ this.status = SignupStatus.enterPhoneNumber, this.phoneNumber, this.verificationId, + this.forceResendingToken, this.exception, }); @override - List get props => [status, phoneNumber, verificationId, exception]; + List get props => [status, phoneNumber, verificationId, forceResendingToken, exception]; SignupState copyWith({ SignupStatus? status, String? phoneNumber, String? verificationId, + int? forceResendingToken, Exception? exception, }) { return SignupState( status: status ?? this.status, phoneNumber: phoneNumber ?? this.phoneNumber, verificationId: verificationId ?? this.verificationId, + forceResendingToken: forceResendingToken ?? this.forceResendingToken, exception: exception ?? this.exception, ); } diff --git a/recipients_app/lib/data/repositories/user_repository.dart b/recipients_app/lib/data/repositories/user_repository.dart index 88679bf17..2ed0839a3 100644 --- a/recipients_app/lib/data/repositories/user_repository.dart +++ b/recipients_app/lib/data/repositories/user_repository.dart @@ -59,18 +59,20 @@ class UserRepository { Future verifyPhoneNumber({ required String phoneNumber, - required Function(String) onCodeSend, + required Function(String, int?) onCodeSend, required Function(FirebaseAuthException) onVerificationFailed, required Function(PhoneAuthCredential) onVerificationCompleted, + required int? forceResendingToken, }) async { await firebaseAuth.verifyPhoneNumber( phoneNumber: phoneNumber, + forceResendingToken: forceResendingToken, timeout: const Duration(seconds: 60), verificationCompleted: (credential) => onVerificationCompleted(credential), verificationFailed: (ex) => onVerificationFailed(ex), - codeSent: (verificationId, [forceResendingToken]) => - onCodeSend(verificationId), + codeSent: (verificationId, forceResendingToken) => + onCodeSend(verificationId, forceResendingToken), codeAutoRetrievalTimeout: (e) { log("auto-retrieval timeout"); }, diff --git a/recipients_app/lib/my_app.dart b/recipients_app/lib/my_app.dart index 9634ca249..c081a602d 100644 --- a/recipients_app/lib/my_app.dart +++ b/recipients_app/lib/my_app.dart @@ -2,6 +2,7 @@ import "package:app/core/cubits/auth/auth_cubit.dart"; import "package:app/data/repositories/repositories.dart"; import "package:app/ui/configs/configs.dart"; import "package:app/view/pages/main_app_page.dart"; +import "package:app/view/pages/terms_and_conditions_page.dart"; import "package:app/view/pages/welcome_page.dart"; import "package:cloud_firestore/cloud_firestore.dart"; import "package:firebase_auth/firebase_auth.dart"; @@ -73,7 +74,11 @@ class MyApp extends StatelessWidget { case AuthStatus.updateRecipientFailure: case AuthStatus.updateRecipientSuccess: case AuthStatus.updatingRecipient: - return const MainAppPage(); + if (state.recipient?.termsAccepted == true) { + return const MainAppPage(); + } else { + return const TermsAndConditionsPage(); + } } }, ), diff --git a/recipients_app/lib/ui/inputs/input_text.dart b/recipients_app/lib/ui/inputs/input_text.dart index 53e97a8e8..32075fdb8 100644 --- a/recipients_app/lib/ui/inputs/input_text.dart +++ b/recipients_app/lib/ui/inputs/input_text.dart @@ -12,6 +12,7 @@ class InputText extends StatelessWidget { final bool isReadOnly; final Widget? suffixIcon; final TextInputType? keyboardType; + final int? maxLength; const InputText({ super.key, @@ -25,6 +26,7 @@ class InputText extends StatelessWidget { this.isReadOnly = false, this.suffixIcon, this.keyboardType = TextInputType.text, + this.maxLength, }); @override @@ -37,6 +39,10 @@ class InputText extends StatelessWidget { // labelText: hintText, suffixIcon: suffixIcon, floatingLabelBehavior: FloatingLabelBehavior.never, + // when maxLength is added TextFormField shows counter e.g. 0/1, + // we don't need it and one of the solutions is to provide SizedBox.shrink() + // see https://stackoverflow.com/a/58819500 + counter: const SizedBox.shrink() ), style: AppStyles.inputText, readOnly: isReadOnly, @@ -47,6 +53,7 @@ class InputText extends StatelessWidget { focusNode: focusNode, validator: validator, keyboardType: keyboardType, + maxLength: maxLength, ), if (hintText != null) Positioned( diff --git a/recipients_app/lib/view/pages/account_page.dart b/recipients_app/lib/view/pages/account_page.dart index 2f2ae33c0..95dda8a78 100644 --- a/recipients_app/lib/view/pages/account_page.dart +++ b/recipients_app/lib/view/pages/account_page.dart @@ -94,6 +94,8 @@ class AccountPageState extends State { "Failed to update profile. Please try again or contact our support"), ), ); + } else if (state.status == AuthStatus.unauthenticated) { + Navigator.of(context).pop(); } }, builder: (context, state) { diff --git a/recipients_app/lib/view/pages/terms_and_conditions.dart b/recipients_app/lib/view/pages/terms_and_conditions.dart deleted file mode 100644 index 16f602e4d..000000000 --- a/recipients_app/lib/view/pages/terms_and_conditions.dart +++ /dev/null @@ -1,113 +0,0 @@ -import "package:app/core/cubits/auth/auth_cubit.dart"; -import "package:app/ui/buttons/buttons.dart"; -import "package:app/ui/configs/configs.dart"; -import "package:flutter/material.dart"; -import "package:flutter_bloc/flutter_bloc.dart"; -import "package:url_launcher/url_launcher_string.dart"; - -// TODO reenable terms and condition page on signin -class TermsAndConditions extends StatelessWidget { - const TermsAndConditions({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: AppSpacings.a16, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 16), - const Text( - "Welcome to Social Income", - style: TextStyle(fontSize: 28, fontWeight: FontWeight.w400), - ), - const SizedBox(height: 16), - const Text( - "To give you the best experience, we use data from your device to", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - height: 1.4, - ), - ), - const SizedBox(height: 16), - const Column( - children: [ - _IconAndText( - "Make the app work and provide our services", - Icons.remember_me, - ), - _IconAndText( - "Improve service performance by use of analytics", - Icons.poll, - ), - _IconAndText("Read our privacy policy", Icons.policy), - ], - ), - const Spacer(), - ButtonBig( - onPressed: () { - final updated = - context.read().state.recipient!.copyWith( - termsAccepted: true, - ); - - context.read().updateRecipient(updated); - }, - label: "Accept", - ), - ], - ), - ); - } -} - -class _IconAndText extends StatelessWidget { - final IconData icon; - final String text; - const _IconAndText(this.text, this.icon); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Icon(icon, size: 36, color: Colors.grey[700]), - Flexible( - child: Padding( - padding: AppSpacings.a16, - child: text == "Read our privacy policy" - ? TextButton( - style: TextButton.styleFrom(padding: EdgeInsets.zero), - onPressed: () async { - const url = "https://socialincome.org/privacy"; - if (await canLaunchUrlString(url)) { - await launchUrlString(url); - } else { - throw "Could not launch $url"; - } - }, - child: Text( - text, - style: const TextStyle( - decoration: TextDecoration.underline, - fontSize: 18, - color: Colors.black, - fontWeight: FontWeight.w500, - height: 1.4, - ), - ), - ) - : Text( - text, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - height: 1.4, - ), - ), - ), - ), - ], - ); - } -} diff --git a/recipients_app/lib/view/pages/terms_and_conditions_page.dart b/recipients_app/lib/view/pages/terms_and_conditions_page.dart new file mode 100644 index 000000000..7173aa710 --- /dev/null +++ b/recipients_app/lib/view/pages/terms_and_conditions_page.dart @@ -0,0 +1,108 @@ +import "package:app/core/cubits/auth/auth_cubit.dart"; +import "package:app/ui/buttons/buttons.dart"; +import "package:app/ui/configs/configs.dart"; +import "package:flutter/gestures.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_bloc/flutter_bloc.dart"; +import "package:url_launcher/url_launcher_string.dart"; + +class TermsAndConditionsPage extends StatelessWidget { + const TermsAndConditionsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + title: const Text("Account"), + centerTitle: true, + ), + body: Padding( + padding: AppSpacings.a16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: "By creating an account, you agree with our ", + style: Theme.of(context) + .textTheme + .headlineLarge! + .copyWith( + color: AppColors.primaryColor, + fontWeight: FontWeight.bold, + )), + TextSpan( + text: "privacy policy.", + style: Theme.of(context) + .textTheme + .headlineLarge! + .copyWith( + color: AppColors.primaryColor, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer() + ..onTap = () async { + const url = "https://socialincome.org/privacy"; + if (await canLaunchUrlString(url)) { + await launchUrlString(url); + } else { + Clipboard.setData( + const ClipboardData( + text: "https://socialincome.org/privacy", + ), + ).then((_) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + "Can't open privacy policy right now. Copied website address to the clipboard."), + ), + ); + }); + } + }, + ), + ], + ), + ), + const SizedBox( + height: 200, + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ButtonBig( + onPressed: () { + final updated = + context.read().state.recipient?.copyWith( + termsAccepted: true, + ); + + if (updated != null) { + context.read().updateRecipient(updated); + } + }, + label: "Create account", + ), + ], + ), + const SizedBox(height: 32), + ], + ), + ), + ); + } +} diff --git a/recipients_app/lib/view/pages/welcome_page.dart b/recipients_app/lib/view/pages/welcome_page.dart index 21d8c928d..eb6d1b0f6 100644 --- a/recipients_app/lib/view/pages/welcome_page.dart +++ b/recipients_app/lib/view/pages/welcome_page.dart @@ -1,9 +1,8 @@ import "package:app/core/cubits/signup/signup_cubit.dart"; import "package:app/core/helpers/snackbar_helper.dart"; import "package:app/data/repositories/repositories.dart"; -import "package:app/ui/configs/app_colors.dart"; -import "package:app/view/widgets/welcome/otp_input.dart"; -import "package:app/view/widgets/welcome/phone_input.dart"; +import "package:app/view/widgets/welcome/otp_input_page.dart"; +import "package:app/view/widgets/welcome/phone_input_page.dart"; import "package:flutter/material.dart"; import "package:flutter_bloc/flutter_bloc.dart"; @@ -28,85 +27,35 @@ class _WelcomeView extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - body: Stack( - children: [ - Column( - children: [ - Flexible( - flex: 2, - child: Container(color: AppColors.primaryColor), - ), - Flexible( - flex: 3, - child: Container(color: AppColors.primaryLightColor), - ), - ], - ), - Column( - children: [ - Expanded( - child: ListView( - padding: const EdgeInsets.fromLTRB(16, 64, 16, 16), - children: [ - const Center( - child: Text( - "Social Income", - style: TextStyle(color: Colors.white, fontSize: 36), - ), - ), - const SizedBox(height: 32), - Image( - image: const AssetImage("assets/phone.png"), - height: MediaQuery.of(context).size.height * 0.4, - ), - const SizedBox(height: 32), - const Text( - "Universal Basic Income\nfrom Human to Human", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: AppColors.primaryColor, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - BlocConsumer( - listener: (context, state) { - if (state.status == SignupStatus.verificationFailure) { - SnackbarHelper.showSnackbar( - context, - message: state.exception.toString(), - type: SnackbarType.error, - ); - } else if (state.status == - SignupStatus.phoneNumberFailure) { - SnackbarHelper.showSnackbar( - context, - message: state.exception.toString(), - type: SnackbarType.error, - ); - } - }, - builder: (context, state) { - switch (state.status) { - case SignupStatus.loadingPhoneNumber: - case SignupStatus.enterPhoneNumber: - case SignupStatus.phoneNumberFailure: - return const PhoneInput(); - case SignupStatus.loadingVerificationCode: - case SignupStatus.enterVerificationCode: - case SignupStatus.verificationSuccess: - case SignupStatus.verificationFailure: - return const OtpInput(); - } - }, - ), - ], - ), - ), - ], - ), - ], + body: BlocConsumer( + listener: (context, state) { + if (state.status == SignupStatus.verificationFailure) { + SnackbarHelper.showSnackbar( + context, + message: state.exception.toString(), + type: SnackbarType.error, + ); + } else if (state.status == SignupStatus.phoneNumberFailure) { + SnackbarHelper.showSnackbar( + context, + message: state.exception.toString(), + type: SnackbarType.error, + ); + } + }, + builder: (context, state) { + switch (state.status) { + case SignupStatus.loadingPhoneNumber: + case SignupStatus.enterPhoneNumber: + case SignupStatus.phoneNumberFailure: + return const PhoneInputPage(); + case SignupStatus.loadingVerificationCode: + case SignupStatus.enterVerificationCode: + case SignupStatus.verificationSuccess: + case SignupStatus.verificationFailure: + return const OtpInputPage(); + } + }, ), ); } diff --git a/recipients_app/lib/view/widgets/welcome/otp_input.dart b/recipients_app/lib/view/widgets/welcome/otp_input.dart index 4685ecf0f..2c1633c01 100644 --- a/recipients_app/lib/view/widgets/welcome/otp_input.dart +++ b/recipients_app/lib/view/widgets/welcome/otp_input.dart @@ -1,80 +1,142 @@ -import "package:app/core/cubits/signup/signup_cubit.dart"; -import "package:app/ui/buttons/buttons.dart"; -import "package:app/ui/configs/app_colors.dart"; -import "package:app/ui/inputs/input_text.dart"; +import "package:app/view/widgets/welcome/otp_input_field.dart"; import "package:flutter/material.dart"; -import "package:flutter_bloc/flutter_bloc.dart"; class OtpInput extends StatefulWidget { - const OtpInput({super.key}); + final Function(String)? onCodeReady; + + const OtpInput({super.key, this.onCodeReady}); @override State createState() => _OtpInputState(); } class _OtpInputState extends State { - late final TextEditingController inputController; + late final TextEditingController digit1Controller; + late final TextEditingController digit2Controller; + late final TextEditingController digit3Controller; + late final TextEditingController digit4Controller; + late final TextEditingController digit5Controller; + late final TextEditingController digit6Controller; @override void initState() { super.initState(); - inputController = TextEditingController(); + digit1Controller = TextEditingController(); + digit2Controller = TextEditingController(); + digit3Controller = TextEditingController(); + digit4Controller = TextEditingController(); + digit5Controller = TextEditingController(); + digit6Controller = TextEditingController(); } @override void dispose() { - inputController.dispose(); + digit1Controller.dispose(); + digit2Controller.dispose(); + digit3Controller.dispose(); + digit4Controller.dispose(); + digit5Controller.dispose(); + digit6Controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final isLoading = context.watch().state.status == - SignupStatus.loadingVerificationCode; - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - InputText( - controller: inputController, - keyboardType: TextInputType.number, - hintText: "Verification code", + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + OtpInputField( + controller: digit1Controller, + onChanged: (value) { + if (value?.length == 1) { + FocusScope.of(context).nextFocus(); + } + _checkCodeStatus(); + }, + ), + OtpInputField( + controller: digit2Controller, + onChanged: (value) { + if (value?.length == 1) { + FocusScope.of(context).nextFocus(); + } else { + FocusScope.of(context).previousFocus(); + } + _checkCodeStatus(); + }, + ), + OtpInputField( + controller: digit3Controller, + onChanged: (value) { + if (value?.length == 1) { + FocusScope.of(context).nextFocus(); + } else { + FocusScope.of(context).previousFocus(); + } + _checkCodeStatus(); + }, ), - const SizedBox(height: 16), - ButtonBig( - isLoading: isLoading, - onPressed: () { - // TODO: catch errors if: - // - no code was entered - context - .read() - .submitVerificationCode(inputController.text); + OtpInputField( + controller: digit4Controller, + onChanged: (value) { + if (value?.length == 1) { + FocusScope.of(context).nextFocus(); + } else { + FocusScope.of(context).previousFocus(); + } + _checkCodeStatus(); }, - label: "Login", ), - TextButton( - onPressed: () async => - context.read().resendVerificationCode(), - child: const Text( - "Resend code", - style: TextStyle( - color: AppColors.primaryColor, - decoration: TextDecoration.underline, - ), - ), + OtpInputField( + controller: digit5Controller, + onChanged: (value) { + if (value?.length == 1) { + FocusScope.of(context).nextFocus(); + } else { + FocusScope.of(context).previousFocus(); + } + _checkCodeStatus(); + }, ), - TextButton( - onPressed: () async => - context.read().changeToPhoneInput(), - child: const Text( - "Back to phone number", - style: TextStyle( - color: AppColors.primaryColor, - decoration: TextDecoration.underline, - ), - ), + OtpInputField( + controller: digit6Controller, + onChanged: (value) { + if (value?.length == 1) { + FocusScope.of(context).unfocus(); + } else { + FocusScope.of(context).previousFocus(); + } + _checkCodeStatus(); + }, ), ], ); } + + void _checkCodeStatus() { + if (_verifyFullCodeReady()) { + final verificationCode = digit1Controller.text + + digit2Controller.text + + digit3Controller.text + + digit4Controller.text + + digit5Controller.text + + digit6Controller.text; + + // call listener with the full code, + var onCodeReady = widget.onCodeReady; + if (onCodeReady != null) { + onCodeReady(verificationCode); + } + } + } + + bool _verifyFullCodeReady() { + return digit1Controller.text.isNotEmpty && + digit2Controller.text.isNotEmpty && + digit3Controller.text.isNotEmpty && + digit4Controller.text.isNotEmpty && + digit5Controller.text.isNotEmpty && + digit6Controller.text.isNotEmpty; + } } diff --git a/recipients_app/lib/view/widgets/welcome/otp_input_field.dart b/recipients_app/lib/view/widgets/welcome/otp_input_field.dart new file mode 100644 index 000000000..372cf3d6c --- /dev/null +++ b/recipients_app/lib/view/widgets/welcome/otp_input_field.dart @@ -0,0 +1,52 @@ +import "package:app/ui/configs/configs.dart"; +import "package:flutter/material.dart"; + +class OtpInputField extends StatelessWidget { + final TextEditingController? controller; + final Function(String?)? onChanged; + + const OtpInputField({ + super.key, + this.controller, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Padding( + padding: AppSpacings.a4, + child: SizedBox( + height: 60, + width: 60, + child: TextFormField( + decoration: const InputDecoration( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: AppColors.primaryColor), + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: AppColors.primaryColor), + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + floatingLabelBehavior: FloatingLabelBehavior.never, + counter: SizedBox.shrink(), + ), + style: AppStyles.inputText, + readOnly: false, + controller: controller, + onFieldSubmitted: null, + onChanged: onChanged, + onTap: null, + autofocus: false, + focusNode: null, + validator: null, + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + ), + ), + ), + ); + } +} diff --git a/recipients_app/lib/view/widgets/welcome/otp_input_page.dart b/recipients_app/lib/view/widgets/welcome/otp_input_page.dart new file mode 100644 index 000000000..771438a84 --- /dev/null +++ b/recipients_app/lib/view/widgets/welcome/otp_input_page.dart @@ -0,0 +1,64 @@ +import "package:app/core/cubits/signup/signup_cubit.dart"; +import "package:app/ui/configs/configs.dart"; +import "package:app/view/widgets/welcome/otp_input.dart"; +import "package:flutter/material.dart"; +import "package:flutter_bloc/flutter_bloc.dart"; + +class OtpInputPage extends StatefulWidget { + const OtpInputPage({super.key}); + + @override + State createState() => _OtpInputPageState(); +} + +class _OtpInputPageState extends State { + @override + Widget build(BuildContext context) { + final phoneNumber = context.watch().state.phoneNumber ?? ""; + + return Scaffold( + appBar: AppBar( + elevation: 0, + title: const Text("Verification"), + leading: BackButton(onPressed: () { + context.read().changeToPhoneInput(); + }), + centerTitle: true, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "We sent you a verification code to $phoneNumber", + style: AppStyles.headlineLarge.copyWith( + color: AppColors.primaryColor, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + OtpInput( + onCodeReady: (verificationCode) => context + .read() + .submitVerificationCode(verificationCode), + ), + const SizedBox(height: 24), + TextButton( + onPressed: () async => + context.read().resendVerificationCode(), + child: const Text( + "Resend verification code", + style: TextStyle( + color: AppColors.primaryColor, + decoration: TextDecoration.underline, + ), + ), + ), + const SizedBox(height: 200), + ], + ), + ), + ); + } +} diff --git a/recipients_app/lib/view/widgets/welcome/phone_input.dart b/recipients_app/lib/view/widgets/welcome/phone_input.dart deleted file mode 100644 index 71fd796db..000000000 --- a/recipients_app/lib/view/widgets/welcome/phone_input.dart +++ /dev/null @@ -1,99 +0,0 @@ -import "package:app/core/cubits/signup/signup_cubit.dart"; -import "package:app/ui/buttons/buttons.dart"; -import "package:app/ui/configs/app_colors.dart"; -import "package:flutter/material.dart"; -import "package:flutter_bloc/flutter_bloc.dart"; -import "package:intl_phone_number_input/intl_phone_number_input.dart"; -import "package:rounded_loading_button/rounded_loading_button.dart"; - -class PhoneInput extends StatefulWidget { - const PhoneInput({super.key}); - - @override - State createState() => _PhoneInputState(); -} - -class _PhoneInputState extends State { - late final RoundedLoadingButtonController btnController; - late final TextEditingController phoneNumberController; - late PhoneNumber number; - - @override - void initState() { - super.initState(); - btnController = RoundedLoadingButtonController(); - phoneNumberController = TextEditingController(); - number = PhoneNumber(isoCode: "SL"); - } - - @override - void dispose() { - phoneNumberController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isLoading = context.watch().state.status == - SignupStatus.loadingPhoneNumber; - - return Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const SizedBox(height: 16), - InternationalPhoneNumberInput( - ignoreBlank: true, - textFieldController: phoneNumberController, - initialValue: number, - selectorConfig: const SelectorConfig( - selectorType: PhoneInputSelectorType.DIALOG, - ), - keyboardType: TextInputType.phone, - selectorTextStyle: const TextStyle( - color: AppColors.primaryColor, - fontSize: 20, - ), - inputDecoration: const InputDecoration( - labelText: "Orange Money Number", - labelStyle: TextStyle( - color: AppColors.primaryColor, - fontSize: 16, - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: AppColors.primaryColor), - ), - ), - textStyle: const TextStyle( - color: AppColors.primaryColor, - fontSize: 20, - ), - inputBorder: const OutlineInputBorder( - borderSide: BorderSide( - width: 200.0, - color: Colors.white, - ), - ), - onInputChanged: (PhoneNumber value) => number = value, - ), - const SizedBox(height: 16), - ButtonBig( - isLoading: isLoading, - onPressed: () { - if (number.phoneNumber != null && - number.phoneNumber!.isNotEmpty) { - context.read().signupWithPhoneNumber( - phoneNumber: number.phoneNumber!, - ); - } - }, - label: "Continue", - ), - ], - ), - ], - ); - } -} diff --git a/recipients_app/lib/view/widgets/welcome/phone_input_page.dart b/recipients_app/lib/view/widgets/welcome/phone_input_page.dart new file mode 100644 index 000000000..d2f8d76ff --- /dev/null +++ b/recipients_app/lib/view/widgets/welcome/phone_input_page.dart @@ -0,0 +1,150 @@ +import "package:app/core/cubits/signup/signup_cubit.dart"; +import "package:app/ui/buttons/buttons.dart"; +import "package:app/ui/configs/app_colors.dart"; +import "package:flutter/material.dart"; +import "package:flutter_bloc/flutter_bloc.dart"; +import "package:intl_phone_number_input/intl_phone_number_input.dart"; +import "package:rounded_loading_button/rounded_loading_button.dart"; + +class PhoneInputPage extends StatefulWidget { + const PhoneInputPage({super.key}); + + @override + State createState() => _PhoneInputPageState(); +} + +class _PhoneInputPageState extends State { + late final RoundedLoadingButtonController btnController; + late final TextEditingController phoneNumberController; + late PhoneNumber number; + + @override + void initState() { + super.initState(); + btnController = RoundedLoadingButtonController(); + phoneNumberController = TextEditingController(); + number = PhoneNumber(isoCode: "SL"); + } + + @override + void dispose() { + phoneNumberController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isLoading = context.watch().state.status == + SignupStatus.loadingPhoneNumber; + + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image( + image: const AssetImage("assets/earth_animation.gif"), + height: MediaQuery.of(context).size.height * 0.3, + ), + const SizedBox(height: 16), + const Text( + "Your mobile phone", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: AppColors.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Stack(children: [ + Positioned( + bottom: 0, + top: 0, + left: 0, + child: Container( + width: 120, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.primaryColor), + ), + child: const SizedBox()), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: InternationalPhoneNumberInput( + ignoreBlank: true, + textFieldController: phoneNumberController, + initialValue: number, + selectorConfig: const SelectorConfig( + selectorType: PhoneInputSelectorType.BOTTOM_SHEET, + useBottomSheetSafeArea: true, + ), + keyboardType: TextInputType.phone, + selectorTextStyle: const TextStyle( + color: AppColors.primaryColor, + fontSize: 18, + ), + inputDecoration: const InputDecoration( + labelText: "Phone number", + labelStyle: TextStyle( + color: AppColors.primaryColor, + fontSize: 18, + ), + enabledBorder: OutlineInputBorder( + borderSide: + BorderSide(color: AppColors.primaryColor), + borderRadius: + BorderRadius.all(Radius.circular(10)), + ), + focusedBorder: OutlineInputBorder( + borderSide: + BorderSide(color: AppColors.primaryColor), + borderRadius: + BorderRadius.all(Radius.circular(10)), + ), + floatingLabelBehavior: FloatingLabelBehavior.never), + textStyle: const TextStyle( + color: AppColors.primaryColor, + fontSize: 20, + ), + onInputChanged: (PhoneNumber value) => number = value, + ), + ), + ]), + const SizedBox(height: 16) + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ButtonBig( + isLoading: isLoading, + onPressed: () { + if (number.phoneNumber != null && + number.phoneNumber!.isNotEmpty) { + context.read().signupWithPhoneNumber( + phoneNumber: number.phoneNumber!, + ); + } + }, + label: "Continue", + ), + ], + ), + const SizedBox(height: 32), + ], + ), + ), + ); + } +} diff --git a/recipients_app/pubspec.lock b/recipients_app/pubspec.lock index d72a4d1a4..2cf4f070c 100644 --- a/recipients_app/pubspec.lock +++ b/recipients_app/pubspec.lock @@ -387,10 +387,11 @@ packages: intl_phone_number_input: dependency: "direct main" description: - name: intl_phone_number_input - sha256: b812c6e3924e56e65b9684076ccd8eb24126abe0c84091d35d75a25e613a1793 - url: "https://pub.dev" - source: hosted + path: "." + ref: safe-area-bottom-sheet + resolved-ref: e02f2c92cd17987c3776d51da1f77a1fa47da277 + url: "https://github.com/MDikkii/intl_phone_number_input.git" + source: git version: "0.7.3+1" io: dependency: transitive diff --git a/recipients_app/pubspec.yaml b/recipients_app/pubspec.yaml index 20bd01041..4b3b12f2c 100644 --- a/recipients_app/pubspec.yaml +++ b/recipients_app/pubspec.yaml @@ -20,7 +20,12 @@ dependencies: sdk: flutter flutter_bloc: ^8.1.2 intl: ^0.17.0 - intl_phone_number_input: ^0.7.3+1 + # use the lib itself after improvements are merged + # https://github.com/natintosh/intl_phone_number_input/pull/400 + intl_phone_number_input: + git: + url: https://github.com/MDikkii/intl_phone_number_input.git + ref: safe-area-bottom-sheet json_annotation: ^4.8.1 json_serializable: ^6.6.2 rounded_loading_button: ^2.1.0