diff --git a/mobile/android/app/src/main/res/drawable-hdpi/android12splash.png b/mobile/android/app/src/main/res/drawable-hdpi/android12splash.png index 803c896e9c..2c08700385 100644 Binary files a/mobile/android/app/src/main/res/drawable-hdpi/android12splash.png and b/mobile/android/app/src/main/res/drawable-hdpi/android12splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/android12splash.png b/mobile/android/app/src/main/res/drawable-mdpi/android12splash.png index 99dc473548..70247e2582 100644 Binary files a/mobile/android/app/src/main/res/drawable-mdpi/android12splash.png and b/mobile/android/app/src/main/res/drawable-mdpi/android12splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-night-hdpi/android12splash.png b/mobile/android/app/src/main/res/drawable-night-hdpi/android12splash.png index 803c896e9c..2c08700385 100644 Binary files a/mobile/android/app/src/main/res/drawable-night-hdpi/android12splash.png and b/mobile/android/app/src/main/res/drawable-night-hdpi/android12splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-night-mdpi/android12splash.png b/mobile/android/app/src/main/res/drawable-night-mdpi/android12splash.png index 99dc473548..70247e2582 100644 Binary files a/mobile/android/app/src/main/res/drawable-night-mdpi/android12splash.png and b/mobile/android/app/src/main/res/drawable-night-mdpi/android12splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-night-xhdpi/android12splash.png b/mobile/android/app/src/main/res/drawable-night-xhdpi/android12splash.png index 19a5018a05..01237ec2c6 100644 Binary files a/mobile/android/app/src/main/res/drawable-night-xhdpi/android12splash.png and b/mobile/android/app/src/main/res/drawable-night-xhdpi/android12splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png b/mobile/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png index cdee3ada0b..0d318e5731 100644 Binary files a/mobile/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png and b/mobile/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png b/mobile/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png index 90b17939ed..d6d4f5d4ad 100644 Binary files a/mobile/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png and b/mobile/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/android12splash.png b/mobile/android/app/src/main/res/drawable-xhdpi/android12splash.png index 19a5018a05..01237ec2c6 100644 Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/android12splash.png and b/mobile/android/app/src/main/res/drawable-xhdpi/android12splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/mobile/android/app/src/main/res/drawable-xxhdpi/android12splash.png index cdee3ada0b..0d318e5731 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/android12splash.png and b/mobile/android/app/src/main/res/drawable-xxhdpi/android12splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/android12splash.png index 90b17939ed..d6d4f5d4ad 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/android12splash.png and b/mobile/android/app/src/main/res/drawable-xxxhdpi/android12splash.png differ diff --git a/mobile/assets/images/airQo-logo2.png b/mobile/assets/images/airQo-logo2.png new file mode 100644 index 0000000000..2307b42aec Binary files /dev/null and b/mobile/assets/images/airQo-logo2.png differ diff --git a/mobile/assets/images/email_link.png b/mobile/assets/images/email_link.png new file mode 100644 index 0000000000..477e570ab1 Binary files /dev/null and b/mobile/assets/images/email_link.png differ diff --git a/mobile/lib/l10n/app_en.arb b/mobile/lib/l10n/app_en.arb index 68a971a451..54f09111d5 100644 --- a/mobile/lib/l10n/app_en.arb +++ b/mobile/lib/l10n/app_en.arb @@ -351,5 +351,18 @@ "languages": "Languages", "dataProvider":"Data Provider: {placeholder}", "reDOLesson": "Re-do lesson", - "reDoQuiz": "Re-do quiz" + "reDoQuiz": "Re-do quiz", + "addYourEmail":"Add your Email", + "yourEmailHasBeenAdded":"Your Email has been added", + "add":"Add", + "skip":"skip", + "oopsSomethingWentWrongPleaseTryAgainLater":"Oops something went wrong! Please try again later", + "tryAgainLater":"Try again later", + "yourEmailIsAlreadyRegistered":"Your Email is already registered", + "weAreShufflingThingsAroundForYou":"We are shuffling things around for you.....", + "youWouldBeRequiredToAddYourEmailToYourProfileOnTheMobileAppToEnableYouAccessThe" :" You would be required to add your email to your profile on the mobile app to enable you access the ", + "withOneAccount":" with one account.", + "addMyEmail":"Add my email", + "remindMeLater":"Remind me later" + } \ No newline at end of file diff --git a/mobile/lib/l10n/app_fr.arb b/mobile/lib/l10n/app_fr.arb index a38f17a27d..8dee6fe7cb 100644 --- a/mobile/lib/l10n/app_fr.arb +++ b/mobile/lib/l10n/app_fr.arb @@ -341,5 +341,17 @@ "languages": "Langues", "dataProvider":"Fournisseur de données: {placeholder}", "reDOLesson": "Refaire la leçon", - "reDoQuiz": "Refaire le quiz" - } + "reDoQuiz": "Refaire le quiz", + "addYourEmail":"Ajoutez votre e-mail", + "yourEmailHasBeenAdded":"Votre e-mail a été ajouté", + "add":"ajouter", + "skip":"sauter", + "oopsSomethingWentWrongPleaseTryAgainLater":"Oups quelque chose a mal tourné! Veuillez réessayer plus tard", + "tryAgainLater":"réessayez plus tard", + "yourEmailIsAlreadyRegistered":"Votre e-mail est déjà enregistré", + "weAreShufflingThingsAroundForYou":"Nous mélangeons des choses pour vous .....", + "youWouldBeRequiredToAddYourEmailToYourProfileOnTheMobileAppToEnableYouAccessThe" : " Vous devrez ajouter votre e-mail à votre profil sur l'application mobile pour vous permettre d'accéder au ", + "withOneAccount":" avec un compte.", + "addMyEmail":"ajouter mon e-mail", + "remindMeLater":"Rappelez-moi plus tard" +} diff --git a/mobile/lib/l10n/app_lg.arb b/mobile/lib/l10n/app_lg.arb index 42083efb09..855a709e43 100644 --- a/mobile/lib/l10n/app_lg.arb +++ b/mobile/lib/l10n/app_lg.arb @@ -343,5 +343,18 @@ "languages": "Ennimi", "dataProvider":"Ensibuko yo'bubaka: {placeholder}", "reDOLesson": "Ddiŋŋana essomo", - "reDoQuiz": "Ddiŋŋana Ekibuuzo" -} \ No newline at end of file + "reDoQuiz": "Ddiŋŋana Ekibuuzo", + "addYourEmail":"Yongerako Email yo", + "yourEmailHasBeenAdded":"Email yo eyongezeddwayo", + "add":"Okwongerako", + "skip":"simbula", + "oopsSomethingWentWrongPleaseTryAgainLater":"Oops waliwo ekikyamu! Nsaba oddemu ogezeeko oluvannyuma", + "tryAgainLater":"Ddamu gezaako oluvannyuma", + "yourEmailIsAlreadyRegistered":"Email yo yawandiisibwa dda", + "weAreShufflingThingsAroundForYou":"Tutabula ebintu ku lulwo.....", + "youWouldBeRequiredToAddYourEmailToYourProfileOnTheMobileAppToEnableYouAccessThe":" Wandibadde weetaaga okwongera email yo ku profile yo ku app y'essimu okukusobozesa okuyingira mu ", + "withOneAccount":" nga erina akawunti emu.", + "addMyEmail":"Yongerako email yange", + "remindMeLater":"Nzijukiza oluvannyuma" + +} diff --git a/mobile/lib/l10n/app_pt.arb b/mobile/lib/l10n/app_pt.arb index ab0e2a7a68..b4888bd304 100644 --- a/mobile/lib/l10n/app_pt.arb +++ b/mobile/lib/l10n/app_pt.arb @@ -341,5 +341,17 @@ "languageChangedSuccessfully": "Idioma alterado com sucesso {placeholder}", "ok": "OK", "languages": "Idiomas", - "dataProvider":"Fornecedor de dados: {placeholder}" + "dataProvider":"Fornecedor de dados: {placeholder}", + "addYourEmail":"Adicione seu e-mail", + "yourEmailHasBeenAdded":"Seu e-mail foi adicionado", + "add":"Adicionar", + "skip":"pular", + "oopsSomethingWentWrongPleaseTryAgainLater":"Opa, algo deu errado! Tente novamente mais tarde", + "tryAgainLater":"Tente novamente mais tarde", + "yourEmailIsAlreadyRegistered":"Seu e-mail já está cadastrado", + "weAreShufflingThingsAroundForYou":"Estamos embaralhando as coisas para você.....", + "youWouldBeRequiredToAddYourEmailToYourProfileOnTheMobileAppToEnableYouAccessThe":" Você deverá adicionar seu e-mail ao seu perfil no aplicativo móvel para permitir o acesso ao ", + "withOneAccount":" com uma conta.", + "addMyEmail":"Adicionar meu e-mail", + "remindMeLater":"Lembre-me mais tarde" } \ No newline at end of file diff --git a/mobile/lib/l10n/app_sw.arb b/mobile/lib/l10n/app_sw.arb index 790bac0328..7e35bf05f2 100644 --- a/mobile/lib/l10n/app_sw.arb +++ b/mobile/lib/l10n/app_sw.arb @@ -342,5 +342,17 @@ "dataProvider":"Mtoa Huduma: {placeholder}", "reDOLesson": "Fanya tena somo", "reDoQuiz": "Fanya tena mtihani", - "continueLearning" : "Endelea kujifunza" + "continueLearning" : "Endelea kujifunza", + "addYourEmail":"Ongeza Barua pepe yako", + "yourEmailHasBeenAdded":"Barua pepe yako imeongezwa", + "add":"Ongeza", + "skip":"ruka", + "oopsSomethingWentWrongPleaseTryAgainLater":"Lo kuna hitilafu! Tafadhali jaribu tena baadaye", + "tryAgainLater":"Jaribu tena baadaye", + "yourEmailIsAlreadyRegistered":"Barua pepe yako tayari imesajiliwa", + "weAreShufflingThingsAroundForYou":"Tunachanganya mambo kwa ajili yako....", + "youWouldBeRequiredToAddYourEmailToYourProfileOnTheMobileAppToEnableYouAccessThe":" Utahitajika kuongeza barua pepe yako kwenye wasifu wako kwenye programu ya simu ili kukuwezesha kufikia ", + "withOneAccount":" na akaunti moja.", + "addMyEmail":"Ongeza barua pepe yangu", + "remindMeLater":"Nikumbushe baadaye" } \ No newline at end of file diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 5684f99797..1d31d2914b 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -59,8 +59,7 @@ void main() async { ); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); - FirebaseMessaging.onMessage.listen((RemoteMessage message) { - }); + FirebaseMessaging.onMessage.listen((RemoteMessage message) {}); runApp(configuredApp); } catch (exception, stackTrace) { @@ -69,14 +68,13 @@ void main() async { title: 'AirQo', theme: customTheme(), localizationsDelegates: const [ + LgMaterialLocalizations.delegate, + LgCupertinoLocalizations.delegate, + LgWidgetsLocalizations.delegate, AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, - LgMaterialLocalizations.delegate, - LgCupertinoLocalizations.delegate, - LgWidgetsLocalizations.delegate, - ], supportedLocales: const [ Locale('en'), // English diff --git a/mobile/lib/screens/email_authentication/email_verification_screen.dart b/mobile/lib/screens/email_authentication/email_verification_screen.dart index 42610519b0..a106aa25d1 100644 --- a/mobile/lib/screens/email_authentication/email_verification_screen.dart +++ b/mobile/lib/screens/email_authentication/email_verification_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'package:app/blocs/blocs.dart'; import 'package:app/models/enum_constants.dart'; import 'package:app/screens/email_authentication/email_auth_widgets.dart'; @@ -6,6 +8,7 @@ import 'package:app/themes/theme.dart'; import 'package:app/utils/utils.dart'; import 'package:app/widgets/widgets.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -208,39 +211,80 @@ class _EmailAuthVerificationWidgetState ); } + Future linkAccounts() async { + final emailAuthModel = + context.read().state.emailAuthModel; + + final emailCredential = EmailAuthProvider.credentialWithLink( + emailLink: emailAuthModel.signInLink, + email: emailAuthModel.emailAddress, + ); + + final user = FirebaseAuth.instance.currentUser; + + try { + if (user != null) { + await user.linkWithCredential(emailCredential); + context.read().add( + const SetEmailVerificationStatus( + AuthenticationStatus.success, + ), + ); + await AirqoApiClient().syncPlatformAccount(); + } + } catch (error) { + context.read().add( + const SetEmailVerificationStatus( + AuthenticationStatus.error, + ), + ); + if (kDebugMode) { + print('Error linking accounts: $error'); + } + } + } + Future _authenticate() async { loadingScreen(context); final emailAuthModel = context.read().state.emailAuthModel; - final emailCredential = EmailAuthProvider.credentialWithLink( emailLink: emailAuthModel.signInLink, email: emailAuthModel.emailAddress, ); try { - final bool authenticationSuccessful = - await CustomAuth.firebaseSignIn(emailCredential); - if (!mounted) return; + final currentUser = FirebaseAuth.instance.currentUser; + if (currentUser != null) { + // Link the email credential with the existing user + await currentUser.linkWithCredential(emailCredential); + } else { + // Perform email authentication if not signed in with phone number + final bool authenticationSuccessful = + await CustomAuth.firebaseSignIn(emailCredential); + if (!mounted) return; - Navigator.pop(context); + if (!authenticationSuccessful) { + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext _) { + return const AuthFailureDialog(); + }, + ); + } + } - if (authenticationSuccessful) { - context - .read() - .add(const SetEmailVerificationStatus( - AuthenticationStatus.success, - )); + context + .read() + .add(const SetEmailVerificationStatus( + AuthenticationStatus.success, + )); + + // Check if account linking was done, skip postSignInActions + if (currentUser == null) { await AppService.postSignInActions(context); - } else { - await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext _) { - return const AuthFailureDialog(); - }, - ); } } catch (exception, stackTrace) { Navigator.pop(context); diff --git a/mobile/lib/screens/email_link/confirm_account_details.dart b/mobile/lib/screens/email_link/confirm_account_details.dart new file mode 100644 index 0000000000..55d91d978c --- /dev/null +++ b/mobile/lib/screens/email_link/confirm_account_details.dart @@ -0,0 +1,311 @@ +import 'dart:async'; + +import 'package:app/blocs/blocs.dart'; +import 'package:app/models/models.dart'; +import 'package:app/screens/email_link/email_link_widgets.dart'; +import 'package:app/screens/home_page.dart'; +import 'package:app/screens/offline_banner.dart'; +import 'package:app/services/rest_api.dart'; +import 'package:app/themes/theme.dart'; +import 'package:app/utils/utils.dart'; +import 'package:app/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../on_boarding/on_boarding_widgets.dart'; +import '../email_authentication/email_verification_screen.dart'; + +class _EmailVerifyWidget extends StatefulWidget { + const _EmailVerifyWidget({ + super.key, + required this.authProcedure, + }); + + final AuthProcedure authProcedure; + + @override + _EmailAuthWidgetState createState() => _EmailAuthWidgetState(); +} + +class _EmailAuthWidgetState extends State { + DateTime? _exitTime; + bool _keyboardVisible = false; + final _formKey = GlobalKey(); + String emailAddress = ""; + final AirqoApiClient apiClient = AirqoApiClient(); + + @override + void initState() { + super.initState(); + context.read().add(InitializeEmailAuth( + authProcedure: widget.authProcedure, + )); + } + + @override + void dispose() { + _formKey.currentState?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + _keyboardVisible = MediaQuery.of(context).viewInsets.bottom != 0; + + return OfflineBanner( + child: Scaffold( + appBar: const OnBoardingTopBar(backgroundColor: Colors.white), + body: PopScope( + onPopInvoked: ((didPop) { + if (didPop) { + onWillPop(); + } + }), + child: AppSafeArea( + backgroundColor: Colors.white, + horizontalPadding: 24, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const EmailLinkTitle(), + const EmailLinkSubTitle(), + const SizedBox(height: 32), + Form( + key: _formKey, + child: SizedBox( + height: 48, + child: BlocBuilder( + buildWhen: (previous, current) { + return previous.status != current.status; + }, + builder: (context, state) { + return TextFormField( + validator: (value) { + if (value == null || !value.isValidEmail()) { + return AppLocalizations.of(context)! + .pleaseEnterAValidEmail; + } + + return null; + }, + onChanged: (value) { + setState(() => emailAddress = value); + }, + onSaved: (value) { + setState(() => emailAddress = value!); + }, + style: inputTextStyle(state.status), + enableSuggestions: true, + cursorWidth: 1, + autofocus: false, + enabled: state.status != AuthenticationStatus.success, + keyboardType: TextInputType.emailAddress, + decoration: inputDecoration( + state.status, + hintText: 'me@company.com', + suffixIconCallback: () { + _formKey.currentState?.reset(); + FocusScope.of(context).requestFocus(FocusNode()); + }, + ), + ); + }, + ), + ), + ), + const EmailLinkErrorMessage(), + const Spacer(), + NextButton( + buttonColor: emailAddress.isValidEmail() + ? CustomColors.appColorBlue + : CustomColors.appColorDisabled, + callBack: () async { + FocusScope.of(context).requestFocus(FocusNode()); + + switch (context.read().state.status) { + case AuthenticationStatus.initial: + case AuthenticationStatus.error: + FormState? formState = _formKey.currentState; + if (formState == null) { + return; + } + + if (formState.validate()) { + formState.save(); + await _sendAuthCode(); + } + break; + case AuthenticationStatus.success: + await verifyEmailAuthCode(context); + break; + } + }, + ), + const SizedBox( + height: 16, + ), + Visibility( + visible: !_keyboardVisible, + child: const SkipLinkButtons(), + ), + ], + ), + ), + ), + ), + ); + } + + Future _sendAuthCode() async { + context.read().add(const SetEmailAuthStatus( + AuthenticationStatus.initial, + )); + + final hasConnection = await hasNetworkConnection(); + + if (!mounted) { + return; + } + + if (!hasConnection) { + context.read().add(SetEmailAuthStatus( + AuthenticationStatus.error, + errorMessage: + AppLocalizations.of(context)!.checkYourInternetConnection, + )); + + return; + } + + final confirmation = await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AuthMethodDialog( + credentials: emailAddress, + authMethod: AuthMethod.email, + ); + }, + ); + + if (confirmation == null || + confirmation == ConfirmationAction.cancel || + !mounted) { + return; + } + + loadingScreen(context); + + final bool? exists = await apiClient.checkIfUserExists( + emailAddress: emailAddress, + ); + + if (!mounted) { + return; + } + + if (exists == null) { + Navigator.pop(context); + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext _) { + return const LinkFailureDialog(); + }, + ); + + return; + } + + AuthProcedure authProcedure = + context.read().state.authProcedure; + if (!exists && authProcedure == AuthProcedure.login) { + Navigator.pop(context); + context.read().add(SetEmailAuthStatus( + AuthenticationStatus.error, + errorMessage: + AppLocalizations.of(context)!.emailNotFoundDidYouSignUp, + )); + + return; + } + + if (exists && authProcedure == AuthProcedure.signup) { + Navigator.pop(context); + context.read().add(SetEmailAuthStatus( + AuthenticationStatus.error, + errorMessage: + AppLocalizations.of(context)!.emailAlreadyRegisteredPleaseLogIn, + )); + + return; + } + + EmailAuthModel? emailAuthModel = + await apiClient.sendEmailVerificationCode(emailAddress); + + if (!mounted) { + return; + } + Navigator.pop(context); + + if (emailAuthModel == null) { + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext _) { + return const LinkFailureDialog(); + }, + ); + + return; + } + + context + .read() + .add(const SetEmailAuthStatus(AuthenticationStatus.success)); + context.read().add(InitializeEmailVerification( + emailAuthModel: emailAuthModel, + authProcedure: context.read().state.authProcedure, + )); + } + + Future onWillPop() { + final now = DateTime.now(); + + if (_exitTime == null || + now.difference(_exitTime!) > const Duration(seconds: 2)) { + _exitTime = now; + + showSnackBar( + context, + AppLocalizations.of(context)!.tapAgainToCancel, + ); + + return Future.value(false); + } + + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) { + return const HomePage(); + }), + (r) => false, + ); + + return Future.value(false); + } +} + +class EmailLinkScreen extends _EmailVerifyWidget { + const EmailLinkScreen({super.key}) + : super(authProcedure: AuthProcedure.signup); + + @override + EmailLinkScreenWidgetState createState() => EmailLinkScreenWidgetState(); +} + +class EmailLinkScreenWidgetState + extends _EmailAuthWidgetState {} diff --git a/mobile/lib/screens/email_link/email_link_page.dart b/mobile/lib/screens/email_link/email_link_page.dart new file mode 100644 index 0000000000..ee5878027e --- /dev/null +++ b/mobile/lib/screens/email_link/email_link_page.dart @@ -0,0 +1,182 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:app/screens/email_link/confirm_account_details.dart'; +import 'package:app/screens/email_link/email_link_widgets.dart'; +import 'package:app/screens/quiz/quiz_view.dart'; +import 'package:app/themes/colors.dart'; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:app/themes/theme.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +Future bottomSheetEmailLink(BuildContext context) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + bool shouldShowBottomSheet = true; + int remindMeLaterTimestamp = prefs.getInt('remindMeLaterTimestamp') ?? 0; + + if (remindMeLaterTimestamp > 0) { + DateTime lastRemindTimestamp = + DateTime.fromMillisecondsSinceEpoch(remindMeLaterTimestamp); + DateTime now = DateTime.now(); + if (now.difference(lastRemindTimestamp).inDays <= 1) { + shouldShowBottomSheet = false; + } + } + if (shouldShowBottomSheet) { + return showModalBottomSheet( + isScrollControlled: true, + enableDrag: false, + elevation: 1, + transitionAnimationController: bottomSheetTransition(context), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + isDismissible: false, + context: context, + builder: (context) { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.9, + child: AnimatedPadding( + duration: const Duration(milliseconds: 500), + curve: Curves.easeIn, + padding: const EdgeInsets.fromLTRB(0, 2, 0, 10), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 3), + SizedBox( + height: 215, + width: 373, + child: AnimatedPadding( + duration: const Duration(milliseconds: 500), + curve: Curves.easeIn, + padding: const EdgeInsets.fromLTRB(4, 4, 4, 10), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.asset( + 'assets/images/email_link.png', + fit: BoxFit.cover, + height: double.infinity, + width: double.infinity, + alignment: Alignment.center, + ), + ), + ), + ), + const SizedBox(height: 10), + AnimatedPadding( + duration: const Duration(milliseconds: 500), + curve: Curves.easeIn, + padding: const EdgeInsets.fromLTRB(30, 0, 30, 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 295.53, + height: 67, + child: AutoSizeText( + AppLocalizations.of(context)!.weAreShufflingThingsAroundForYou, + textAlign: TextAlign.center, + style: TextStyle( + color: CustomColors.quizColorBlack, + fontWeight: FontWeight.w700, + fontSize: 19.58, + ), + ), + ), + const SizedBox(height: 10), + SizedBox( + width: 271, + height: 67, + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text:AppLocalizations.of(context)!.youWouldBeRequiredToAddYourEmailToYourProfileOnTheMobileAppToEnableYouAccessThe, + style: const TextStyle( + color: Color(0xFF485972), + fontSize: 13.24, + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: "AirQo analytics", + style: TextStyle( + color: CustomColors.appColorBlue, + fontSize: 13.24, + fontWeight: FontWeight.w400, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () async { + Uri url = + Uri.parse('https://platform.airqo.net'); + if (await canLaunchUrl(url)) { + await launchUrl( + url, + mode: LaunchMode.inAppBrowserView, + ); + } else { + throw 'Could not launch $url'; + } + }, + ), + TextSpan( + text: AppLocalizations.of(context)!.withOneAccount, + style: const TextStyle( + color: Color(0xFF485972), + fontSize: 13.24, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + InkWell( + onTap: () async { + Navigator.push(context, MaterialPageRoute( + builder: (context) { + return const EmailLinkScreen(); + }, + )); + }, + child: EmailLinkActionButton( + text: AppLocalizations.of(context)!.addMyEmail, + ), + ), + const SizedBox(height: 10), + InkWell( + onTap: () async { + Navigator.pop(context, false); + prefs.setInt( + 'remindMeLaterTimestamp', + DateTime.now().millisecondsSinceEpoch, + ); + }, + child: EmailLinkSkipButton( + text: AppLocalizations.of(context)!.remindMeLater, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/mobile/lib/screens/email_link/email_link_widgets.dart b/mobile/lib/screens/email_link/email_link_widgets.dart new file mode 100644 index 0000000000..afba462c60 --- /dev/null +++ b/mobile/lib/screens/email_link/email_link_widgets.dart @@ -0,0 +1,321 @@ +import 'package:app/blocs/email_auth/email_auth_bloc.dart'; +import 'package:app/models/enum_constants.dart'; +import 'package:app/screens/home_page.dart'; +import 'package:app/themes/app_theme.dart'; +import 'package:app/themes/colors.dart'; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class EmailLinkActionButton extends StatelessWidget { + const EmailLinkActionButton({ + super.key, + required this.text, + }); + final String text; + + @override + Widget build(BuildContext context) { + return Container( + height: 39.94, + width: 159.66, + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: CustomColors.appColorBlue, + borderRadius: const BorderRadius.all( + Radius.circular(8.0), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + text, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + letterSpacing: 16 * -0.022, + ), + ), + const SizedBox( + width: 6, + ), + ], + ), + ); + } +} + +class EmailLinkSkipButton extends StatelessWidget { + const EmailLinkSkipButton({ + super.key, + required this.text, + }); + final String text; + + @override + Widget build(BuildContext context) { + return IntrinsicWidth( + child: Container( + height: 39.94, + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: const BoxDecoration( + color: Color.fromARGB(0, 0, 0, 0), + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + text, + style: const TextStyle( + color: Color.fromARGB(197, 0, 0, 0), + fontSize: 14, + letterSpacing: 16 * -0.022, + ), + ), + const SizedBox( + width: 6, + ), + ], + ), + ), + ); + } +} + +class EmailLinkErrorMessage extends StatelessWidget { + const EmailLinkErrorMessage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.errorMessage.isEmpty) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 16), + child: SizedBox( + child: Row( + children: [ + SvgPicture.asset( + 'assets/icon/error_info_icon.svg', + ), + const SizedBox( + width: 5, + ), + Expanded( + child: AutoSizeText( + 'Your Email is already registered ', + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: CustomColors.appColorInvalid, + fontSize: 14, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +class EmailLinkTitle extends StatelessWidget { + const EmailLinkTitle({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + String message; + switch (state.status) { + case AuthenticationStatus.initial: + message = AppLocalizations.of(context)!.addYourEmail; + break; + case AuthenticationStatus.error: + message = + AppLocalizations.of(context)!.oopsSomethingsWrongWithYourEmail; + break; + case AuthenticationStatus.success: + message = AppLocalizations.of(context)!.success; + break; + } + + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 10), + child: AutoSizeText( + message, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: CustomTextStyle.headline7(context), + ), + ); + }, + ); + } +} + +class SkipLinkButtons extends StatelessWidget { + const SkipLinkButtons({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.status == AuthenticationStatus.success || + context.read().state.authProcedure != + AuthProcedure.signup) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + children: [ + const SizedBox(height: 8), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const HomePage(), + ), + ); + }, + child: Text( + AppLocalizations.of(context)!.skip, + style: const TextStyle( + color: Colors.blue, // Customize the color as needed + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class EmailLinkSubTitle extends StatelessWidget { + const EmailLinkSubTitle({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + String message; + switch (state.status) { + case AuthenticationStatus.error: + return const SizedBox.shrink(); + case AuthenticationStatus.initial: + message = + AppLocalizations.of(context)!.wellSendYouAVerificationCode; + + break; + case AuthenticationStatus.success: + message = "Your Email has been added"; + break; + } + + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 8), + child: AutoSizeText( + message, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: CustomColors.appColorBlack.withOpacity(0.6), + ), + ), + ); + }, + ); + } +} + +class LinkAuthButtons extends StatelessWidget { + const LinkAuthButtons({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.status == AuthenticationStatus.success) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: context.read().state.authProcedure == + AuthProcedure.signup + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + EmailLinkActionButton( + text: AppLocalizations.of(context)!.add, + ), + const SizedBox( + width: 12, + ), + EmailLinkSkipButton( + text: AppLocalizations.of(context)!.skip, + ), + ], + ) + : const SizedBox.shrink(), + ); + }, + ); + } +} + +class LinkFailureDialog extends StatelessWidget { + const LinkFailureDialog({super.key}); + + @override + Widget build(BuildContext context) { + return CupertinoAlertDialog( + title: Text( + AppLocalizations.of(context)!.oopsSomethingWentWrongPleaseTryAgainLater, + textAlign: TextAlign.center, + style: CustomTextStyle.headline8(context), + ), + actions: [ + CupertinoDialogAction( + onPressed: () async { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const HomePage(), + ), + ); + }, + isDefaultAction: true, + isDestructiveAction: false, + child: Text( + AppLocalizations.of(context)!.tryAgainLater, + style: CustomTextStyle.button2(context) + ?.copyWith(color: CustomColors.appColorBlue), + ), + ), + ], + ); + } +} diff --git a/mobile/lib/screens/email_link/link_verification_screen.dart b/mobile/lib/screens/email_link/link_verification_screen.dart new file mode 100644 index 0000000000..a33e514964 --- /dev/null +++ b/mobile/lib/screens/email_link/link_verification_screen.dart @@ -0,0 +1,313 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:app/blocs/blocs.dart'; +import 'package:app/models/enum_constants.dart'; +import 'package:app/screens/dashboard/dashboard_view.dart'; +import 'package:app/screens/email_authentication/email_auth_widgets.dart'; +import 'package:app/services/services.dart'; +import 'package:app/themes/theme.dart'; +import 'package:app/utils/utils.dart'; +import 'package:app/widgets/widgets.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../widgets/auth_widgets.dart'; +import '../on_boarding/on_boarding_widgets.dart'; + +Future verifyLinkAuthCode(BuildContext context) async { + await Navigator.of(context).push( + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => + const _EmailAuthVerificationWidget(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(0.0, 1.0); + const end = Offset.zero; + const curve = Curves.ease; + + var tween = Tween( + begin: begin, + end: end, + ).chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + ), + ); +} + +class _EmailAuthVerificationWidget extends StatefulWidget { + const _EmailAuthVerificationWidget(); + + @override + State<_EmailAuthVerificationWidget> createState() => + _EmailAuthVerificationWidgetState(); +} + +class _EmailAuthVerificationWidgetState + extends State<_EmailAuthVerificationWidget> { + DateTime? _exitTime; + final _formKey = GlobalKey(); + String _inputCode = ''; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const OnBoardingTopBar(backgroundColor: Colors.white), + body: PopScope( + onPopInvoked: ((didPop) { + if (didPop) { + onWillPop(); + } + }), + child: AppSafeArea( + horizontalPadding: 24, + backgroundColor: Colors.white, + child: BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const EmailVerificationTitle(), + const EmailVerificationSubTitle(), + const SizedBox( + height: 20, + ), + Form( + key: _formKey, + child: SizedBox( + height: 64, + child: AnimatedPadding( + duration: const Duration(milliseconds: 500), + curve: Curves.easeIn, + padding: const EdgeInsets.symmetric(horizontal: 36), + child: TextFormField( + validator: (value) { + String? error; + if (value == null) { + error = AppLocalizations.of(context)! + .pleaseEnterTheCode; + } + + if (value != null && value.length < 6) { + error = AppLocalizations.of(context)! + .pleaseEnterAllTheDigits; + } + + if (value != + state.emailAuthModel.token.toString()) { + error = AppLocalizations.of(context)! + .pleaseEnterAllTheDigits; + } + + if (error != null) { + context + .read() + .add(const SetEmailVerificationStatus( + AuthenticationStatus.error, + )); + } + + return error; + }, + onChanged: (value) { + setState(() => _inputCode = value); + }, + showCursor: state.codeCountDown <= 0 && + state.status != AuthenticationStatus.success, + enabled: state.codeCountDown <= 0 && + state.status != AuthenticationStatus.success, + textAlign: TextAlign.center, + maxLength: 6, + cursorWidth: 1, + keyboardType: TextInputType.number, + style: inputTextStyle( + state.status, + optField: true, + ), + decoration: optInputDecoration( + state.status, + codeSent: state.codeCountDown <= 0, + ), + ), + ), + ), + ), + const EmailVerificationCodeCountDown(), + Visibility( + visible: state.status != AuthenticationStatus.success, + child: const AuthOrSeparator(), + ), + Visibility( + visible: state.status != AuthenticationStatus.success, + child: const ChangeAuthCredentials(AuthMethod.email), + ), + const Spacer(), + Visibility( + visible: state.status == AuthenticationStatus.success, + child: const AuthSuccessWidget(), + ), + const Spacer(), + NextButton( + buttonColor: _inputCode.length >= 6 + ? CustomColors.appColorBlue + : CustomColors.appColorDisabled, + callBack: () async { + switch (state.status) { + case AuthenticationStatus.success: + break; + case AuthenticationStatus.error: + case AuthenticationStatus.initial: + if (_inputCode.length < 6) { + return; + } + FormState? formState = _formKey.currentState; + if (formState == null) { + return; + } + if (formState.validate()) { + await _authenticate(); + } + return; + } + + if (!mounted) return; + + if (state.authProcedure == AuthProcedure.login) { + await Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) { + return const DashboardView(); + }), + (r) => false, + ); + } else { + await Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) { + return const DashboardView(); + }), + (r) => false, + ); + } + }, + ), + const SizedBox( + height: 12, + ), + ], + ); + }, + ), + ), + ), + ); + } + + Future linkAccounts() async { + final emailAuthModel = + context.read().state.emailAuthModel; + + final emailCredential = EmailAuthProvider.credentialWithLink( + emailLink: emailAuthModel.signInLink, + email: emailAuthModel.emailAddress, + ); + + final user = CustomAuth.getUser(); + + try { + if (user != null) { + await user.linkWithCredential(emailCredential); + context.read().add( + const SetEmailVerificationStatus( + AuthenticationStatus.success, + ), + ); + await AirqoApiClient().syncPlatformAccount(); + } + } catch (error) { + context.read().add( + const SetEmailVerificationStatus( + AuthenticationStatus.error, + ), + ); + if (kDebugMode) { + print('Error linking accounts: $error'); + } + } + } + + Future _authenticate() async { + loadingScreen(context); + + final emailAuthModel = + context.read().state.emailAuthModel; + final emailCredential = EmailAuthProvider.credentialWithLink( + emailLink: emailAuthModel.signInLink, + email: emailAuthModel.emailAddress, + ); + + try { + final currentUser = CustomAuth.getUser(); + if (currentUser != null) { + await currentUser.linkWithCredential(emailCredential); + } else { + final bool authenticationSuccessful = + await CustomAuth.firebaseSignIn(emailCredential); + if (!mounted) return; + + if (!authenticationSuccessful) { + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext _) { + return const AuthFailureDialog(); + }, + ); + } + } + + context + .read() + .add(const SetEmailVerificationStatus( + AuthenticationStatus.success, + )); + } catch (exception, stackTrace) { + Navigator.pop(context); + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext _) { + return const AuthFailureDialog(); + }, + ); + await logException(exception, stackTrace); + } + } + + Future onWillPop() { + final now = DateTime.now(); + + if (_exitTime == null || + now.difference(_exitTime!) > const Duration(seconds: 2)) { + _exitTime = now; + + showSnackBar( + context, + AppLocalizations.of(context)!.tapAgainToCancel, + ); + + return Future.value(false); + } + + Navigator.pop(context, false); + + return Future.value(false); + } +} diff --git a/mobile/lib/screens/home_page.dart b/mobile/lib/screens/home_page.dart index d8cd84e10b..890a81d0e1 100644 --- a/mobile/lib/screens/home_page.dart +++ b/mobile/lib/screens/home_page.dart @@ -4,6 +4,7 @@ import 'package:animations/animations.dart'; import 'package:app/blocs/blocs.dart'; import 'package:app/constants/config.dart'; import 'package:app/models/models.dart'; +import 'package:app/screens/email_link/email_link_page.dart'; import 'package:app/screens/profile/profile_view.dart'; import 'package:app/screens/settings/update_screen.dart'; import 'package:app/services/services.dart'; @@ -241,6 +242,24 @@ class _HomePageState extends State { }); } }); + WidgetsBinding.instance.addPostFrameCallback((_) async { + final user = CustomAuth.getUser(); + if ((user != null && user.phoneNumber != null)) { + if (user.email != null) { + return; + } else if (user.isAnonymous) { + return; + } + await Future.delayed(const Duration(seconds: 2)); + if (mounted) { + await bottomSheetEmailLink(context); + } + } + }); + } + + Future showEmailLinkBottomSheet(BuildContext context) async { + await bottomSheetEmailLink(context); } Future _initializeDynamicLinks() async { diff --git a/mobile/lib/screens/profile/profile_edit_page.dart b/mobile/lib/screens/profile/profile_edit_page.dart index ffb0a05554..a242918b99 100644 --- a/mobile/lib/screens/profile/profile_edit_page.dart +++ b/mobile/lib/screens/profile/profile_edit_page.dart @@ -1,5 +1,6 @@ import 'package:app/blocs/blocs.dart'; import 'package:app/models/models.dart'; +import 'package:app/screens/email_link/confirm_account_details.dart'; import 'package:app/themes/theme.dart'; import 'package:app/widgets/widgets.dart'; import 'package:flutter/material.dart'; @@ -21,6 +22,9 @@ class ProfileEditPage extends StatelessWidget { horizontalPadding: 16, child: BlocBuilder( builder: (context, profile) { + bool isAccountLinked = profile.phoneNumber.isNotEmpty && + profile.emailAddress.isNotEmpty; + return ListView( physics: const BouncingScrollPhysics(), children: [ @@ -32,14 +36,72 @@ class ProfileEditPage extends StatelessWidget { height: 40, ), Visibility( - visible: profile.phoneNumber.isNotEmpty, - child: EditCredentialsField( - profile: profile, - authMethod: AuthMethod.phone, + visible: true, // Always show the phone number field + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + EditCredentialsField( + profile: profile, + authMethod: AuthMethod.phone, + ), + const SizedBox(height: 16), + Visibility( + visible: !isAccountLinked, // Show only if not linked + child: GestureDetector( + onTap: () { + // Navigate to EmailVerificationScreen + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const EmailLinkScreen(), + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Email Address', + style: TextStyle( + fontSize: 12, + color: CustomColors.inactiveColor, + ), + ), + const SizedBox(height: 4), + TextFormField( + initialValue: '', + onTap: () { + FocusScope.of(context).unfocus(); + }, + decoration: InputDecoration( + filled: true, + fillColor: Colors.white, + hintText: 'Enter your email address', + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide( + color: Colors.transparent, + width: 1.0), + borderRadius: BorderRadius.circular(8.0), + ), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide( + color: Colors.transparent, + width: 1.0), + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + ], ), ), Visibility( - visible: profile.emailAddress.isNotEmpty, + visible: isAccountLinked, child: EditCredentialsField( profile: profile, authMethod: AuthMethod.email, diff --git a/mobile/lib/screens/profile/profile_widgets.dart b/mobile/lib/screens/profile/profile_widgets.dart index b87d6b43ce..0e3f08ed11 100644 --- a/mobile/lib/screens/profile/profile_widgets.dart +++ b/mobile/lib/screens/profile/profile_widgets.dart @@ -879,14 +879,31 @@ class EditProfileAppBar extends StatelessWidget implements PreferredSizeWidget { Size get preferredSize => const Size.fromHeight(60); } -class EditCredentialsField extends StatelessWidget { +class EditCredentialsField extends StatefulWidget { const EditCredentialsField({ super.key, required this.authMethod, required this.profile, + this.isSaveClicked = false, }); + final AuthMethod authMethod; final Profile profile; + final bool isSaveClicked; + + @override + EditCredentialsFieldState createState() => EditCredentialsFieldState(); +} + +class EditCredentialsFieldState extends State { + late bool isReadOnly; + + @override + void initState() { + super.initState(); + // Set the initial readOnly status based on whether the email is present + isReadOnly = widget.profile.emailAddress.isNotEmpty; + } @override Widget build(BuildContext context) { @@ -895,7 +912,7 @@ class EditCredentialsField extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - authMethod == AuthMethod.email + widget.authMethod == AuthMethod.email ? AppLocalizations.of(context)!.email : AppLocalizations.of(context)!.phoneNumber, style: TextStyle( @@ -907,16 +924,44 @@ class EditCredentialsField extends StatelessWidget { height: 4, ), TextFormField( - initialValue: authMethod == AuthMethod.email - ? profile.emailAddress - : profile.phoneNumber, + initialValue: widget.authMethod == AuthMethod.email + ? widget.profile.emailAddress + : widget.profile.phoneNumber, enableSuggestions: false, - readOnly: true, + readOnly: widget.isSaveClicked || isReadOnly, style: TextStyle(color: CustomColors.inactiveColor), + onChanged: (value) { + if (widget.authMethod == AuthMethod.email && + !widget.isSaveClicked) { + context.read().add( + UpdateProfile( + widget.profile.copyWith(emailAddress: value), + ), + ); + } + }, + onTap: () { + // Allow editing when the user taps the field + if (!widget.isSaveClicked) { + setState(() { + isReadOnly = false; + }); + } + }, decoration: InputDecoration( filled: true, fillColor: Colors.white, hintText: '-', + suffixIcon: isReadOnly + ? null + : Container( + padding: const EdgeInsets.all(10), + height: 20, + width: 20, + child: SvgPicture.asset( + 'assets/icon/profile_edit.svg', + ), + ), focusedBorder: OutlineInputBorder( borderSide: const BorderSide(color: Colors.transparent, width: 1.0), diff --git a/mobile/lib/services/rest_api.dart b/mobile/lib/services/rest_api.dart index b9c795fc1b..1a0dc4461c 100644 --- a/mobile/lib/services/rest_api.dart +++ b/mobile/lib/services/rest_api.dart @@ -10,7 +10,6 @@ import 'package:app/utils/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:http/retry.dart'; -import 'package:intl/intl.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:uuid/uuid.dart'; diff --git a/mobile/lib/utils/custom_localisation.dart b/mobile/lib/utils/custom_localisation.dart index c12f9690f9..51dcc23a5e 100644 --- a/mobile/lib/utils/custom_localisation.dart +++ b/mobile/lib/utils/custom_localisation.dart @@ -107,10 +107,9 @@ const lugandaDateSymbols = { 'Mukulukusa Bitungo Tungo', 'Museenene', 'Ntenvu', - ], 'STANDALONEMONTHS': [ - 'Gatonnya', + 'Gatonnya', 'Mukutula Nsanja', 'Mugula Nsigo', 'Kafumuula Mpawu', @@ -122,7 +121,6 @@ const lugandaDateSymbols = { 'Mukulukusa Bitungo Tungo', 'Museenene', 'Ntenvu', - ], 'SHORTMONTHS': [ 'Jan.', @@ -318,6 +316,8 @@ class LgMaterialLocalizations extends GlobalMaterialLocalizations { required super.twoDigitZeroPaddedFormat, }); + bool shouldReload(_LgMaterialLocalizationsDelegate old) => false; + // #docregion Getters @override String get moreButtonTooltip => r'More'; @@ -797,6 +797,9 @@ class LgCupertinoLocalizationsDelegate extends LocalizationsDelegate { const LgCupertinoLocalizationsDelegate(); + @override + bool shouldReload(LgCupertinoLocalizationsDelegate old) => false; + @override bool isSupported(Locale locale) => locale.languageCode == 'lg'; @@ -804,9 +807,6 @@ class LgCupertinoLocalizationsDelegate Future load(Locale locale) async { return DefaultCupertinoLocalizations.delegate.load(locale); } - - @override - bool shouldReload(LgCupertinoLocalizationsDelegate old) => false; } class LgCupertinoLocalizations extends DefaultCupertinoLocalizations { @@ -821,6 +821,9 @@ class LgWidgetsLocalizationsDelegate extends LocalizationsDelegate { const LgWidgetsLocalizationsDelegate(); + @override + bool shouldReload(LgWidgetsLocalizationsDelegate old) => false; + @override bool isSupported(Locale locale) => locale.languageCode == 'lg'; @@ -828,9 +831,6 @@ class LgWidgetsLocalizationsDelegate Future load(Locale locale) async { return DefaultWidgetsLocalizations.delegate.load(locale); } - - @override - bool shouldReload(LgWidgetsLocalizationsDelegate old) => false; } class LgWidgetsLocalizations extends DefaultWidgetsLocalizations { diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 6ec0955f43..89264384d3 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -123,7 +123,7 @@ flutter_native_splash: # 640 pixels in diameter. # App icon without an icon background: This should be 1152×1152 pixels, and fit within a circle # 768 pixels in diameter. - image: assets/images/airQo-logo.png + image: assets/images/airQo-logo2.png # Splash screen background color. color: "#ffffff" @@ -139,6 +139,6 @@ flutter_native_splash: # parameters from above. # image_dark: assets/images/airQo-logo.png - # color_dark: "#ffffff" + color_dark: "#ffffff" #icon_background_color_dark: "#eeeeee"