From d66c8a2dcd6a52c66c6a20b35cedfc255c312961 Mon Sep 17 00:00:00 2001 From: Matthew Horne Negro Date: Thu, 14 Mar 2024 18:09:27 +0100 Subject: [PATCH 01/19] Add Google sign-in option dummy test --- lib/internet/api/results/classification.dart | 2 + .../api/sign-in/google_sign_in_api.dart | 9 +++ lib/main.dart | 2 + lib/pages/home/home_page.dart | 2 + lib/pages/profile/profile_page.dart | 63 +++++++++++++++++++ lib/pages/sign-in/sign_in_page.dart | 35 +++++++++++ lib/routes.dart | 2 + pubspec.lock | 50 ++++++++++++++- pubspec.yaml | 1 + 9 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 lib/internet/api/sign-in/google_sign_in_api.dart create mode 100644 lib/pages/profile/profile_page.dart create mode 100644 lib/pages/sign-in/sign_in_page.dart diff --git a/lib/internet/api/results/classification.dart b/lib/internet/api/results/classification.dart index f3a76a04..27856dbf 100644 --- a/lib/internet/api/results/classification.dart +++ b/lib/internet/api/results/classification.dart @@ -7,6 +7,7 @@ enum Classification { molecularMassProblem, chemicalProblem, chemicalReaction, + signIn, } const Map stringToClassification = { @@ -18,4 +19,5 @@ const Map stringToClassification = { 'molecularMassProblem': Classification.molecularMassProblem, 'chemicalProblem': Classification.chemicalProblem, 'chemicalReaction': Classification.chemicalReaction, + 'signIn': Classification.signIn, }; diff --git a/lib/internet/api/sign-in/google_sign_in_api.dart b/lib/internet/api/sign-in/google_sign_in_api.dart new file mode 100644 index 00000000..0a9c482d --- /dev/null +++ b/lib/internet/api/sign-in/google_sign_in_api.dart @@ -0,0 +1,9 @@ +import 'package:google_sign_in/google_sign_in.dart'; + +class GoogleSignInApi{ + static final _googleSignIn = GoogleSignIn(); + + static Future login() => _googleSignIn.signIn(); + + static Future logout() => _googleSignIn.disconnect(); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 63ab2eea..15098ec3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ import 'package:quimify_client/pages/organic/finding_formula/finding_formula_pag import 'package:quimify_client/pages/organic/naming/naming_page.dart'; import 'package:quimify_client/routes.dart'; import 'package:quimify_client/storage/storage.dart'; +import 'package:quimify_client/pages/sign-in/sign_in_page.dart'; main() async { _showLoadingScreen(); @@ -69,6 +70,7 @@ class QuimifyApp extends StatelessWidget { title: 'Quimify', home: HomePage(clientResult: clientResult), routes: { + Routes.signIn: (context) => const SignInPage(), Routes.inorganicNomenclature: (context) => const NomenclaturePage(), Routes.organicNaming: (context) => const NamingPage(), Routes.organicFindingFormula: (context) => const FindingFormulaPage(), diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 79040ae0..a37fd1f7 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -7,6 +7,7 @@ import 'package:quimify_client/pages/calculator/calculator_page.dart'; import 'package:quimify_client/pages/home/widgets/quimify_menu_button.dart'; import 'package:quimify_client/pages/inorganic/inorganic_page.dart'; import 'package:quimify_client/pages/organic/organic_page.dart'; +import 'package:quimify_client/pages/sign-in/sign_in_page.dart'; import 'package:quimify_client/pages/widgets/dialogs/messages/message_dialog.dart'; import 'package:quimify_client/pages/widgets/gestures/quimify_swipe_detector.dart'; import 'package:quimify_client/pages/widgets/quimify_colors.dart'; @@ -26,6 +27,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { final List _pages = const [ + SignInPage(), InorganicPage(), OrganicPage(), CalculatorPage(), diff --git a/lib/pages/profile/profile_page.dart b/lib/pages/profile/profile_page.dart new file mode 100644 index 00000000..534f1ce3 --- /dev/null +++ b/lib/pages/profile/profile_page.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:quimify_client/pages/home/home_page.dart'; + +import '../../internet/api/results/client_result.dart'; +import '../../internet/api/sign-in/google_sign_in_api.dart'; + +class ProfilePage extends StatelessWidget { + final GoogleSignInAccount user; + final ClientResult? clientResult; + + ProfilePage({ + Key? key, + required this.user, + this.clientResult, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text('Detalles de la cuenta'), + centerTitle: true, + actions: [ + ElevatedButton( + child: Icon(Icons.logout), + onPressed: () async { + await GoogleSignInApi.logout(); + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => + HomePage(clientResult: clientResult))); + }, + ) + ], + ), + body: Container( + alignment: Alignment.center, + color: Colors.blue, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Profile', style: TextStyle(fontSize: 24)), + SizedBox( + height: 32, + ), + CircleAvatar( + radius: 40, + backgroundImage: NetworkImage(user.photoUrl!), + ), + SizedBox(height: 8), + Text( + 'Nombre: ' + user.displayName!, + style: TextStyle(color: Colors.white), + ), + SizedBox(height: 8), + Text( + 'Email: ' + user.email, + style: TextStyle(color: Colors.white), + ) + ], + ), + ), + ); +} diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart new file mode 100644 index 00000000..ea962acf --- /dev/null +++ b/lib/pages/sign-in/sign_in_page.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import '../../internet/api/sign-in/google_sign_in_api.dart'; +import 'package:quimify_client/pages/profile/profile_page.dart'; + +class SignInPage extends StatelessWidget { + const SignInPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ElevatedButton.icon( + onPressed: () => signIn(context), // Pass the context here. + icon: Icon(Icons.email), + label: Text('Sign Up with Google'), + ), + ], + ); + } + + Future signIn(BuildContext context) async { // Accept the context here. + final user = await GoogleSignInApi.login(); + + if (user == null){ + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Sign In Failed')), + ); + } else { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => ProfilePage(user: user)), + ); + } + } +} + diff --git a/lib/routes.dart b/lib/routes.dart index 1937eeda..62b71ce9 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -5,6 +5,7 @@ class Routes { static const String organicNaming = 'organic/naming'; static const String organicFindingFormula = 'organic/finding-formula'; static const String calculatorMolecularMass = 'calculator/molecular-mass'; + static const String signIn = 'sign-in'; static final Map fromClassification = { Classification.inorganicFormula: inorganicNomenclature, @@ -12,6 +13,7 @@ class Routes { Classification.inorganicName: inorganicNomenclature, Classification.organicName: organicFindingFormula, Classification.molecularMassProblem: calculatorMolecularMass, + Classification.signIn: signIn, }; static bool contains(Classification classification) => diff --git a/pubspec.lock b/pubspec.lock index 8b405154..894a7317 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -461,6 +461,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "0c56c2c5d60d6dfaf9725f5ad4699f04749fb196ee5a70487a46ef184837ccf6" + url: "https://pub.dev" + source: hosted + version: "0.3.0+2" google_mobile_ads: dependency: "direct main" description: @@ -469,6 +477,46 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: "0b8787cb9c1a68ad398e8010e8c8766bfa33556d2ab97c439fb4137756d7308f" + url: "https://pub.dev" + source: hosted + version: "6.2.1" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: bfd42c81c30c6faba16e0f62968d5505a87504aaa672b3155ee931461abb0a49 + url: "https://pub.dev" + source: hosted + version: "6.1.21" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: a7d653803468d30b82ceb47ea00fe86d23c56e63eb2e5c2248bb68e9df203217 + url: "https://pub.dev" + source: hosted + version: "5.7.4" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: df6c91e74b7bb07335c864434c2064007dafa5539a96adeca6e83811be583ec4 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: a278ea2d01013faf341cbb093da880d0f2a552bbd1cb6ee90b5bebac9ba69d77 + url: "https://pub.dev" + source: hosted + version: "0.12.3+2" graphs: dependency: transitive description: @@ -1108,4 +1156,4 @@ packages: version: "3.1.2" sdks: dart: "3.2.6" - flutter: ">=3.16.0" + flutter: ">=3.16.6" diff --git a/pubspec.yaml b/pubspec.yaml index 4e79d06c..e9c2be83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: device_preview: ^1.1.0 google_mobile_ads: ^4.0.0 fast_rich_text: ^0.0.1 + google_sign_in: ^6.2.1 dev_dependencies: flutter_test: From a69132244da3ccd623def3d347b08da3b092b4fd Mon Sep 17 00:00:00 2001 From: Matthew Horne Negro Date: Sun, 5 May 2024 23:20:25 +0200 Subject: [PATCH 02/19] Add profile/account button to home page --- lib/pages/home/home_page.dart | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index d44c5299..bf459c9f 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -7,11 +7,11 @@ import 'package:quimify_client/pages/calculator/calculator_page.dart'; import 'package:quimify_client/pages/home/widgets/quimify_menu_button.dart'; import 'package:quimify_client/pages/inorganic/inorganic_page.dart'; import 'package:quimify_client/pages/organic/organic_page.dart'; -import 'package:quimify_client/pages/sign-in/sign_in_page.dart'; import 'package:quimify_client/pages/widgets/dialogs/messages/message_dialog.dart'; import 'package:quimify_client/pages/widgets/gestures/quimify_swipe_detector.dart'; import 'package:quimify_client/pages/widgets/quimify_colors.dart'; import 'package:quimify_client/pages/widgets/quimify_scaffold.dart'; +import '../sign-in/profile/profile_page.dart'; class HomePage extends StatefulWidget { const HomePage({ @@ -27,7 +27,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { final List _pages = const [ - SignInPage(), + //SignInPage(), InorganicPage(), OrganicPage(), CalculatorPage(), @@ -166,6 +166,26 @@ class _HomePageState extends State { height: 17, color: QuimifyColors.inverseText(context), ), + //const SizedBox(width: 120), + const Spacer(), + Padding( + padding: const EdgeInsets.only(right: 15.0), + child: IconButton( + padding: EdgeInsets.zero, + iconSize: 38, + alignment: Alignment.centerRight, + icon: Icon( + Icons.account_circle_sharp, + color: QuimifyColors.inverseText(context), + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => ProfilePage()), + ); + }, + ), + ) ], ), ), From 5fa243938593c599932fa8c4ddf651ad2a9752f1 Mon Sep 17 00:00:00 2001 From: Matthew Horne Date: Thu, 6 Jun 2024 23:57:21 +0200 Subject: [PATCH 03/19] Sign In page created --- assets/images/icons/google-logo.png | Bin 0 -> 38249 bytes lib/pages/sign-in/sign_in_page.dart | 89 ++++++++++++++++++++-------- pubspec.yaml | 4 ++ 3 files changed, 69 insertions(+), 24 deletions(-) create mode 100644 assets/images/icons/google-logo.png diff --git a/assets/images/icons/google-logo.png b/assets/images/icons/google-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..39ace85d285b2bce8ddde3763268983ac4730351 GIT binary patch literal 38249 zcmd?Rg5@)C5G14&7(oRT1O{mYL_$DP=?)1= zG3b;Oq`T(XXYhVMzyINxdtdin_t|Idwby#D-22>M^*ahw=3Gjf_LRmo;!oz>1RA)wlnGi-+M%#06am4G1(W7eo>h^4h%9fkjVA1j` zYaTnzuY;oRPJF$qDPyHGp3PL+u4C}y!$2=; z6A_-fe5wNX_IZ+aFgw!xhDy?B^#L}Hk2@Fe5r%Q)n#(&aPw#O&@ZOiEo;qVqmoRpK zzwIS|axAff#5hIL5OI}vSpKOU|>)YlC*x0!et zN$pDg;>yazl4!?NBYRY=_F~4w47W>`e#V3g1BSb+&@}gU2Cqx8@}D<9IJ3T|F~*2~ z{fz0A_jo{_rs^^@*DGIIz%eoSA?SMv6hk zPu#DNq*^yBX9R>sKoB~=_`J`?N|$z@7IL2(X3@$rX2=40qrhZXel>{v0FG zKQvWY`8lD@J+dm~&DyQVxaz4TevEgU3jgz7W%9-t?(2V8ODyE#fp!Snst;$ic_LlD zMKMAhZto=Ym!oe~P$yh{(OPxuWxFYEo})a;V?E0JSJDIC!Ie^<*`wUZ|6UF6%XmE| zY1c;Fc?cl~$Bg%tOKQ{JDw0X^lCkIF23(hpRLt&P`R;1YEYZGBcD&xkr=Qw6s<`7ixhjXo|cfB-cHZDtD$A0jY!6n70;M7(N>6 zJ&8?d4kT|@mKse9hza?t zQOe-P$gu}7V=6bEn3Xh)=;WTikxn5lpm3c7-W#+5TvT|8Pdw}v%OWSSya@2 z#kZZ_HW3jdyVzgHZ=m(JDPVd+SV%R}EiFwGhj7tNNc$qlOa5u1;hRZ~{|Io$04P|G zV(-C-s9=XZ<7HMNf<+1{w5G6?bGcLveMb{5^~Q>eYzjU1mI4o0_^iL(Ro59#a`Xjx9B~G*68i%UO(;*5>oBh*Bi@1{%aU zwOHZQO9_YLzpM}4Q>U!ch*A;+U3jDH?y#LZu$f9t&!;&_z=un#y;jR|5;=~mY{3e{ zrKkjmj=Id}+f*o!FXzUVPMSw1Y7>YG)W!ed4Dl%au5weS_Z>S?N^&l4L%iHq%h}<# zMbf!r`S?z5ZU)a#@!!GuQ-6I_k#?wvtV0>t+qg39C2E+@8ySASe4Cwsv$@V5X)l)| zv2VIu^H*c;h}w~4L9N&F+YYZ6ncewvt)0cj8X|C5A-_n(ZlcS(cAc`;7TjH z3Age;);C|uLQ|Jwj<`RGnRD*!_REFkvC+av!0OwI?afWRl{4ldTbixVZr{I#Wj;B5 z*W#&f7B8|?PvT(3h7AEP+RBsv)<_3;pZbo+C+hY?Ps|lf1dZzhz7gJCe(o>>spMGz zbnv(A;DT0m3&-i3ea1}dBz{nThMskW2<2jnC5!&nY3JiP4SN;Y?+lQvE`8~^Q(Uyr zTuDzF^4&m3UP)7;am>|$TgL(u%k=u%lr3|pwPB9Pjj*_VIh;EOEyOJRZpxM6PsYpN(fFqq zNC@lg$*#W%)=}EY967wY`al|*iq{)ij24JG8Y^^Vb>cilkn4rX&EMqlzhXKB=D*`O z!%sj{Ri`i>tp|^z)}y8GoG0L6h+tVVoUZRW@LIimV1Kkz-ugw$Iw8Y{cM^oSlu?9f z_#fKW&8sR812MBo+?=#fD+Ryeo=ogqm30TPS_v%~tev1MPU;0)YCl=S=e78LMu6pV zLvg+lt)ye;%Wda*nh;nJKP@Js2IC`;GfeH9Cs&32pdhN43KgmL#_^%|Onm!7!34%7 zHV1#KR&oXWn0CQ`(4V1(R+or6W|RgNcxH3;Rn8NzrS>uIZgJ_=ohl5|%LfM#gf~G- zIP`|TXA~Z_$I+c2FqD$l3gV9kYF!kW7+lBwwX93ujRG?%V7<@>w2Av$@H z;L-?tb1_?SI@0QN6T-h#{tcGvY5CRqSNwr_+qORKF-q#!xt6qUuuD$fp7AYC^TB{Q z-6qi+ypB`^PKcXwvNi1k$(Sk@cyR8(I_k{m5$7mG4}uJdVWUL*Q64#6-Hh4Y#BBj3 z)TQ|HVekU;L-Yt(Bq_^eczgE03TzN|xFV&bw*8<&qj7sDt1~VVpa|@uhZw||g^o=s z%x?@a6#kp(ms;hJ9uL#igxhCf0%k3w_yu;INKy!8!4k3fDr!AOQkI_Z#4rH==A?o_ zO1`q_b72Ugt?fx-;@-*swjLveA-p?Im6NHhszOpP0#pwOgjsRy%dt&RU1GI;5_bb; zJ}9^Nk;3^{oh?$=x3|thScww3qUFm&?mpA0nTEh&AR%mjzh^@l?c0Hl*^y0yNOB-T z8ajIpW36izVXQ@#1oQCf1S0Be^jA~7A1)js3qjWoWuM5um@v2`}Ip#FR-_i{Q}rFRCRkWJ^48@E~ll-r|KW4 ziT`qobL>7r+rY(^bf<3$8XJo}2}|&jBCrur;5~RTossddwA{}`^I9-i=XTOc58id7 zVVar_38PP}?Hs)@Z{BWD`PgMUN-(2mJxoWaQp9b48Hg?KA^}(~^6RTVTfB6o-^`-` z_ENe(EAA=Dm27ar9-9Dk%6Pr|_VO81eQ|Ermajn223_AL$D=GjLRyg-)(z$2BSjyd zRId?b)|i1l4ysGEpf%I5VLtctH+Gcj_aP(UCLD=@Y>A>rNSZOg3GN zyW=FV-piIw#rb~YsC*K$+y3j=i7aPjxi`e&eAIgHq=9sGL+b1FR&o?Ybmv8K z7{a3jKdX9lyRDm@3WC^{O(+CobR~9n>}3&X91$CY!l-K5|52ULqQryML#e%EpH!6R z?VV1AP=!QLQ1uL5b~AV6j&w}263pI$PVGocYk&GBCSITC0@xi3Bt)adXW(kQ$&qw2 z$$EzjLXAo99Hu$2J;{d2%qpW%C7!Fq*FM|a|2b+?&x_*(<^nk^*Ftg z#6e8D0GN|hQ|^Cv=RS_gR~w&wfwl76GjctFai+CxJ_J)gqL8s4loz-G|b`Pq_+ zpS?9)9WR=Gh5)K~;<1sF>x@+}OeFkOqu+zgeU3ww zUS!{_<_D5STDi|No`W}dD6}6^IGp0gWSeVgTlgEmFQU0G4 zx=r`k?Fe*^GpEl}{2UjQfeOu_M~ZtE(+ADVv`Aq6Daj%~epQT@L;9dk9bXDN> z=z}r#tT#ZLX!bzLPjMF>1S@+O7gYjVSo$?z_<_fP^3P|s zudC+n5@|pIVGK|m%Im1fhdj`o3jZQa4R~{?)q`&&n)f9CztNV(4dq3=A{z%%tnvz-+QUpYuSCM- zqcFfdYRx3UNQ67CU;w8I)4+OoUN#JmlX74B-AMpsM6r!h=R3xo5!@CEe*Qp}W2+Qh zf93s*0Ax_tY>butpZjO0 z;yJRO0XRYx)jXP|K7_$)=?FL9E4zzoO`xcoMsMNMbEvX^PX$O?-{Rp zMP0^@{yqbt`C&Y>K8tS$J_PESJw?U)>0_2-gJ&vGG*)K4JMgg0kMoKg8{qEHg3D_q zGb4-c4m){@M;Q{DYg;ju&NKV_qStWv&M(#ch_sWR`)(dvfcmB6JcP zkB`-emGSV4olBIOdCD~uL+pG<<^nI?dM0$hzk{q94z!oC+s%AHL@Jlr_2R#t?aRzz(2k0QC2dgCzs(QYJ!*L81^ zhjEaOzx6CQgRM%Gy+>Sg%j!1^;780 zgf|Y@0IV?|{U7JF7mjLq=C%H91qc~-v+%1H!Nw5H;e_9xXUmw15EyFUEeAt1NzlfM zso^1U+_eKhPc|{G^W*Uj(9$(sL6F8zBk>9O7@s6|@CI9$)6r8z9XTYc`Ta+H4q;jF z4>wjGh(^r*evY(xFaAHq$)TyNnZZAd-#%U>s;YuP)Nc% z1QX|1OvRmF@>Hp5(*9FS3al9MN4HdMMb$LL{teE>k=#-q&XU;fV)Q`@*5Ru#x}c4M6=V#+2qg)NYtYI< z?1`SSUq61S2=3m(Jcs2>gL$$JTij6JlY^bN5-NO=pN0M-FELDn^<9S@3k&D6&HsQc zF^!8Nc_1C7{oiijNSbOY+;^v}A&oln*dDOMbwa>P$KTHxNL-ch0eeh3rt7$Hts|ZU8i66QMStWf5Ju!Kcs-% z`~UpTu@6w+R#plJaqpj@ynFOL+oRKFGd4}*ADmG)JxQSoxM&*)alH5cNb{G(j~b5e z8tliO0p$(C)kv?lp5*O3@&8Au+Usvl?mspQ#eD_d#6{_D9%Gzn>t};^cRSxiv&Y=D z8cfZ_tNlt>bvjabS|x|OHBjJZLR0qQis$c&I+3PP&z>CRbU}uy)$YJ@;o-CiquY>} zZ-kSm>yMQY{?B5rMQMm}Re<%s-hZdEPdk+Y#|ZT--@@3lS5Id2b#4}^3= zIoIIOe;S<6L_4&x58Z8+Sg{&1J1l^UrjMUe%sgpcH2MoLCs8#_$S_k<2Rtk*r@FpA z!7_Lv?ypsE9^V*7^JOC?uq@y=@{&-0>s4{5I8Q=z#l<+?_yG!*y&UpI5$a9gcf18D zO<2w_<^}f(j!e8tvW@uBJK{HXj{c*urpRD#iM?)pmrYGhoIBODcmi3a=4se}l8#II zpD5t%)UubFnUTH-=x(*#&zb60zI&x_-O-zGqDF8)XXBvY%d6|*-H&5P^6_>DdBLbh zXqp#fFBs38Es-gDC#x{9lN>WBt*<4%N~2Y#4STz~AyS>J_Owin$3_`kDifww15pgr zQb%X!H^M@>RL*|;79}N6Fpfsn)V7ai=1$kKSm<3kHc0Q8^tQ-XN_4r(9OBP}A#}pD zcdx2i!iJtfSt_qq&694qS2u%fRsa$e?PxNZ+XJ|Ta>z$=RBmE5p=&}pY=+)^YWr|R zx$sL{Z=JXt&xL`4-i0WKwf!~!v_k9QSLIUwl1bGiHTZS(`!JSKgZ^vvY4_VW6|f ze=>X7o~a00>N0}=g5WG11o67PZD|P53YoggJs>$F>hz-_J*+U1b%>ip!ocMj<`JO} zpL7X3{6!L8u;&#;^5DtjH;aMn2-M!enYfYs9@f@3d#oN6_vFOm%oM79CwE@T-n(Da z9(MhE8?HH=++}j|t??E_u2Qqfn68l` z?HScRo@`jrzW&ytcV7{Cyi2j}Mg5jGCB` z=mdOBw&*1@zIjJNg@!o5n!v8Oea5dRLmkq}ry(}8+_+AiIam1ylz1~9-LznmfCsQ& zbIbTt%s6M|A+6I(qlq|?>543OZL7;OT9D!3_mt2@?yWFFWuOjvhgia?ou%+DF*rQ__1 z-c{iEtIRgX8GQ&_=(31f?rv-X{uw(&DHyXrE0i4f=W4v9!|EiJtp9=3gte+x4p$s# z2eaaYaoQ_+OmiTbfaJl)+{gH*;~DBBHWp11vz4kj8zBapd#_CjCXtiqtL@l5XQb*m zB`M}-d(RRwTvSOxT&IWTTvdyIx^h$}gT)F!2YSif^-Jr>@>Bz(XZyYNRl4n{K-Ncn zUpPZi5&D+LVxWpXB4#GBx7za(jokT0Xr!Mq8#0NPDF47dAk{c5%{8xn+8p>=AToy? zY2Sl8OKq7`XYZT^p5nXpbIkQp8zsMTxGzmYv^YMy^?#jY%$0wyxfy~!&+@eIX$&-{ zkigMNNq6K2dgn*<&XA8fNg}Y9xN(aZzNpW1!BZ1oZfp}ATk~hloJ*ejK35?NR~#KrAnabelS?~v(_uwe8IRZqnB_Qh zGA!9>xw=k9`r(A_aXb0p_ZZNWgiADaXxtQu{`;)b$o(wftn#ERb%NXP56IB%_$d-1 z%GVhc+3$HN&kk!Vp9R=PgR=opy>$s~n}(BMrD&8Yx~=;{lWHAKIz#0;B&`9Ug$?z& zAJ4oE^Fbw4ueHcUnG(~B^L>Zfk)IAwPUbp=3Y6Z@)V;98Xmn-X4TA`t8h?JB`hXaC zCg1WCA9BlwKYQ~pLb!cD6s+~HXAKIA;Z)<}R{reQFs=9U zr~l#Oh|cQ-TQ{xpHIiAcz)82eL3U98(YY5%Yju3Al1>AM&k#-D;j@%8YEMkox)r%E z(h`6j6rNG>Cue%^CI;$%U5v}iYK=bPZ`VL5_6}x6x^JN#S63!0pY?fh>`VMi5^nl# zr*R}lYXc`V`h_vBCRq{L7n)eFa4F1ZFvz@A+jKk0cmQ>$E*Z_kzrre)6nRQ+RVf~ z9>$NJA(x@Wpgu0vLIpuu-#(y7`Rn{2%4gxYv1bUE_K!9COga97+L*m9ee>!3OO5rj zeinwMJH%KP1D_|49)$m-_NrTXJPAY^K}#d!_Wb#LzTg3|r|Z`BPjLedWqx0(niD`a zwec&K?A)wgh=a6t?>%;V4p>ItTF9|McZJet)G3~nr-7QUa9e-;XjOwEn*S0xbm%g2 zt@yr^){0i#>r0{6`BDwGs8Rm<2pXBulscl?V365XhvKB*=J;7a?9gt*pMhWds%@f0 zhOz+_3KK8Ft>r@7D2@GKjzp7)^y!!`<|SZ=f{Gv0o%FGoA`a!JLt>wP_P*PhBqMr| zWu-NF^Wf7WymQi~1+LfmCn|8=@xuPXSpB=xYs&K7rbj3Zi23ShO3LoLKke+sTv_a* zOkG#^$!MI1iD<3`=l!6f#G@thOFIt~erY@e_KKBZeRhssx6A1usc7IfhhYaOBx$|) zFZs{eW+w=kIJV0<6E}5oUpJQUHu)`)3WeJn6%{}F$y7O1%Ff^Z-82$FV~r@lDox7# zZ{>L*luf>OAO6aauK*xzWR277u&*+BdV#O6(QAAExGYovs-9|lESj1^=puJsJc$z3 zeg0HRbf&&}R~CWdHY%FQ5=oBPEykVpTH#skgu@EuN}%0gng0w`x>~yrYcI5h=UDDl z^MCE%&covCDdY?VrOy1$30qe$#SF%Qde()kz3ozpozMI|=A)2N9mLQ93*`-|17Fd1_8PpllQZY)uO^Qa=qZKEW6QNt)5*$poXWiyT+P zhB#=g>t>9P1L&@J0O;(P%&L4`LscpF<^}j(bNF9Qef|sxYs?Q#r1Mg|eA={dTO3rV ze2F)$q^vCPIUrEO(yt$P&OcZ)RrVDIUY(NC5z#7R-xp)lyh1(26yum@d(s}5j9CtR zP6FJ9O2hy)*}OvRe}KLL0R4IZJ}SBp*Rc*FBNd8&)W(nluMfBvZzY08jo}9V(%!qY zA80E4@H#c>HPC|P&F}Ld=AxV=V!~CecF3@Qb7K2EKV7*Yqgm?|c;7~dM}Bb?woB(G za_spv!Vx6zZerT4UU1Hkq%G<*N{D4h$gR7=?q{XVrwYT~iNg)O6`(MEOC}yNkm<>F&B$oM~pwZo^A}HNc6{UP@&&Ap4o?^vMfdK!MfwaC&_D?(f^WowIWE z{1`#>tuq`mz1AO}2`j{5$`gtZxvoKzDB`lzt163te4f13CiE|qdwW<8W~xRHcHbNjOBx)pa9@jd!)H%_>Gx44{R~{pcX(c3%H#sf`jUG{cU!H?49OxwXmcy zPdW_|d zB+m{)pQ_?l{slVtI*~P2HbM$iV~4lkb8SJ8AA}pS>PK>MCD1)k!we=1V-Vn2X3Epf-P z*1IS-W6WuES9|=>7a&Xs9A%@!(kL{UD9df9P1@cLogi*Ce(NG$X;Ckz`v*KnIk5+s zjqkXm|A>gSem=3%5O7`b-IaBvGxd(;#T-oGwI3lbri3hTSnncx~(yS}ss?-%)!n z-Nj1*mx8VT^i{PbG^zC9Oe&a-ma;(BL2a*9eDr&Lm$zy;xNiziHAHdWFuNp!2&l92 zqoxymv{BW}1UK`b-IojyaosukXO$d|k=o^*2=;mI8!2)!8oKkfG0I)1LhB%ds7}s| zkhisN3>x4CRB|Da8Dtg47kkle`0>#zP6plUIR_t^=-*_VJT>RXL#)Ok>Hh6KL0YZA zH0JIt6|JO?>*3@*wrfwmaL+OuCbc%@)hR2-(mXv{O+?0JF|4061J?vle@TzFuu6Gt>W)jCJ;@AD9=!n}p~wl_;yKJ(}}x{FQak z=x3ROPI+uXMdvO7U=Bf(uk*wAWo8eNWF#m3?j9NyJTt`IueJ9`$D7=ltE)6jP=7ew zx~q&=qKCp~j=vJLphs3NMDLfqmDatx|>Z$}YVednYI*cO7f3>jV%-MW^X`Rz=) zE{6MTYwh)Fm!Lu4+RCbc&pc7 zAFYJs`E9FWz)2T`^#du8)SEjQe9^)#0)C`CCv?33a7QKW6w%)xL#|F9#eonoqDlj% z)`=GqrR2}N9#Hyl!#O8yLd@B$#X+N9Dj~|tE~qN9G2}+oiAT4m{iplrNWk=!C4EGX zLeV7(o3hV(e1|H**Zy)>j*F#l4R;`J)llO<@w|Re^%4bUg53NlS$bGHGLo!}{e2O2 z^@mhTUDxpNe3n7|vIz-$bwya7btQ#VW~_?Rv58kdJr~2f9zFOlxHO_u!_g)K4*u_v zM`pbbgb!^EV2VRvS2bpP+Vs91KcvoyOjQ9%IZni0q_*)B;W?MoKxnADdAP|f+k}(7 z@@UteL1H3y?~VvP9b6uhGgf;&4zXpIJY!V3AmlXvo*;iWY97e@isb-4sCR`Q6HTeJ z_tfvh)HA()ZW@g@A_J5J@td_2NE`9*_;6Fw)vI9AH4b(oEr)apwr$e&qT0o8FwjtO z`qWtX{^o}}Y*7C-U~tbJWd2;mL}?sdI1Q%y)++%CH6i4>T<1$(?uL%R%~Wu8#VjQ- zLT%;?M!o7$PTgc%zN>mfJ4<);vlto}Mm<6jBn5#LGnuEZ_q}|wU){@3sWT+$zz~%9 zi8Dfdvz6?uDJN{PDffU*wDQzuf9FRHK7RkgXd@9UD%RY}U{`KKFhZ!hF>=gXaG(Fj zDaNwkg-MS6dOfA#fyb`w6U%P!w>I?WzNs-3# z<}rUe5_9=COU~V@{tD+`+RQPrYu@d@a*4Jra?q5qeGRfM6`fx3Gq6NbAm;N+6~fV6 zO3w1uLO=(^Y?Q=rug^SHh$k5RoVWvV*`-E&!=$m`KnM4igNfico$34ge;vt(v=Zs( z!t@4p!0j4Sg;RhSdJOpB*OWB_8*&!2BploQAJ<;#a+eDkB2&W@{On}D1W7uv6INXY z81x{ci`i8U2GUwogovP4cjnzFW@tS692R`R|j zzkojsj?iMTZ0Ml?@%RIoODZ=**-Lme%H?kJdEY_qKVYX^>|c1b4|j}#Eg41J$&g~p z3SuwmCNJWa{LR6@T6$NIi^i=b$6RLSDja%YdxDU4B{JAPJI+0k==#RfkWs zHZ|hC<43|jf9={$Am>^^@{e{U&pW7@$iOnLtGmY5-xahL^it?=Rv$anrO?rNZ|}0f z*(GkZ?_x4>=$kHZ7YPoD5b3G!{q9yn6d6JX+oAr>=iq=yF7HkwzqqRhQo7^y&T6HF zh2LULa=0W8JmIX)L&y->+<(bo7aW8^l&2gK{l8ZHYPp4if8^kBY2W%Ll-{=U{HS@z zb*23Z^Q8ym{;N^cd_GA8C-@Y&iwBBan)j|9Y|}-O=OpO({-PlN+RrT@8aVP-h!7ni zB$}`5!)*91yhTo(etD)veUV`h%u5Q?dga18#dfr|>S^MKMA4Z#M-jDIw!LQ?T1^v9 z4~)|_fk~iqGL`tOh?TZmfw}HekB-Fdqo|}JC0!bvLXvYSe+YbY06sl{)0~GEg`ZBE zk28eWe+#Eb(n|HuC0cLAeYK1kQ2{q%p}O(khD)XM@iEz=c4sh@EXyh%4r`2mZY~Nm z1eJ6>^KVq>GTqZV79==#8+{RopRT)hxg&dHLx)>()%wZ=$?1{4-s1mub!TlrNpb5U zu9XTIn5(rrz<>F&bxz$}b?nBE543-;pN5?|$kZYQFGUvP!Y&36M+_J1OtTthO|c}L zyma$c3}3OBng=bMCBf5%L#OfOp19~VouhY5rH%}(T0E)af$y1wrEbC8qaDo?y59;QO4^#jsoseee#xGiIbQJStjp-S z5+x9ajP>3`tQXJGEjSVaC^&ZW*<8hk6*Tp`M;Y%VYC167(2v)Q+t3S!A&v34Yp6JTxi5vGwWt$dDoEgoP8J+UkELymdl zv>g>WC|?Mo@S3?9`W;2Bwm9*YyVfk7{FH$j^WpZN-QC#dJkdBfTmUt#SFk9mKD1N2 z6CiOxE%Kd`^a^#wQ+I~H?WNFE;cwjsSNm5iEnrqI zLu|q)1~KQ4c9i~BYruXlxbW|@!rT1kD({b#`z;XMGaz&N$-A#pH{%;w*CYKD?@qG< zPjt#qfqCTdw1n87IqOfY#}*qD;-LSqD(%)O>8oe5H=1H}T-Kzmvcl0N*N+TWyzFT% z!qey>jd-iXY5dOh=VZBhy9!Bq0&jAO#tuc^OTSJkUGya=4`yt?-N=_Mki{v6`IFo< zQ-RKSWwis*cRxB<05lI6OHIob>VJ3&NjAT0DQPjz`u*+OV3F-r1BXXWpj?h5ASS4! zKNk`Z8^TXi-#HEP#P>yeOxAJ^49sPIZzGQt8#Ml~Wy^0a>4y|Z{Vcn$6l#4eB|m8V zfNYO(7KLR*4@FG+^_;+noOgHIM`|YN!YofboAD$yn4yYO@yh4 z{zam3zV;5aYCb#RzzfwkV=7MHul2b8Nb)*c2xP6cuVJRbr7b=YrP%w{*Hhx> zD0IVh=I8mdK=Fk0GC6u}e#>+MKClLye>*?ek?B(Q&4cR)2>9*Yz8W%iH^+P{IV@Qm z61!#K1J27(s#v}lXbYOSYaWgq6C^;}z`n;x8(0|WrnzA`|dQ>S9F0X))*7{g-i zwY%$nX4n5#ShX>}Mu9ni3`WWjiF-YzZi%Wc*D=G4|aGi<(%US)MASQt6B^(&xxjVLMg6BhT%yq}mP4>(j^Vj11s?qrPf) zn#(&@4H|ukG}W8Gl4F%MJiy)4S<_e#gC5|SH&TU#BMaLYVx%dM2LSz1TOd`4EW=OI zP|Rt7_&fuc1-3uffX&2T;^L5hWZR15(Rparo^o`aai^10OaXScfMZctaC2j6-XKyHgVl@S3334*Z+$;wuh333j;5a zq0|vK!4%-}sKI$+z24M0LuAq_?xVnvlOGi|9tIzHy-nl1iL6e&FQ^niO%?KIE=)Qp z5P$3gcI#O$545F8-lz)Nc;#x9`L%m_KbOdB2M#=d?DZm<7t{Dw3SFA)jnBvNbuNKn zU*Z5Z6p$L6Tk2Z#n~EY7l!>*QUv!^#udcwwB-oiBGc_c8gjwp%SbLSMm3`e18Sgkf z0he*WP)$}8w6D$!oZ?+uLmDP(3>Z`c1N0YH1b0DTx)gCGR#9n0;BoYW5$=M;5a2Gv zKkJO+zLLdAsbT-4QOEgEpO!4^P}B}6DeA-AlI~OAb6Nj(cIn5&dEKTNq{IP;gw)1( z6a4+=s2vf4L!yVg8bgSnRf{b3bgo0oeZZTffcH_aNE#B*2H6a;8hF^u zi?h=CO+6lt47R7+34$ZX9z5XDy`Zpk$4#D@%l;uhb8k;)Sw&xPO5bjLo2x7Fs*njK6egkm`>l8VuS>j@MNP_-q|1We#^lD3mw8yDO&nhjnYdWiuJ4`UQv5@_!LfR zXgroWg1z*h9S08fZ;Oyq(F)=g7>4@3NRXkH$+2DLi{KWhoI(-%r&vPHuM15XBf^De zkNn1y7sK4sO2T1ls?)^+A_c_Iita>!6-+=8w=7a#|El$3GCAZ9K}RA-kLwO$?bV%I zcT7!r#Bb61o(OvZv?6NG>N5R!Bq>Lz7o-(N4_i@cgm*^Qyu-(wIJw{&%q*wJ$5gX5 zk|@R1YJ$ar3>pt*`s9pK!)8|1zUe}< zavuAE36fHK&*98ZhT`RfiJ6_dbq|UDL4gh7FgT)C#05_iWFlI2hNtDd2^34_fH7Z(Z{P!{d@1?py(iPQBc6p$7g0mZ^~7KB|I2sn|tM+s4QO!QpbTCW9P1c2XM zhB4=LPaF~I z*1{cO2$D?Z3efM4jNpczlxu?OYT%#p+u!lUXGMkLzG;g=$&+2B=wjcHu93T#4X z>T)1SxfrI0cUr86Z zFqR9wFQz~<7a&m4z7oTpSL(kCG~qK2&>LL?I!0yfyKfZ41{Ji(p#sgyDR|ULO;f2-%4;5Dch0L z>f~74^OqeFXou&F!i4Oqr#-6|v|oc#AT%&0fx@SRqQ%5Sul+JeAF#u^vGcaTQ2@|; zsC4l{8sp&l;Og{XT4)oz(#BqIJOiXrCyEU!R298#K+u-A*wMLCNEQh$#&%!761)!w zO{|pJyD}tdmMrDDk`nino7~m0v}3!GswlHTqUj3`Se@kwo7yoc-S)CxKELENja7Z(onRI)xyYL@fx_{NJuTeN+J(o+nTiSA{d-_nUe)*nc{@7t<#}jG8?M4#=DI{-Kk4RvD;AO6ns3q5-(*3Vzs{ z%DN1w^$|T3#IJYpSYw=NQ0w$=O9;5oZ7KQ;_Fq`3cz(&G$__bOBvcBR`&}!XNC2_a z*=M0ds7!|VM=tSfc}91RW%qNlSR6Dt@M(sFH&kg&|6y)mT7(C3G3L2<#lnFZ&O;%e zMh1`y3MQn6usedgePb!$t||ES+zSu@p5f^bM57E$0gTLvgT0WG1%naO=^Awm!3OrM z$^fH}3045duK&(d~c{nEeFOlmH^sEVX7?un@zwJYeB^QDhke>rEfHUBf#a8-6d0UwwSp+8b>e&ObVg_08UDg6^5Q4Tn2x~pf1peBOx z=G}e^U})hM5^z?GqICfuI|BJB2Me?Ti_r`Jd$&F7Upr2xUFNKpx68A?A6-{ z8pIt0*7H3~81l}P%&YaJsb(DjV7_i$P%3uN2y4QujucWK4x15z?r3IO<%s#v zZg1zSaCMWNu5IYm6C8#G<6LJ}zuly=eh5_vPBUDp#Trr9h6rM5Bv_Hin{!i20~q|AMW z5o@btBLu!q`9+GIh3P^g|$07-s6{x=^n(Sz35VQlt@t^2GwkPSw^MtCbnYfzlI$v@Z4`{5P%ODv82y&Nz>Y-?2)RjF%hs$uH^mZ z=Ydj>D9SeU?pUF5PgMF2`~%ux4SU8+Tn9HdTA3hDzj&=Pp1+X~CWMc-JbhPe{37-n zKO|v>gjO;H#*cR)ZXNj7py&no-!lkeKz6&rxo&w#%W!-|>h!B4&fEYb7CZ-*v#%_; zpm9~XINml(0_QtSk@x`mwFyQDNb%_Hxo!f&j{cq=Faf3DWr)3v!;u&owYo_t->{_} zA@8)HDP@G23k%7KgY^+rvPTl?W&AR*jGa0{WC~u=ucbpLPJ@^^%0_FYMo3u3L+h`% zlVhu{K!R`x!#Cf#1Z!$MH11lQu^hRVCR9~OhZYZ{+sVA-^RAf&ewb2F=GNNY6wY4? zIgFgd>OggdCPVWAn=-IvqKC^j{tT-baABq}2o#1>vU!4JbS?fNU={pS?gE;+&@-U9 zAw7JU7y?Z=&Wi|L!j|@@lp4x9R#4zA&WGN?@%hvJ$xlW})tZZ{W@|&PVbp z{E6N=WcbRy?uvoEn6p){tDq~cwUG#=6Myt6jWUqIc~jr}4WEOUu3uNklZAxQn!xV7 zd^R{`ZBn)Meoepj87jC`EY~XpS}vi=@dGvad_y-d3k|6rH9e9Nfp5S>npz6$_fTse z(Rip#5QvmK=c>{8V!Kqw`_c~=aCi?7-NVH_5<7qcl*uhnxbi`uC}Q>!6;fexyrHXH zA@77Ybqh6~o}h=lH~J&HK4L|;?aBW0s~ZgDA(viNZ7=XX{?Yci>)o{*`{K*E|fv=wYSi+3-cz`2S%vG@iYM zc2M*CbWiajDH8kBE)>J>;z?`$sXNU;bX3kapi6t&;1N z@~$eF*+LVX7(z6X3yrtHw@0)U={|nX56hG@+xRa&0F7+J5s_Rn#k2CKgR;1SQSwM^ zf@H^p+#87qmaKKd*IJep7O0~NEiYyiXk+uLfRbQ!nll>Tk_BDXAKrYE7JA-d>L4L9 zZNPA=*E)gI?t#9rx*sn!F$wsPKL6VA00JtszJs92A*wq||MkI@K<4s~omv$xTE(y| z1*Ho36|#VZi346#?}B zSKeEHMfrUJqYo`5h)O7>lpv*~)PR66q?D+%l(fWZ+ue~V=f#m<9Lg9`ET^0M774h4lXi+n$2LJ8wFqJUp#@>t1Os5|7kaSRJ4kE zS2Xn|p>f2!T0;6WIc$)7xGJsot`afmSCNcWFW8g+$RG)|l|m)a!~8Z3p$s*%EMxbL z{x}zGZBFvV`xV3(?A?z586=<{lq)j>{zhOoK!^oIs%=woQn0;xOLA79`7K+e(@s?w#eT2LDjgsGr3HHn`HtuY~2`$DJkxpZtkY&lBp z+X8+Bh)=wnPwd{DMkX{7)7X#&RMSFTaXmC}2S!_p3K6rnyL?>}h>*Vv1;dOK_ zBT$at9Gu^BaY#n6Q|miEza_*KmOu*`^x?0FLKHo@oHkacjp(!N8xDIx?;F%w`kV;? ze~RAyE2TB~7$IwKLhv;%`e6?_MCD1%ik^8S(7z(e^fh^$N|(Of!J{{#sMZ&)5Wx>(#MpHo##c=S=h~W&Q*n!5Ef>;WrtKgn36^tty zZ=QDz{sgk$_J;8{y+r?5bh30O3uW|maO~i(E!}+V=4~t`FR&n2a`b`01@q%GNTWUH zG-o6ivUFLeN5BiH_?Z#!G>V8k{?|#}J+^tG0yQ;(h1-v)f zPSWHeYD0^#TA#p(nx7E48EL|iN%9fX&-Z@P`ul(SP3te2cgKdP02l?m*U;(BIP4eU+%{gP3jgJj04{dtW=a)!g|cB#0((21_hRaAbwP8 zJG@=0GYNmzgl4P=ru@tKZz)@9-{oO1zK*p>Bl8yrK6S2R$72WAQ-HQ#M2@eEeu`Ot zijeIAnNFhSrt%}ZhRiH*&Nn(4VdUBsl5y}H$sW<CTu&RpPHkru z2{zPRw=?7Nch|V~+Nz`j$(mhZXuf>d-{slmG>Ky*71hc|@JZ+j^U4}FpbYp0U_6EJ ztv?8dhX!@4h8+0F{ zspV@&-LuckS>ZrQ4B0fI{Gw}oYi-@n9|POWdUP!$j7pVie%d%Ni}Et^GjY8;tn2Z(dz}xw+JIUddEg$ON<_;E+0a zJ=xAx=QQy3*nBaG00Xu-tSYsX91hMLC9~f+?2E^9MiIV%VZu+b+z^uO^QQM>ZLUxj z|89zb6C9Gim|rqK{ZLL?_W{J6owwk1@2R0K8aP?Nmeuu96DetdgY--Zjlg5H{0e{V z>;12#e!sAne}#}y`E3-`rGjQC5b@|Du_kdBGTw6DfTC?GpS+02Jj+&$==FK~RGWLi zy#qjdlc_i2t=I@SVchWz;Is=K^NwNgLi}Z&hhAu>L!O#nNkDczB&kdhZ9#5uc5uct z(pQjZ1PcSooqiF&xrH30Fz`odF2*XleYtDt9jC)8b7SawZTgb9bgWr;)AXg*9b({t zn(nCC{WatsBT>eWDs!ZY#wU^$<6=0a%#p?TxP&+L*-JgVnZk!bH0V6)h zEvcmafgP%JD7teZbnh|b>Lf^UrlAoi<*&38Q#7vIOY858nIE9EeR=5;J`@mWO{0k= zL3&%XXT@<6lLa_(e^-ZD4Th%>pV;Uj)mZEvJR-0AN%Lq%vf3NV)yD?nUg9THqXz5j zObRps>_hku)1kk&_@~-!qWpoOe@0iyn6Ec>zh$*}G=fM>x3`Ezg$5Mwr1Ya~w4v#g zrJ&$US-cYSj}y=y{7E)WakVb?LTTBm3L9~Q-;n#CWN5_%KQ={Fu}29yr1CN9?Y zuvpr1hP|{o$}E~TEZnYO%KYfULzUgXQKxs5V)ziC0M&pVON~OkMnITc%Rkc`n$!eD zvXOr&ECH~H_YqCj-Kc>$^NRFnK8$mlqnyEyB^-1!2B&YUq->$psCG6-m;)7%%Jkw;CM1Sn`Y_KFXiM2`_7STU z__#7n&E16y2dlA#*1slC+&m0>3lJ3jd(mz@GHOE^z{a8W@(TB32=$@&aXj4Pvx1}T zW%7frPfE5pb-`ApUR-OAw!uNhVwAyiM#c?cWo|HzYN6>~07QtyV?SJT?z_77x%NBY zaV?)I+D-#szUud2pHxiT+UIiXz;JOYB9-%D)X}m&5C~>uOItK{Tn*JlLBb zX+j65nMErBo}Tdhxa_f|K!3&0 zsZ_Aua<7lybPb?E+Vs{8hG2WX^h7_l@D}(;Mn{mZ!En%p{hm1HugD`7hX;=YEdS06 zP;JaS|ASV10VK3Tt){V1W$+Y^?wkf zXz?KM35kr%$svp9(P+qnBdiXDY8A8%o2WQoaHWSX+{Ynypfcn8_ca4a&^EZ}KnsMg z(;$ETH(Q;Lt(%1d2bsR^#7U50xAHga`2!*Cl`6)ZC8LZ>Z2szR?)CTu0rltU^yZYx zet-C$6Hc&6$O8Q4=FI}mH>;TH)?s30?Iyg_MV4Y}2}VsYpkGTIbc)AZGjWTIyaC6z zpnNx4CC3{6D@tJor)q9Kr?L!hI=%zYRrs$XekTBg`O}K>Z%M3v9JK*7*WCvRwmT4d zmrxSki|AzwYdmm9>e5v)W_a1}(n<2`jg?gDrct}-Ui|Bj=)gkS{9<{7OAPhXjE+^HCW5& zTjsh3{HF3ZkLk(z?SH+)wnyxNZ3csCRiBcLOdf%}f_?0o?YR?NGMcdD(+TX-HYX84 zgSC=vc%s#3g+7O!3t!!SH?(TJ?r-Z&T!e1(Ph?Y4)%^h+nz-@nkFYHZ5}N_hvwmI6 z$?O->%vM@Gql~VUGLoY|_9)U+t2Rf&aqiiA35Wnbi8QrMa##UbQxB`S*|#$mg&Y{VQ_1xjm?)jQrQ&Dt{9{;hUOf zA=VNR$f&l%1_a-*l0n3PFH;>2aqTnw<3zpkZ2uHsdR_}1B=sI3A~;A*C)6;r(v%|_ zK#vgk%&TjK*KKizIT%ceJjW?-x-$7o1d_);i?(%pB^F)*X=-Z^qg)T^+>m6)vrp?z z_yMYZZIoZHm<4R#j=2c7$&+m>#&igSQtt?lZRCv|$b+*8!N$UVrRT zZyP|#ot{O0ED_xPz}_l>f84X+#@IZSZ(A1n-!tnsLF^!8$^*xxYIB-3#3Z%%{NYmP zK=|8UM6)3o{5D87QRaJ0M~6bUcTG&lMlZ=iX=z7{hoa}NbNMIzV(94Ye5 z4m~pakDgXFydB&p+loGG6US^BrS^-BUmn?(bYD=A_4oeqHr=-Ut^y>WlCgCsGO(noW(Jf>2AnZiO&b2d zg03r5Z@LneqkuuzP?nl>i-ZfbuXz|xiiGfC1pDb!tTn2A(blA387kXirzYj1Ty{Ly zeQm)`ZP2RD5pOjRb+$;i=q}%gP5r6{vzyVx7W=|d_ac8p#cnJvS)Y6J1D)~hUxMAl z&+J7_IH+ott&&2{=G{*v9o7W(;so%%$=kVXY5pB?C6Cjl+GzdhbxFINq~Exo%P0Vb zX7^BaXej(N$Fj;PG$ls{PE_2qM{K^p%l8hBd@gbmQct>T|ISmrbyT1ZX-f87e=z9} zvaTuP>*F#ULvR3Xnzc~VEg)|xS8E&7BAZ5&4bBR!!eNUM1m#^3CV5{2kM2UU??SM> z+b(O~2*|y&jL781AwFNTrXB82w=MudF8VHBjFK4lq=`_c-Becg}l2 zW+-jK^Bu?xMe$*%6;I!NaW@&3=86YLzpOdSbX1Or3LyYFZ%=m67!Zr7ef#I9b_gs# z4{X~d@oVj?)>3a_qplzF?@T+sFQ)G%F6*@Z9jgaX0vcC*`G@tHj^l5_@gc##*~UZ03tKa6r0LdbXbxa+6&~3>UygL< z>m5!j?gsQcc#O*#)rv26=1`H3F6mWiVy^)sLae&!mi*{fizNGX1aUu`!6|d304_A&b_td zE$_m?(kd_HN1fEXZ^ip%E7d+)KcnneOY9N6OudKpH!pz5FAx!k(3%{eXVYw`^&`;L3G;>*9m zC3Za2ul$TdJ|hJRkjswwr;ODdto?DuAnF|`Qxyitp(M(a+~1(rB;j)nB$&`TV5f&d z`zFeU_IM5RNe`?;EJ=g#S(6qzx@_YoB8k_xO-?|lU<6kDJFY1=TL0GRbmtXV5f$1+ zscyOT!w3-qa3uNl#32)4$fm1g{S+ZVjNbk-Q5w!a<|%A&cI!}0qC#->`21FrR28mm zo$s%yAlEXP7 ztzxF3&zGx=<0?1@Rck0ZSVyFR3w11pMerteAfNS7q(Ruv*A93>%02?ju; zo@N4DH_@q>DkCz~7vx)1#}@y4HsplZZy^BA3U;*8_|DjoGXARASMC!mh~rpA zY1Zo;<16;2FjY;KAr}eRj^FnPi6~S^(<3#j7d~P$KpWHJ+y>+UoAcD|T?Pn67ro41 zU3V&! z)*1f!`iZ%u>IzQE&aA>oRYC_psgK?EEei(NWOQ@pq@epV>4jGQ1`NCK()h^p;93J3 zO(SCT7i{|19}k_uIc=7%K6(YjoTCT2z_bUXoF^b6F4YAQ=?V4GgGQTC);J)!5?v$N zpZMUNJGq$EuG8UErhz*`Ld^4r035&5EA)p))<29Bm*@GT@3D~~^gXj#j3wTUDv3d?prTmFfu* z!)<6o+V61NIC@P9+#3EyM9lY__9(gev<0m0xEKJU=vr@pCB=5J^Y-MrqtCkb+iAm6V_i*fLo!+E#9s=gP;B*eWxThvytY{Xtd78!>!TB6>YnjEEy#MA@v zB|TdOwdXwoz?xG;aE~HKtlqZWho2&d1wGZ0YF5cP3LLYc=0o13vO5b~CTygvWi(cP ztrrY^R!;umJ-DRpeC$-$nOTEVZGzvWdeBw4MW#yTYvirq-a+67_32By-DM?PtI=)v zHK>0kBLv`DOf2Cn2cZo*xg6SQH<=tS?d1SV1x*d~Qzxx;6XGdM9}C_TW%lbpT?Hk! z=#H?Nqn6y!swvJfePx2qMB;#{-rLRZS*z3%M9$6NN(e&gb4@uR-;CYFx5c{h^2&@* zzZh^B8q0alK4pew;w^ssNu(b;OfA1QyQ{99KY3LrS5|Dv%q3(8>!gp=a9KY+zU1pi{A$_^iNDWYc-f7r+de+$J*> z{wvGK(5VA%0AO-rW6k%k2!k8kpi<|?jTY8RGJHrP!o6okE!vv8Q+_{5-s8&_gktRs z`pQ{L4YjY^Ko6XZlwNdWugIfzsGBc+2L}gCR!MR+vc@h#=ce*rrz=?Z!AXsIh?;Kb zhN!-kHaZO0Ct~W)w+1)zPW0o9m1qV)sqH0Y?o%Q-6Fc7Gk#%mJjEJjF*WkaggU~A^ z0pbbH4(p_2G18z_v%;^R_WGmhZPWwg8n@qvkb0!NE}Sf4CuOC*Z-stRM`Yp$aKt|k zMLBW$PK2vUj61#>wKR;x*IF1U$pzZev4H*ye95&z8%OVN)p`zniX!z;12I$@;N816@kOIQKaUnv zqn3q16Oz0<783-I9kw%`W;4{}7B~F1EUNm{jl7uOI)CpH;NW+>{>!szs@@>`^c0?m zvZOQ`6`wm?TL>eNy*4~4;~NxGWM_YKze(SX@)_ukr7-l9b;s$dd<8hJe! za&1)k_Bj1*Hmj4554zc>fL`b2m^uhHOIC?v@jFfIpe6}0bU!ZENK3lh3c6C0J27E6 zxQ!YWHtXIPnb}gx;SH1^)AJYV+Nga&^=mr3d|k;x>djA_W^Id8t*y+zTYuKIzh!&- zG6)fHseJNQC*|5>>0*)A+t)pC)xK?KfTFI~?^&+@yUVl?pre%v=3NnC+r$N~DD z8sPfEGCn(eAd4IZ+NBt$P2TgZ&1C5maaZD3A~%ppZ*d!HpVwvOYCvXHyQXoZmGddc zfjQ>O{*77xb~(y@hbKE|P8Jl|^g5XuqWB(Gr9LB;-0#8h<cIxjeixGLXSBZ{O-Dj=W?SUTzHRQz2LU}*fWp9n3F1H{APpc zA%QMV$+W~*9jNl9n0g59z*CYp(RW-@d0j?ezxaD(vB;x3wWM%0FcttCQwn zO|M?lzmB#VrORpE;RbD>hy$u@D5fy$rRDzT)LPK$UA?9xzW|PnQ)gHs44e9AnN7Fi z@b6QEgZd+9mM<*V;OPOrf1oBabylsS4?^vWtj-r`YA9yeM1h_zJe z>gfXZq2_o_YX4(tT$wfd?y4a)Q9eD@IPke-TT0se@(~K}FES(~{a-*%cJf&=!kuM< zg{#u$!-wR)VTex#dLeyY7u>;H@7&pnqq!kJo|QT_1uk`sTGpe$b!_z96&+Rq|!)oG+XatY6CoUCC z20Hm72@^05H8QF+kaw55x2)rgz0~RGei*^~@w-o-xOF6UB!}w%+Ngem(IXet&>0vn zZ_9`ZV8Ud=EPo7VNl_=@Lt0NRc1mmq>F|Eh zouwx*a5pVH@7Yk39q5ycFALMfv+Lc+F^l(iYOnEV)L?F&P4t{V`LVRlP`r6uFF<3CdT2$z2BGn1er?8m(GTINOIf1dY(oGp905s#}ud(%eRM z+VCi%LPAVi@HD@e?rGKuf3mCdP(i9cem&G)R`Omjz`sgHE%a2tmcA?w5IxE|&k44# zEatDg0ZI_Du%}OyYacKW3dQ#%lm{yW_#GRj8NoPv&?i=YxtY>%i_;*~%j?CBYGFl2 zqy(P(`{Nt1Ws_%Ua}W&5HX8Fz-aGf%LW67zeR^bqh!tjEA>n)=!+NISgH(VqP{w&w zXWhCr{IA!9;oJ|$@KUvR$M<8#U3s5DgXU~Ue1(Z1bi)ayRC^)Fn!xYa1a>=4-z||` z=}Bm9CoUnF-gCf$J7BjzPs1IOzJ04?`Rs)d1ceH;%wpJIx2UcV5kY&C?N1){{66iT5g8C*EQc6oMc(`6)VH&vV`}U6Apj4MtNY)pRLY&8cTW&){nDFYM|G%e+{tAPx z@YYLNECNAeJk+}t+} zGUmTs+5+pD&`!yX`U(BG7nxkC!zhX@COof;ncVJ-5*$G-G9{ zndB0vu07MlQKlMDk&|&J{*+_*3UR<>qwRo6uY*QHi4Sw~J!_&cR-Ux-(5CIRUAYm> zxRYmyL#`M@|8jWl%gq{|zpjkWSZ6f03}_AY{O&ynz-L|N>@Avk@P!h~do}V05%u-H znu#x@CttdMqkG8`QQp6+0@4e5wCzY`JR);dr z?SxfT*R8Di;WXVS21WY!$IUC&eNB09XyF0)5LVai6qVfDR>lfxAr%K(w?zA{ibfg` z^_%51Yvq{sj-Um zbl>1=*cI7ELzHs9nxFq{Ei74?_vt8XzPbEL)36u!0~ z4;|mr+`14IMz$d>PRU1u;&8mKHiNSB$JfP-c)H_gh`-%mHIl*Rl{tMTyaSb}U)>U4 z*mth`%nmL3+%g?Zv-B2sHDDx0uI@6pB!zKg6JQ;Nj`rR$@ttV#GdbXz$8K|We$?Il zr^z!=BWA{0RfDN7{Z`?P<1dQGkm&@{k@7eo^LYzIEhM1g{Wr6n?BLbHSkuW}Ns7gI z^w>d|UgA!GV98OUT}rZkB6$4mO6FTf6Dc2+;B~9-rVGL?vPHkVO|Q}!YyB#J1?2DO z=STo3&TV``E5$#$fMC`)jTFGHSFa5BQzO*3^3l5-+&9a%Si7h^hUNk{Ud3Pjtou-6 zl{{CyBhPcwi^hQH_NYvQ+KjQM<(NNi%l!_(MoP6VHGy`?$6^i$%*Gy9=Im>#ja_xi zLe0|iS=P&Fa2o!ikm&SoKOXJ)sYPRxDv8RtrqmGeIqyJa-3NV>Y*ui(*7<49#Jg)~ zGnCi%yFCXb*n6m2g4^;DN7F}k$h2;dUs|AirRShtwU30hD2C&S=RP?PmS z4{4WEpzXts4{J6e>D=&jo-rBRn#c2{?+ITku~>f+xEXqdnAqUX5jQxo!Cut@>?mnJ zxq%Ks2%crADuKq^s7}u4RF9K=egp2K6?~%nOjP$=_CUVAp|=o~wfDk)_8qnDe_uCK zFVWQ1*?B~7&t*@|(C>JBZj-QEL~*#yh3M#3V}u8k%&2{>f9P4Ju@QsFwj&ROU>TJr1O+xD(0Y zOaWXGP_vJrX}KON@kkZWSndz*rAr<^qMIt4AA8$mJ9rKfqoBUr(Z=m#t-1k^Kf9PH zsa%t{qw4Fm`1g2I?PQSaMCRB5+o0KT0#YW56cjOS-x{cl{xD*gtuU(bF0hf7n3-8@ z``@kbF#P|9BN$vpLiftq-9Cfk{$`(U+WKtdm!>?mG3OT9wEJrY6j)$oo4hv)Bw5_n z$UveLTHU(t5jw7feaw{M7#qVug$i!68p@JNBbVfrBmApX+~>7C@C75`n38#fn^X44 zBHUOv`}kSbIMrvbD}m`b67p-AiYEMTg71`MJOzEXt@m2K6@{a)6Lm-8`Rm>@3)_88 zg*APw#F^;~+S9U+t?wZ|WIsycB<(3MBffH1b^Yat&Guk5yIYC5h%b-I(`Em{P%*s9^dyHO{vzA5zi{cMmFijPdV+bIT8dgih8Tf8O53oOcKN!10J7i?)&FoHTkZA*+(iBP~$qGrRLHSb$q^I{W1c zLm7%SXtuY;W{|d4Hgb*SwP$XHhMfVO6uA&@OM2ru)RhDMgExt-b?vm zgsXfDHy3iaDOjR~6L60uC~=Ov<*6WN*i}I(ILFeDwz!(Cb$H-(GZQFjai|Qlme=`` zPzd%H@ig!FNaDE2OC{F+Hu3W7V-fTrmevfmqE^Tn3FdChoC{i4t!^TziZ@Xo9WEucVr|Ywm6h7&A@5J+QgY%J}7q?nSx%gRz zfTD6hBeKwDDEq4IBRbV2Wq|j9jFkcX*aA=8fh{dEZFy3}skY;sp4~E~cic5o@Uq!N zl4q@D+xDX-rR>*#02Hf4a#e`yb|!V-w-^ua6A7@Wj#J`(iqXN&5xVrWr_! z#L8)3fWfG^$5Y!+i+(RO-HmjOViTJhy`Qs@)&+qKZ|tYau+)D0q|6)~6ZZRPTSl(W zlJ3Xy&uaxfSf(<&-*t)^aH`h4biU=Qn*CAj*`gM_LkF;5I-OUbig99AkT2c9?bFB? zBoBjE3KMm9C7%c2L0+2&9R!%rOlKT!QT)u;6AmVq-11j+g%JRu^wdAa{utgmA%maw z0)qB;-=1RAt39`RW=nssYzwtL^HjR(#bu8EnPUpbLe0QvNTtUA0}Ys|LBI$VbU8)b zB$sAOu9JY5v-{b%D@Nx@%VfGuNolIox`Lb#ZCJ%n8Kri`!cquM$~=kscWe z-MPMrj)hy}BsIM_;u{f<-_rBzTRt#8^xc~Eh+&s*r!2zzpCq9uk&#}*3W21(d<*X| zoJ9GyA^zPsGRN4>VV#SpEOXcQj562gl`mOERU~>*7^{|7nRcq!=2qwZQrJhG?om<_NM zggU$w<%yoVa#M*a{u-!nhh$+TF^Hu??fwj)qvtc)FyC~TjZ3|!)47xb4Vy{wZjoT| z5mnMcheHT4Y8-uZ*QA&phd6nd`k8w3`{p86O$+keNu;=AJL;ZJJu0NUd zLS_zuGh5Igwd6b)Mu;cRI%bZP?!zp{gZxmUp8XW@}MYHc2Kcxv#a-Ey63xO*Y{|1GF_bBE2RE^a_3S9i^`|BB0}-Y7 zDPxp~(2>uVW3U7!P^aP>V#wL}^bc(luWfzFoHsf^tPJ&>$ zXj^7a=y{7`+uep3r=!s(nft=ATh;J58BUf6wLXq4a+X9L^1%m!r8g$C928E}ajCF2|k+(p?2BM0|vN9P!ULg5*&CX@>la1?`_bG1s<41!#fbXAZX zRslAs07DsWaWHCqG9Ko-Y4S7`GsRwzF(f3#f3wljBSSM+cjbmbEfL#**N_Gg1h z+sc>+I5lOlbxFJ01M$HmS3jpE6C{z zVL3U#H}|>zuR*8!{h_s~rpQ2~>dlp6iaiDv{P%d)2K(8SY_^ZK5vW`Df&jo43iMN1 zy6sNaTqAX(Zp4kXHlegGV@<&|m_H-gIiqRREDef-O6)}7NmyvrpV{^9GtA%hNHQr! zhU2jwyHMNFwW37WiG!+ zoVja5eL*)aW#;0+%`~lkmltU)7)$ii?v1@*UEoy+HmB5OdcS$us9p9IC+3~AcpSXi zyWOMtv(1E;u@C_`cH_C3oqFZzPP!o%PA{^SlN}m;o7cDcB{iNO>K`xD=&f-wb&VJ| z*O&5_<8^P}OIr+bUrbi;a)p~``gL>=sYiD`D&vfnV;o#WC zz@0!2qpyfBX-_K5dj`(#bH^7MNawt{r}zW7ZcZr;Z+^|b^jI{8)8Q-xYpc|^d860Y zx6b=NGn#o%*83f7e6V~+NB{+h3FAZ>C{Yxc)7eDPde(68{S@MC|$TzuX za{usvx;JncI}S;ju=JAz_3Y4n0cGYK1XI^*a?sDA{Y>U{b$%Z?5(NpO76#5y#=<8E-m1s=7aGDDY{_DOz0075Ov z;(LA%?^A5kMUnzJ>WsL;%BxA|g#A+ECzdj5NBJQu4P z8fa=B&v9Av&A`cKdx_Yg%QHX6+XA)Z1QoMc9^+7$@kCL(on>~1s$O~vcxl>t@(0#N z=m>foNa*-=<{7cYg-tPjWf}r7KiZn`>2ss?qBIqrY@A|jVzk_PnLO>9tfoN@{Fhbz zg?QaGwxPbZi%vIa1UU97EIOIUgn*s;_#o$=3@<{1%i_@Q!lu>-K^y$Lqn)vn6GYs zo2VJ^eb!U+H{XJ%*-tL~Jn`G>x6Stcxd}blV7>TvCFzlxaGKrUJTh^ZXAw`ZZp01lXt4S;o?Kk#?-~y$@TE6hJjLah(St+Qe9BWH` zz4tZY;DyU+aDh7Tu-5YC?c8~lXc`=LPx_ENm-y#*s8IVJ%%lOl^JRHi!&TL;p_vbZ zfYCUso8DED&iZ#~7w^DE3^qXFxgz?rkG!=+?muqzaz!_}^ripqb8K|B6ypOFg#4ND z;?$XF4hcO}FMJEu4!4>Op-ne+*ad}VKb>K#2;UfJ>jUq)wiEfl(SKZoN1wd#3O zPc_GAk(u}N8$zI^WQ$&{VNzWO3UZ2`)hFKo*bR^u-rCCD=H;Q-a$m!_#{sfcmeG{hxN;g|{A-S- z(|8gOTCC3F5Lu}MPtaxH$b>$ZH5*TivvNq7K2x=Y#(--Yz&QGR26dAvNTp^A=RT}j z=eID!lELZx&1wRuldUu)$&@&0aTD9ul^+QHwB+v#Pws=BFZ%tqqO@>>j!-Vc`Nhv}4#oLJCQ zR+#H>MI0PXFEWUJ^o)$RUB$^fv{;hYK5RaD)^T4l9tg#3@jFNK@97T_HOa+>W9pZI z%8->wfWf{?oabs*qnO~9fO4kI6Z6YyFVh$%R-1QgY_i3 zOlhy$o+r2g^>ixF`-gNSB3faBQT(TPv&A(9d1m8oj%cKXJSjd9Coj=UL6L>U>QX9X zrlxb?DyV<=V&Ose78%*OE1VoPu6YUY>6R+oT}h5QrFD*l{(C$c^bUJLImDxW=+B{^ zj>FLUZnIk}8oey44H3|J06quxs@~S^@oXm)NtCg-*YU@H*xz>T1w$yVYpHn z#mR#rPSnOo11TL%s#?%G6?tZkt$zU$f7wI$nD;^1lxh5AFGL6X43uPqI?Vl-gfY#cU9 zi`G>Dg5^D2-10bWOfcgFt_=b4jk?Zrb77mZWyb)K4HRTRMx*P@KW4wL4US_pe)Ci~ zPI>ZyFE`aRds|r|4S>st)DZF&P^R@apXyOO?ZhDlVCtE@jrylWj|5=^D^SqqGpU>+ zic;t89sm3f@gG1y({ysLi2PLb9`p-O?28kAe;bVY>b87%GJ?*YiNeQ;Uka^(Ax!u^ zs>fwi9dR2lHPF>uUCT&yr{|rgXE<%uc{esZze5p!YHn}|;D*A~r)P7$F7m!eY&hau|VFP?nio-4@AO z?dIz+@8yich@hz+Ut#KEkrO;G6WGT-sSUZE6Cn6HnGoQ42(eg}_w!%=!%^)0HC~{y zozG`ksv9dB8*4s+XIp@*QMTP&_`D+D^k{E_ljgMAfF)jrGl;J>;fM0%~P`1KA>AR*ty;^`Z&k}ray*;y*wcK624PJSEqWNT$kt2Rcmy9+;rV(AT5MpB-l87HkU_?l>{@nR$7r~i$)IYckOd->cp+FxVv1gwkS7NeSrsv->lH_e z4>EbIS;5V@@uBlA29LVCiE7xzu4mmI|IL=(*MND3C#GEcNSi?sRD`=Gxi+bhKetQP z-yQP9F?ztq_aZ+dNn2jks*0)PWnO|%8HImU7?y7oKtIe)%Mz=3kG1h$3BY>C~MihzKuWf1EF$ zUkq9$(gXVGot1v#i3NORe?=mIV(nFKyG59;mTmi09e*TvTn;Ctt86R0Nrzi6+xV%n zx;Tyaa|3uFbbmO)&n)}YL*G@Z`s7#a)PLxbg@yAKvD_f+>f&#DK4^(h`Hnw(Cu^y3 z`g(!z$+^QpZzMQe0pbOX2@_9>)d)MM&pL}4t(A4Ne^Iy&g?eUHt8X<4mYXQ>c^(gp zx8*Bb;6paz`*U4sl`HB9;VzRp$yxe5$kS*%lWTGlBlzOT;XRjV=c2{`MqUU2{Pcw1 zh&I7wdGG-O{})6MqB{OEo7TgQn`h1Dna{z8^Z$)b#X+0L>J`?-1xH7D)vbbWcs`YVE#dma{9~AwVVZ zA&TQC(EVpx6Ot7T=Rb+yWq@q}01R$F8d!2yH+aNhG~)_lF#VzgHTgRPjDwcrm*cC5 zwewqEHC)ekJFi%(TgAsQ6}0&7h%fQ}{p$)Z5c)|5JxlkxLbQnqlI-HuG?hpA_YWN9 z?0g*N6)a+I>vB2G247`bh?L{ zx+5TC0OPf~{g9wn{fB0>+&3J*Mk4HD5+f0)6>NQSAS?FrF#@SPetQThSW$T2VYeCS zS?e>gaBvr?O?pAwW=IH^ePI7`ZTFSMs_5V%8;5_qMXTWrvobS8la6-GbB3mrv0En^ z(a732p@|OBcFEoDo~8msYpJj}rY4VxB-N43mqZt2gp@2Mr*vkXU84*MzT%(rkWy?& zQr+F~4wI8rYtQ-dV~OcZGXxp;ArgUbuR+=FpkYhn97Fsb6PAHZjJ2B|Yn>t$f*hpl zBB#du7J}Hvpo>{q?NUbR{gtXv5icbeHv)-69Xp7Z-%g11lYV9h^x6 zU-AdnbkuX;M*ME7|Du37ZOUvw946W|kw};poqwH3ZEiU)nw6cM^P{r&9}k)eURx|u zwyCz;9KL(FAB%sXL7}K?IIY~*_~9D@@w-{_tvNe4@1yy6Mon}S$tAGR(*wd;hZI$c4!Pot!&OGO7-?}91+fBO@#abU9UWpvdt z!4udfh{IL!sjG#VtEGgwvnBWi-50zs#CPu=-@OOA_XQ>Hi%AFy-xU;;5EOLgI}-bU eec<3^VPoa>|NQ~ME6s2mn&FBX3Plf1gZ>YTY0uaI literal 0 HcmV?d00001 diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart index ea962acf..b5b45576 100644 --- a/lib/pages/sign-in/sign_in_page.dart +++ b/lib/pages/sign-in/sign_in_page.dart @@ -1,35 +1,76 @@ import 'package:flutter/material.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import '../../internet/api/results/client_result.dart'; import '../../internet/api/sign-in/google_sign_in_api.dart'; -import 'package:quimify_client/pages/profile/profile_page.dart'; +import '../widgets/quimify_colors.dart'; class SignInPage extends StatelessWidget { - const SignInPage({Key? key}) : super(key: key); + final ClientResult? clientResult; + + const SignInPage({ + Key? key, + this.clientResult, + }) : super(key: key); @override Widget build(BuildContext context) { - return Column( - children: [ - ElevatedButton.icon( - onPressed: () => signIn(context), // Pass the context here. - icon: Icon(Icons.email), - label: Text('Sign Up with Google'), - ), - ], - ); - } + return Scaffold( + backgroundColor: QuimifyColors.background(context), + body: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/logo.png', + height: 175, + ), + + SizedBox(height: 50), - Future signIn(BuildContext context) async { // Accept the context here. - final user = await GoogleSignInApi.login(); + // Google Sign-In button + ElevatedButton.icon( + onPressed: () async { + final user = await GoogleSignInApi.login(); + if (user != null) { + Navigator.of(context).pushReplacementNamed('/loading'); + } + }, + icon: Image.asset('assets/images/icons/google-logo.png', height: 24), + label: const Text('Iniciar Sesión con Google'), + style: ElevatedButton.styleFrom( + primary: Colors.white, + onPrimary: Colors.black, + minimumSize: const Size(double.infinity, 50), + side: const BorderSide(color: Colors.black), + ), + ), + SizedBox(height: 20), + // Apple Sign-In button (assuming you have Apple sign-in set up) + ElevatedButton.icon( + onPressed: () { + // TODO Handle Apple sign-in + }, + icon: Icon(Icons.apple, color: Colors.white, size: 24), + label: Text('Iniciar Sesión con Apple'), + style: ElevatedButton.styleFrom( + primary: Colors.black, + onPrimary: Colors.white, + minimumSize: Size(double.infinity, 50), + ), + ), - if (user == null){ - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Sign In Failed')), - ); - } else { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => ProfilePage(user: user)), - ); - } + SizedBox(height: 50), + + Image.asset( + 'assets/images/branding.png', + height: 25, + ), + ], + ), + ), + ), + ); } } - diff --git a/pubspec.yaml b/pubspec.yaml index 8cfaf724..1823f675 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: fast_rich_text: ^0.0.1 webview_flutter: ^4.7.0 device_info_plus: ^9.1.2 + google_sign_in: ^6.2.1 dev_dependencies: flutter_test: @@ -45,9 +46,12 @@ flutter: - assets/images/dietanoic-acid.png - assets/images/discord.png - assets/images/gmail.png + - assets/images/logo.png + - assets/images/branding.png - assets/images/icons/logo.png - assets/images/icons/branding-slim.png + - assets/images/icons/google-logo.png - assets/images/icons/search.png - assets/images/icons/lock.png From 1e9daa5b12637a2c5885a3fdc59f15494b35d577 Mon Sep 17 00:00:00 2001 From: Matthew Horne Date: Fri, 7 Jun 2024 00:48:49 +0200 Subject: [PATCH 04/19] Sign In page perfected --- assets/images/icons/google-logo.png | Bin 38249 -> 20103 bytes lib/pages/sign-in/sign_in_page.dart | 104 ++++++++++++++++------------ 2 files changed, 59 insertions(+), 45 deletions(-) diff --git a/assets/images/icons/google-logo.png b/assets/images/icons/google-logo.png index 39ace85d285b2bce8ddde3763268983ac4730351..001e1a21d74893211d3b4e37350a42bb56e44715 100644 GIT binary patch literal 20103 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 diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart index b5b45576..a6361867 100644 --- a/lib/pages/sign-in/sign_in_page.dart +++ b/lib/pages/sign-in/sign_in_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:quimify_client/pages/home/home_page.dart'; import '../../internet/api/results/client_result.dart'; import '../../internet/api/sign-in/google_sign_in_api.dart'; import '../widgets/quimify_colors.dart'; @@ -17,57 +18,70 @@ class SignInPage extends StatelessWidget { return Scaffold( backgroundColor: QuimifyColors.background(context), body: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/images/logo.png', - height: 175, - ), + child: Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/logo.png', + height: 175, + ), - SizedBox(height: 50), + SizedBox(height: 50), - // Google Sign-In button - ElevatedButton.icon( - onPressed: () async { - final user = await GoogleSignInApi.login(); - if (user != null) { - Navigator.of(context).pushReplacementNamed('/loading'); - } - }, - icon: Image.asset('assets/images/icons/google-logo.png', height: 24), - label: const Text('Iniciar Sesión con Google'), - style: ElevatedButton.styleFrom( - primary: Colors.white, - onPrimary: Colors.black, - minimumSize: const Size(double.infinity, 50), - side: const BorderSide(color: Colors.black), + // Google Sign-In button + ElevatedButton.icon( + onPressed: () async { + final user = await GoogleSignInApi.login(); + if (user != null) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => HomePage(clientResult: clientResult), + )); + } + }, + icon: Image.asset('assets/images/icons/google-logo.png', + height: 24), + label: const Text('Iniciar Sesión con Google'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.black, + backgroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), ), - ), - SizedBox(height: 20), - // Apple Sign-In button (assuming you have Apple sign-in set up) - ElevatedButton.icon( - onPressed: () { - // TODO Handle Apple sign-in - }, - icon: Icon(Icons.apple, color: Colors.white, size: 24), - label: Text('Iniciar Sesión con Apple'), - style: ElevatedButton.styleFrom( - primary: Colors.black, - onPrimary: Colors.white, - minimumSize: Size(double.infinity, 50), + + SizedBox(height: 20), + + // Apple Sign-In button (assuming you have Apple sign-in set up) + ElevatedButton.icon( + onPressed: () { + // TODO Handle Apple sign-in + }, + icon: Icon(Icons.apple, color: Colors.white, size: 24), + label: Text('Iniciar Sesión con Apple'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.black, + minimumSize: Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), ), - ), - SizedBox(height: 50), + SizedBox(height: 50), - Image.asset( - 'assets/images/branding.png', - height: 25, - ), - ], + Image.asset( + 'assets/images/branding.png', + height: 25, + ), + ], + ), ), ), ), From d1862deed702ae9ec156daae755e9f3f1c4adf35 Mon Sep 17 00:00:00 2001 From: Matthew Horne Date: Thu, 4 Jul 2024 18:34:10 +0200 Subject: [PATCH 05/19] google_sign_in_api.dart implemented --- lib/internet/api/sign-in/google_sign_in_api.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/internet/api/sign-in/google_sign_in_api.dart b/lib/internet/api/sign-in/google_sign_in_api.dart index 0a9c482d..3003053f 100644 --- a/lib/internet/api/sign-in/google_sign_in_api.dart +++ b/lib/internet/api/sign-in/google_sign_in_api.dart @@ -1,9 +1,14 @@ import 'package:google_sign_in/google_sign_in.dart'; -class GoogleSignInApi{ +class GoogleSignInApi { static final _googleSignIn = GoogleSignIn(); static Future login() => _googleSignIn.signIn(); + static Future loginSilently() => _googleSignIn.signInSilently(); + + static Future isLoggedIn() => _googleSignIn.isSignedIn(); + static Future logout() => _googleSignIn.disconnect(); -} \ No newline at end of file + +} From e8497a159fb3c40a3d93f4f458756175a869b589 Mon Sep 17 00:00:00 2001 From: Matthew Horne Date: Thu, 4 Jul 2024 18:35:14 +0200 Subject: [PATCH 06/19] Sign-in page small temporary fix --- lib/pages/sign-in/sign_in_page.dart | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart index a6361867..bf3eaf16 100644 --- a/lib/pages/sign-in/sign_in_page.dart +++ b/lib/pages/sign-in/sign_in_page.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/pages/home/home_page.dart'; import '../../internet/api/results/client_result.dart'; import '../../internet/api/sign-in/google_sign_in_api.dart'; @@ -29,17 +28,16 @@ class SignInPage extends StatelessWidget { height: 175, ), - SizedBox(height: 50), + const SizedBox(height: 50), // Google Sign-In button ElevatedButton.icon( onPressed: () async { final user = await GoogleSignInApi.login(); if (user != null) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => HomePage(clientResult: clientResult), - )); + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => + HomePage(clientResult: clientResult, user: user,))); } }, icon: Image.asset('assets/images/icons/google-logo.png', @@ -55,26 +53,32 @@ class SignInPage extends StatelessWidget { ), ), - SizedBox(height: 20), + const SizedBox(height: 20), // Apple Sign-In button (assuming you have Apple sign-in set up) ElevatedButton.icon( - onPressed: () { + onPressed: () async { // TODO Handle Apple sign-in + final user = await GoogleSignInApi.login(); + if (user != null) { + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => + HomePage(clientResult: clientResult, user: user,))); + } }, - icon: Icon(Icons.apple, color: Colors.white, size: 24), - label: Text('Iniciar Sesión con Apple'), + icon: const Icon(Icons.apple, color: Colors.white, size: 24), + label: const Text('Iniciar Sesión con Apple'), style: ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.black, - minimumSize: Size(double.infinity, 50), + minimumSize: const Size(double.infinity, 50), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), ), ), - SizedBox(height: 50), + const SizedBox(height: 50), Image.asset( 'assets/images/branding.png', From c48c646982ebd73466214153162d542bffb7ed9d Mon Sep 17 00:00:00 2001 From: Matthew Horne Date: Thu, 4 Jul 2024 18:36:26 +0200 Subject: [PATCH 07/19] Profile page first prototype --- lib/pages/profile/profile_page.dart | 108 ++++++++++++++++++---------- 1 file changed, 71 insertions(+), 37 deletions(-) diff --git a/lib/pages/profile/profile_page.dart b/lib/pages/profile/profile_page.dart index 534f1ce3..680b574e 100644 --- a/lib/pages/profile/profile_page.dart +++ b/lib/pages/profile/profile_page.dart @@ -1,63 +1,97 @@ import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; -import 'package:quimify_client/pages/home/home_page.dart'; - +import 'package:quimify_client/internet/api/sign-in/google_sign_in_api.dart'; import '../../internet/api/results/client_result.dart'; -import '../../internet/api/sign-in/google_sign_in_api.dart'; +import '../sign-in/sign_in_page.dart'; +import '../widgets/bars/quimify_page_bar.dart'; +import '../widgets/dialogs/loading_indicator.dart'; +import 'package:quimify_client/pages/widgets/quimify_scaffold.dart'; + +import '../widgets/quimify_colors.dart'; + class ProfilePage extends StatelessWidget { - final GoogleSignInAccount user; final ClientResult? clientResult; + final GoogleSignInAccount user; ProfilePage({ Key? key, - required this.user, this.clientResult, + required this.user, }) : super(key: key); @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: Text('Detalles de la cuenta'), - centerTitle: true, - actions: [ - ElevatedButton( - child: Icon(Icons.logout), - onPressed: () async { - await GoogleSignInApi.logout(); - Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => - HomePage(clientResult: clientResult))); - }, - ) - ], - ), + Widget build(BuildContext context) { + return PopScope( + onPopInvoked: (bool didPop) async { + if (!didPop) { + return; + } + + hideLoadingIndicator(); + }, + child: QuimifyScaffold.noAd( + header: const QuimifyPageBar(title: 'Perfil'), body: Container( - alignment: Alignment.center, - color: Colors.blue, + width: 900, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: QuimifyColors.foreground(context), + borderRadius: BorderRadius.circular(15), + ), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + //crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text('Profile', style: TextStyle(fontSize: 24)), - SizedBox( - height: 32, - ), + + const SizedBox(height: 20), + CircleAvatar( - radius: 40, + radius: 50, backgroundImage: NetworkImage(user.photoUrl!), ), - SizedBox(height: 8), + + const SizedBox(height: 20), + Text( - 'Nombre: ' + user.displayName!, - style: TextStyle(color: Colors.white), + 'Nombre: ${user.displayName!}', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), - SizedBox(height: 8), + + const SizedBox(height: 20), + Text( - 'Email: ' + user.email, - style: TextStyle(color: Colors.white), - ) + 'Email: ${user.email}', // Can never be null + style: const TextStyle(fontSize: 16), + ), + + const SizedBox(height: 20), + + ElevatedButton( + onPressed: () { + // Implement the "Gana dinero con Quimify" functionality here + }, + child: Text('Gana dinero con Quimify'), + ), + + const SizedBox(height: 20), + + ElevatedButton( + onPressed: () async { + await GoogleSignInApi.logout(); + // Navigate back to the sign-in screen after signing out + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => SignInPage()), + ); + }, + child: Text('Cerrar Sesión'), + ), + //const SizedBox(height: 15), + //const SizedBox(height: 5), // + 15 from cards = 20 ], ), ), - ); + ), + ); + } } From 48cd414a3a5f2137795718448f431bcf03bd2e28 Mon Sep 17 00:00:00 2001 From: Matthew Horne Date: Thu, 4 Jul 2024 18:36:56 +0200 Subject: [PATCH 08/19] Add user handling for Home Page --- lib/pages/home/home_page.dart | 111 +++++++++++++++++----------------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index bf459c9f..bc13a58e 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; +import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/internet/api/results/client_result.dart'; import 'package:quimify_client/pages/calculator/calculator_page.dart'; import 'package:quimify_client/pages/home/widgets/quimify_menu_button.dart'; @@ -11,15 +12,18 @@ import 'package:quimify_client/pages/widgets/dialogs/messages/message_dialog.dar import 'package:quimify_client/pages/widgets/gestures/quimify_swipe_detector.dart'; import 'package:quimify_client/pages/widgets/quimify_colors.dart'; import 'package:quimify_client/pages/widgets/quimify_scaffold.dart'; -import '../sign-in/profile/profile_page.dart'; + +import '../profile/profile_page.dart'; class HomePage extends StatefulWidget { const HomePage({ Key? key, required this.clientResult, + required this.user, }) : super(key: key); final ClientResult? clientResult; + final GoogleSignInAccount? user; @override State createState() => _HomePageState(); @@ -27,7 +31,6 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { final List _pages = const [ - //SignInPage(), InorganicPage(), OrganicPage(), CalculatorPage(), @@ -133,63 +136,63 @@ class _HomePageState extends State { }, child: QuimifyScaffold.noAd( header: SafeArea( - bottom: false, // So it's not inside status bar - child: Container( - padding: const EdgeInsets.only( - top: 15, // TODO 17.5? - bottom: 20, - left: 20, - ), - child: Row( - children: [ - SizedBox( - height: 50, - width: 50, - child: IconButton( - icon: Image.asset( - 'assets/images/icons/logo.png', - color: QuimifyColors.inverseText(context), - ), - // To remove native effects: - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - // So it fills container (48 x 48): - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () {}, + bottom: false, // So it's not inside status bar + child: Container( + padding: const EdgeInsets.only( + top: 15, // TODO 17.5? + bottom: 20, + left: 20, + ), + child: Row( + children: [ + SizedBox( + height: 50, + width: 50, + child: IconButton( + icon: Image.asset( + 'assets/images/icons/logo.png', + color: QuimifyColors.inverseText(context), ), + // To remove native effects: + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + // So it fills container (48 x 48): + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () {}, ), - const SizedBox(width: 15), - Image.asset( - 'assets/images/icons/branding-slim.png', - height: 17, - color: QuimifyColors.inverseText(context), - ), - //const SizedBox(width: 120), - const Spacer(), - Padding( - padding: const EdgeInsets.only(right: 15.0), - child: IconButton( - padding: EdgeInsets.zero, - iconSize: 38, - alignment: Alignment.centerRight, - icon: Icon( - Icons.account_circle_sharp, - color: QuimifyColors.inverseText(context), - ), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => ProfilePage()), - ); - }, + ), + const SizedBox(width: 15), + Image.asset( + 'assets/images/icons/branding-slim.png', + height: 17, + color: QuimifyColors.inverseText(context), + ), + //const SizedBox(width: 120), + const Spacer(), + Padding( + padding: const EdgeInsets.only(right: 15.0), + child: IconButton( + padding: EdgeInsets.zero, + iconSize: 38, + alignment: Alignment.centerRight, + icon: Icon( + Icons.account_circle_sharp, + color: QuimifyColors.inverseText(context), ), - ) - ], - ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => ProfilePage(user: widget.user!,)), + ); + }, + ), + ) + ], ), ), + ), body: QuimifySwipeDetector( leftSwipe: () => _goToPage((_currentPage - 1) % 3), rightSwipe: () => _goToPage((_currentPage + 1) % 3), From b5ad69671dd5c12dd1133fdb423a8cddc7131441 Mon Sep 17 00:00:00 2001 From: Matthew Horne Date: Thu, 4 Jul 2024 18:37:53 +0200 Subject: [PATCH 09/19] Implement user Sign-In logic when loading --- lib/main.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 15098ec3..f3db0050 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/internet/ads/ads.dart'; import 'package:quimify_client/internet/api/api.dart'; import 'package:quimify_client/internet/api/results/client_result.dart'; @@ -17,6 +18,8 @@ import 'package:quimify_client/routes.dart'; import 'package:quimify_client/storage/storage.dart'; import 'package:quimify_client/pages/sign-in/sign_in_page.dart'; +import 'internet/api/sign-in/google_sign_in_api.dart'; + main() async { _showLoadingScreen(); @@ -30,6 +33,7 @@ main() async { SecurityContext.defaultContext.setTrustedCertificatesBytes(bytes); } catch (_) {} // It's already present in modern devices anyways + GoogleSignInAccount? user = await GoogleSignInApi.loginSilently(); ClientResult? clientResult = await Api().getClient(); Ads().initialize(clientResult); @@ -39,6 +43,7 @@ main() async { enabled: false, // !kReleaseMode, builder: (context) => QuimifyApp( clientResult: clientResult, + user: user, ), // Wrap your app ), ); @@ -57,9 +62,11 @@ class QuimifyApp extends StatelessWidget { const QuimifyApp({ Key? key, this.clientResult, + required this.user, }) : super(key: key); final ClientResult? clientResult; + final GoogleSignInAccount? user; @override Widget build(BuildContext context) { @@ -68,9 +75,8 @@ class QuimifyApp extends StatelessWidget { value: const SystemUiOverlayStyle(statusBarColor: Colors.transparent), child: MaterialApp( title: 'Quimify', - home: HomePage(clientResult: clientResult), + home: user != null ? HomePage(clientResult: clientResult, user: user) : SignInPage(clientResult: clientResult,), routes: { - Routes.signIn: (context) => const SignInPage(), Routes.inorganicNomenclature: (context) => const NomenclaturePage(), Routes.organicNaming: (context) => const NamingPage(), Routes.organicFindingFormula: (context) => const FindingFormulaPage(), From 61e563d624e11c82b6c3060c8bc172fe94eff021 Mon Sep 17 00:00:00 2001 From: Matthew Horne Date: Tue, 9 Jul 2024 12:49:57 +0200 Subject: [PATCH 10/19] Connection to main server --- lib/internet/api/api.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internet/api/api.dart b/lib/internet/api/api.dart index acf1cbbf..f6c27cf6 100644 --- a/lib/internet/api/api.dart +++ b/lib/internet/api/api.dart @@ -26,7 +26,7 @@ class Api { static const _apiVersion = 6; static const _clientVersion = 13; - static const _authority = 'api2.quimify.com'; + static const _authority = 'api1.quimify.com'; static const _mirrorAuthority = 'api2.quimify.com'; static const _timeout = Duration(seconds: 15); From e3b5c36e69ee4937a905e482aea03507e2e34a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Ortiz?= Date: Sun, 4 Aug 2024 14:01:40 +0200 Subject: [PATCH 11/19] General fixes & Improvements Updated google_mobile_ads, sign in page file in order to improve its performance & logic and found a bug (temporally, needs to be done locally) --- lib/pages/profile/profile_page.dart | 3 +- lib/pages/sign-in/sign_in_page.dart | 136 +++++++++++++++------------- pubspec.lock | 4 +- pubspec.yaml | 2 +- 4 files changed, 79 insertions(+), 66 deletions(-) diff --git a/lib/pages/profile/profile_page.dart b/lib/pages/profile/profile_page.dart index 680b574e..817ae435 100644 --- a/lib/pages/profile/profile_page.dart +++ b/lib/pages/profile/profile_page.dart @@ -47,7 +47,8 @@ class ProfilePage extends StatelessWidget { CircleAvatar( radius: 50, - backgroundImage: NetworkImage(user.photoUrl!), + // TODO: Make it locally + backgroundImage: NetworkImage(user.photoUrl ?? 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg'), ), const SizedBox(height: 20), diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart index bf3eaf16..93b26002 100644 --- a/lib/pages/sign-in/sign_in_page.dart +++ b/lib/pages/sign-in/sign_in_page.dart @@ -16,79 +16,91 @@ class SignInPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( backgroundColor: QuimifyColors.background(context), - body: Center( - child: Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 40.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/images/logo.png', - height: 175, - ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40.0), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Center( // Added Center widget for vertical centering + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: constraints.maxHeight * 0.3, + maxWidth: constraints.maxWidth * 0.8, + ), + child: Image.asset( + 'assets/images/logo.png', + height: 175, // Set a fixed height for the image + ), + ), + const SizedBox(height: 50), - const SizedBox(height: 50), - - // Google Sign-In button - ElevatedButton.icon( - onPressed: () async { - final user = await GoogleSignInApi.login(); - if (user != null) { - Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => - HomePage(clientResult: clientResult, user: user,))); - } - }, - icon: Image.asset('assets/images/icons/google-logo.png', - height: 24), - label: const Text('Iniciar Sesión con Google'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.black, - backgroundColor: Colors.white, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + // Google Sign-In button + ElevatedButton.icon( + onPressed: () async { + final user = await GoogleSignInApi.login(); + if (user != null) { + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => + HomePage( + clientResult: clientResult, user: user))); + } + }, + icon: Image.asset( + 'assets/images/icons/google-logo.png', height: 24), + label: const Text('Iniciar Sesión con Google'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.black, + backgroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), ), ), - ), - const SizedBox(height: 20), + const SizedBox(height: 20), - // Apple Sign-In button (assuming you have Apple sign-in set up) - ElevatedButton.icon( - onPressed: () async { - // TODO Handle Apple sign-in - final user = await GoogleSignInApi.login(); - if (user != null) { - Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => - HomePage(clientResult: clientResult, user: user,))); - } - }, - icon: const Icon(Icons.apple, color: Colors.white, size: 24), - label: const Text('Iniciar Sesión con Apple'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: Colors.black, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + // Apple Sign-In button (assuming you have Apple sign-in set up) + ElevatedButton.icon( + onPressed: () async { + // TODO Handle Apple sign-in + final user = await GoogleSignInApi.login(); + if (user != null) { + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => + HomePage( + clientResult: clientResult, user: user))); + } + }, + icon: const Icon( + Icons.apple, color: Colors.white, size: 24), + label: const Text('Iniciar Sesión con Apple'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.black, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), ), ), - ), - const SizedBox(height: 50), + const SizedBox(height: 50), - Image.asset( - 'assets/images/branding.png', - height: 25, - ), - ], - ), + Image.asset( + 'assets/images/branding.png', + height: 25, + ), + ], + ), + ); + }, ), ), ), ); } -} +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index b8f94d03..f1f911c4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -489,10 +489,10 @@ packages: dependency: "direct main" description: name: google_mobile_ads - sha256: d2ef5ec1e1f31137fc241bdeab3037c31062d387dd221fd884fb1160444c788b + sha256: e2d18992d30b2be77cb6976b931112fc3c4612feffb5eb7a8b036bd7a64934da url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.1.0" google_sign_in: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1823f675..c3a1a7ea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: envied: ^0.3.0+3 signed_spacing_flex: ^1.1.0 device_preview: ^1.1.0 - google_mobile_ads: ^4.0.0 + google_mobile_ads: 5.1.0 fast_rich_text: ^0.0.1 webview_flutter: ^4.7.0 device_info_plus: ^9.1.2 From 445f779f3a2148e61ed9ab6c1725dd63f230c556 Mon Sep 17 00:00:00 2001 From: Matthew Horne Date: Mon, 19 Aug 2024 11:33:42 +0200 Subject: [PATCH 12/19] Reformat code with 'dart format' --- lib/pages/sign-in/sign_in_page.dart | 129 ++++++++++++++-------------- 1 file changed, 65 insertions(+), 64 deletions(-) diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart index 93b26002..0ef61c7b 100644 --- a/lib/pages/sign-in/sign_in_page.dart +++ b/lib/pages/sign-in/sign_in_page.dart @@ -21,81 +21,82 @@ class SignInPage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 40.0), child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - return Center( // Added Center widget for vertical centering - child: Column( + return Center( + // Added Center widget for vertical centering + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxHeight: constraints.maxHeight * 0.3, - maxWidth: constraints.maxWidth * 0.8, - ), - child: Image.asset( - 'assets/images/logo.png', - height: 175, // Set a fixed height for the image - ), - ), - const SizedBox(height: 50), + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: constraints.maxHeight * 0.3, + maxWidth: constraints.maxWidth * 0.8, + ), + child: Image.asset( + 'assets/images/logo.png', + height: 175, // Set a fixed height for the image + ), + ), + const SizedBox(height: 50), - // Google Sign-In button - ElevatedButton.icon( - onPressed: () async { - final user = await GoogleSignInApi.login(); - if (user != null) { - Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => - HomePage( - clientResult: clientResult, user: user))); - } - }, - icon: Image.asset( - 'assets/images/icons/google-logo.png', height: 24), - label: const Text('Iniciar Sesión con Google'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.black, - backgroundColor: Colors.white, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + // Google Sign-In button + ElevatedButton.icon( + onPressed: () async { + final user = await GoogleSignInApi.login(); + if (user != null) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => HomePage( + clientResult: clientResult, user: user))); + } + }, + icon: Image.asset('assets/images/icons/google-logo.png', + height: 24), + label: const Text('Iniciar Sesión con Google'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.black, + backgroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), ), ), - ), - const SizedBox(height: 20), + const SizedBox(height: 20), - // Apple Sign-In button (assuming you have Apple sign-in set up) - ElevatedButton.icon( - onPressed: () async { - // TODO Handle Apple sign-in - final user = await GoogleSignInApi.login(); - if (user != null) { - Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => - HomePage( - clientResult: clientResult, user: user))); - } - }, - icon: const Icon( - Icons.apple, color: Colors.white, size: 24), - label: const Text('Iniciar Sesión con Apple'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: Colors.black, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + // Apple Sign-In button (assuming you have Apple sign-in set up) + ElevatedButton.icon( + onPressed: () async { + // TODO Handle Apple sign-in + final user = await GoogleSignInApi.login(); + if (user != null) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => HomePage( + clientResult: clientResult, user: user))); + } + }, + icon: const Icon(Icons.apple, + color: Colors.white, size: 24), + label: const Text('Iniciar Sesión con Apple'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.black, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), ), ), - ), - const SizedBox(height: 50), + const SizedBox(height: 50), - Image.asset( - 'assets/images/branding.png', - height: 25, - ), + Image.asset( + 'assets/images/branding.png', + height: 25, + ), ], - ), + ), ); }, ), @@ -103,4 +104,4 @@ class SignInPage extends StatelessWidget { ), ); } -} \ No newline at end of file +} From 8f4ab956e8994751233307d4814451c03e6176c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Ortiz?= Date: Tue, 27 Aug 2024 18:32:30 +0200 Subject: [PATCH 13/19] First version of GoogleSignIn Implementation Multi Auth services support, added support in order to request to our API, birthday, and more. --- .../api/sign-in/google_sign_in_api.dart | 102 +++++++++++++- lib/main.dart | 16 ++- lib/pages/home/home_page.dart | 111 +++++++-------- lib/pages/profile/profile_page.dart | 27 ++-- lib/pages/sign-in/sign_in_page.dart | 129 +++++++++--------- 5 files changed, 243 insertions(+), 142 deletions(-) diff --git a/lib/internet/api/sign-in/google_sign_in_api.dart b/lib/internet/api/sign-in/google_sign_in_api.dart index 3003053f..d0bbf11a 100644 --- a/lib/internet/api/sign-in/google_sign_in_api.dart +++ b/lib/internet/api/sign-in/google_sign_in_api.dart @@ -1,14 +1,104 @@ import 'package:google_sign_in/google_sign_in.dart'; -class GoogleSignInApi { - static final _googleSignIn = GoogleSignIn(); +class UserAuthService { + static final _googleSignIn = GoogleSignIn(scopes: scopes); - static Future login() => _googleSignIn.signIn(); + static QuimifyIdentity _identity = QuimifyIdentity( + isPremium: false, + photoUrl: + 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg', + displayName: 'Quimify', + email: 'Quimify@quimify.com', + birthday: 'a'); - static Future loginSilently() => _googleSignIn.signInSilently(); + static QuimifyIdentity get identity => _identity; - static Future isLoggedIn() => _googleSignIn.isSignedIn(); + static List scopes = [ + 'email', + 'https://www.googleapis.com/auth/user.birthday.read', + ]; - static Future logout() => _googleSignIn.disconnect(); + static Future signOut() async { + if (_googleSignIn.currentUser != null) await _googleSignIn.signOut(); + } + static Future getGoogleUser() async { + if (_googleSignIn.currentUser == null) { + await UserAuthService.signInGoogleUser(); + } + return _googleSignIn.currentUser; + } + + static Future signInGoogleUser() async { + final user = await _googleSignIn.signIn(); + if (user == null) return null; + return await postLogin(AuthProviders.google, user, null); + } + + static GoogleSignInAccount? getAppleUser() { + // TODO: Implement Apple Sign-In + return _googleSignIn.currentUser; + } + + // TODO: Hacer la petición POST /login(id) + static Future postLogin(AuthProviders service, + GoogleIdentity? googleUser, AppleIdentity? appleUser) async { + switch (service) { + case AuthProviders.google: + if (googleUser != null) { + // At the moment, returning default QuimifyIdentity + _identity = QuimifyIdentity( + isPremium: false, + googleUser: googleUser, + photoUrl: googleUser.photoUrl ?? + 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg', + displayName: googleUser.displayName ?? 'Quimify', + email: googleUser.email, + //TODO: Get Birthday from Google + birthday: googleUser.displayName); + } + print('Enviado /login'); + return _identity; + case AuthProviders.apple: + // TODO: Implement Apple Sign-In + return _identity; + case AuthProviders.none: + return _identity; + } + } + + static Future handleSilentAuthentication( + AuthProviders service) async { + // At the moment, only works with GooglwSignIn + if (service != AuthProviders.google) return null; + + final googleUser = await _googleSignIn.signInSilently(); + if (googleUser != null) { + return await postLogin(AuthProviders.google, googleUser, null); + } + return null; + } } + +// Later implementation, just for logic now +class AppleIdentity {} + +class QuimifyIdentity { + final GoogleIdentity? googleUser; + final bool isPremium; + final String photoUrl; + final String displayName; + final String? email; + final String? birthday; + + QuimifyIdentity({ + this.googleUser, + required this.isPremium, + required this.photoUrl, + required this.displayName, + this.email, + this.birthday, + }); +} + +enum AuthProviders { google, apple, none } diff --git a/lib/main.dart b/lib/main.dart index f3db0050..06c8dc1a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/internet/ads/ads.dart'; import 'package:quimify_client/internet/api/api.dart'; import 'package:quimify_client/internet/api/results/client_result.dart'; @@ -14,9 +13,9 @@ import 'package:quimify_client/pages/home/home_page.dart'; import 'package:quimify_client/pages/inorganic/nomenclature/nomenclature_page.dart'; import 'package:quimify_client/pages/organic/finding_formula/finding_formula_page.dart'; import 'package:quimify_client/pages/organic/naming/naming_page.dart'; +import 'package:quimify_client/pages/sign-in/sign_in_page.dart'; import 'package:quimify_client/routes.dart'; import 'package:quimify_client/storage/storage.dart'; -import 'package:quimify_client/pages/sign-in/sign_in_page.dart'; import 'internet/api/sign-in/google_sign_in_api.dart'; @@ -33,7 +32,10 @@ main() async { SecurityContext.defaultContext.setTrustedCertificatesBytes(bytes); } catch (_) {} // It's already present in modern devices anyways - GoogleSignInAccount? user = await GoogleSignInApi.loginSilently(); + QuimifyIdentity? user = + await UserAuthService.handleSilentAuthentication(AuthProviders.google); + + print(user); ClientResult? clientResult = await Api().getClient(); Ads().initialize(clientResult); @@ -66,7 +68,7 @@ class QuimifyApp extends StatelessWidget { }) : super(key: key); final ClientResult? clientResult; - final GoogleSignInAccount? user; + final QuimifyIdentity? user; @override Widget build(BuildContext context) { @@ -75,7 +77,11 @@ class QuimifyApp extends StatelessWidget { value: const SystemUiOverlayStyle(statusBarColor: Colors.transparent), child: MaterialApp( title: 'Quimify', - home: user != null ? HomePage(clientResult: clientResult, user: user) : SignInPage(clientResult: clientResult,), + home: user != null + ? HomePage(clientResult: clientResult, user: user) + : SignInPage( + clientResult: clientResult, + ), routes: { Routes.inorganicNomenclature: (context) => const NomenclaturePage(), Routes.organicNaming: (context) => const NamingPage(), diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index bc13a58e..5df9be72 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -2,8 +2,8 @@ import 'dart:io'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/internet/api/results/client_result.dart'; +import 'package:quimify_client/internet/api/sign-in/google_sign_in_api.dart'; import 'package:quimify_client/pages/calculator/calculator_page.dart'; import 'package:quimify_client/pages/home/widgets/quimify_menu_button.dart'; import 'package:quimify_client/pages/inorganic/inorganic_page.dart'; @@ -23,7 +23,7 @@ class HomePage extends StatefulWidget { }) : super(key: key); final ClientResult? clientResult; - final GoogleSignInAccount? user; + final QuimifyIdentity? user; @override State createState() => _HomePageState(); @@ -136,63 +136,66 @@ class _HomePageState extends State { }, child: QuimifyScaffold.noAd( header: SafeArea( - bottom: false, // So it's not inside status bar - child: Container( - padding: const EdgeInsets.only( - top: 15, // TODO 17.5? - bottom: 20, - left: 20, - ), - child: Row( - children: [ - SizedBox( - height: 50, - width: 50, - child: IconButton( - icon: Image.asset( - 'assets/images/icons/logo.png', - color: QuimifyColors.inverseText(context), + bottom: false, // So it's not inside status bar + child: Container( + padding: const EdgeInsets.only( + top: 15, // TODO 17.5? + bottom: 20, + left: 20, + ), + child: Row( + children: [ + SizedBox( + height: 50, + width: 50, + child: IconButton( + icon: Image.asset( + 'assets/images/icons/logo.png', + color: QuimifyColors.inverseText(context), + ), + // To remove native effects: + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + // So it fills container (48 x 48): + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () {}, ), - // To remove native effects: - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - // So it fills container (48 x 48): - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () {}, ), - ), - const SizedBox(width: 15), - Image.asset( - 'assets/images/icons/branding-slim.png', - height: 17, - color: QuimifyColors.inverseText(context), - ), - //const SizedBox(width: 120), - const Spacer(), - Padding( - padding: const EdgeInsets.only(right: 15.0), - child: IconButton( - padding: EdgeInsets.zero, - iconSize: 38, - alignment: Alignment.centerRight, - icon: Icon( - Icons.account_circle_sharp, - color: QuimifyColors.inverseText(context), - ), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => ProfilePage(user: widget.user!,)), - ); - }, + const SizedBox(width: 15), + Image.asset( + 'assets/images/icons/branding-slim.png', + height: 17, + color: QuimifyColors.inverseText(context), ), - ) - ], + //const SizedBox(width: 120), + const Spacer(), + Padding( + padding: const EdgeInsets.only(right: 15.0), + child: IconButton( + padding: EdgeInsets.zero, + iconSize: 38, + alignment: Alignment.centerRight, + icon: Icon( + Icons.account_circle_sharp, + color: QuimifyColors.inverseText(context), + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProfilePage( + user: widget.user!, + )), + ); + }, + ), + ) + ], + ), ), ), - ), body: QuimifySwipeDetector( leftSwipe: () => _goToPage((_currentPage - 1) % 3), rightSwipe: () => _goToPage((_currentPage + 1) % 3), diff --git a/lib/pages/profile/profile_page.dart b/lib/pages/profile/profile_page.dart index 817ae435..a637ac89 100644 --- a/lib/pages/profile/profile_page.dart +++ b/lib/pages/profile/profile_page.dart @@ -1,20 +1,18 @@ import 'package:flutter/material.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/internet/api/sign-in/google_sign_in_api.dart'; +import 'package:quimify_client/pages/widgets/quimify_scaffold.dart'; + import '../../internet/api/results/client_result.dart'; import '../sign-in/sign_in_page.dart'; import '../widgets/bars/quimify_page_bar.dart'; import '../widgets/dialogs/loading_indicator.dart'; -import 'package:quimify_client/pages/widgets/quimify_scaffold.dart'; - import '../widgets/quimify_colors.dart'; - class ProfilePage extends StatelessWidget { final ClientResult? clientResult; - final GoogleSignInAccount user; + final QuimifyIdentity user; - ProfilePage({ + const ProfilePage({ Key? key, this.clientResult, required this.user, @@ -42,20 +40,21 @@ class ProfilePage extends StatelessWidget { child: Column( //crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox(height: 20), CircleAvatar( radius: 50, // TODO: Make it locally - backgroundImage: NetworkImage(user.photoUrl ?? 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg'), + backgroundImage: NetworkImage(user.photoUrl ?? + 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg'), ), const SizedBox(height: 20), Text( - 'Nombre: ${user.displayName!}', - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + 'Nombre: ${user.displayName}', + style: + const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 20), @@ -71,21 +70,21 @@ class ProfilePage extends StatelessWidget { onPressed: () { // Implement the "Gana dinero con Quimify" functionality here }, - child: Text('Gana dinero con Quimify'), + child: const Text('Gana dinero con Quimify'), ), const SizedBox(height: 20), ElevatedButton( onPressed: () async { - await GoogleSignInApi.logout(); + await UserAuthService.signOut(); // Navigate back to the sign-in screen after signing out Navigator.pushReplacement( context, - MaterialPageRoute(builder: (context) => SignInPage()), + MaterialPageRoute(builder: (context) => const SignInPage()), ); }, - child: Text('Cerrar Sesión'), + child: const Text('Cerrar Sesión'), ), //const SizedBox(height: 15), //const SizedBox(height: 5), // + 15 from cards = 20 diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart index 93b26002..7ebddfb6 100644 --- a/lib/pages/sign-in/sign_in_page.dart +++ b/lib/pages/sign-in/sign_in_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:quimify_client/pages/home/home_page.dart'; + import '../../internet/api/results/client_result.dart'; import '../../internet/api/sign-in/google_sign_in_api.dart'; import '../widgets/quimify_colors.dart'; @@ -7,6 +8,8 @@ import '../widgets/quimify_colors.dart'; class SignInPage extends StatelessWidget { final ClientResult? clientResult; + static UserAuthService userAuthService = UserAuthService(); + const SignInPage({ Key? key, this.clientResult, @@ -21,81 +24,81 @@ class SignInPage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 40.0), child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - return Center( // Added Center widget for vertical centering - child: Column( + return Center( + // Added Center widget for vertical centering + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxHeight: constraints.maxHeight * 0.3, - maxWidth: constraints.maxWidth * 0.8, - ), - child: Image.asset( - 'assets/images/logo.png', - height: 175, // Set a fixed height for the image - ), - ), - const SizedBox(height: 50), + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: constraints.maxHeight * 0.3, + maxWidth: constraints.maxWidth * 0.8, + ), + child: Image.asset( + 'assets/images/logo.png', + height: 175, // Set a fixed height for the image + ), + ), + const SizedBox(height: 50), - // Google Sign-In button - ElevatedButton.icon( - onPressed: () async { - final user = await GoogleSignInApi.login(); - if (user != null) { + // Google Sign-In button + ElevatedButton.icon( + onPressed: () async { + final user = await UserAuthService.signInGoogleUser(); + print(user?.displayName); + if (user == null) return; Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => - HomePage( - clientResult: clientResult, user: user))); - } - }, - icon: Image.asset( - 'assets/images/icons/google-logo.png', height: 24), - label: const Text('Iniciar Sesión con Google'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.black, - backgroundColor: Colors.white, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + builder: (context) => HomePage( + clientResult: clientResult, user: user))); + }, + icon: Image.asset('assets/images/icons/google-logo.png', + height: 24), + label: const Text('Iniciar Sesión con Google'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.black, + backgroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), ), ), - ), - const SizedBox(height: 20), + const SizedBox(height: 20), - // Apple Sign-In button (assuming you have Apple sign-in set up) - ElevatedButton.icon( - onPressed: () async { - // TODO Handle Apple sign-in - final user = await GoogleSignInApi.login(); - if (user != null) { - Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => - HomePage( - clientResult: clientResult, user: user))); - } - }, - icon: const Icon( - Icons.apple, color: Colors.white, size: 24), - label: const Text('Iniciar Sesión con Apple'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: Colors.black, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + // Apple Sign-In button (assuming you have Apple sign-in set up) + ElevatedButton.icon( + onPressed: () async { + // TODO Handle Apple sign-in + final user = await UserAuthService.signInGoogleUser(); + if (user != null) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => HomePage( + clientResult: clientResult, user: user))); + } + }, + icon: const Icon(Icons.apple, + color: Colors.white, size: 24), + label: const Text('Iniciar Sesión con Apple'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.black, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), ), ), - ), - const SizedBox(height: 50), + const SizedBox(height: 50), - Image.asset( - 'assets/images/branding.png', - height: 25, - ), + Image.asset( + 'assets/images/branding.png', + height: 25, + ), ], - ), + ), ); }, ), @@ -103,4 +106,4 @@ class SignInPage extends StatelessWidget { ), ); } -} \ No newline at end of file +} From 3086200448bc28ef4ce6e9272744812417827112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Ortiz?= Date: Fri, 30 Aug 2024 17:54:32 +0200 Subject: [PATCH 14/19] Anonymous + Rework Added an anonymous way to sign in. Reworked some code in order to make it better --- lib/internet/api/sign-in/info_google.dart | 84 +++++++++++++++++++ ...ogle_sign_in_api.dart => sign_in_api.dart} | 34 ++++---- lib/main.dart | 2 +- lib/pages/home/home_page.dart | 2 +- lib/pages/profile/profile_page.dart | 2 +- lib/pages/sign-in/sign_in_page.dart | 44 ++++++---- 6 files changed, 132 insertions(+), 36 deletions(-) create mode 100644 lib/internet/api/sign-in/info_google.dart rename lib/internet/api/sign-in/{google_sign_in_api.dart => sign_in_api.dart} (77%) diff --git a/lib/internet/api/sign-in/info_google.dart b/lib/internet/api/sign-in/info_google.dart new file mode 100644 index 00000000..27740820 --- /dev/null +++ b/lib/internet/api/sign-in/info_google.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; + +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:http/http.dart' as http; + +// Returns Gender and Birthday from Google +Future> getInfoGoogle( + GoogleSignInAccount googleUser) async { + // Retrieve the authentication headers + final Map headers = await googleUser.authHeaders; + + // Safely handle the 'Authorization' header + final String authorizationHeader = headers['Authorization'] ?? ''; + + // Make the initial HTTP GET request to the Google People API + final response = await http.get( + Uri.parse( + 'https://people.googleapis.com/v1/people/me?personFields=genders,birthdays'), // Use your API key as needed + headers: {'Authorization': authorizationHeader}, + ); + + // Handle the response + if (response.statusCode == 200) { + // Decode the JSON response + final data = jsonDecode(response.body); + + // Extract gender information + String gender = 'Gender not found'; + if (data['genders'] != null && data['genders'].isNotEmpty) { + gender = data['genders'][0]['formattedValue']; + } + + // Extract birthday information + String birthday = 'Birthday not found'; + bool yearMissing = false; + + if (data['birthdays'] != null && data['birthdays'].isNotEmpty) { + final birthdayDate = data['birthdays'][0]['date']; + if (birthdayDate != null) { + int? year = birthdayDate['year']; + int? month = birthdayDate['month']; + int? day = birthdayDate['day']; + + if (year != null && month != null && day != null) { + DateTime birthdayDateTime = DateTime(year, month, day); + int timestamp = birthdayDateTime.millisecondsSinceEpoch; + birthday = timestamp.toString(); + } else { + yearMissing = year == null; + birthday = + '${year ?? ''}-${month != null ? month.toString().padLeft(2, '0') : ''}-${day != null ? day.toString().padLeft(2, '0') : ''}'; + } + } + } + + // If the year is missing, make another API call with age range + if (yearMissing) { + final rangedResponse = await http.get( + Uri.parse( + 'https://people.googleapis.com/v1/people/me?personFields=ageRanges'), + headers: {'Authorization': authorizationHeader}, + ); + + if (rangedResponse.statusCode == 200) { + final rangedData = jsonDecode(rangedResponse.body); + if (rangedData['ageRanges'] != null && + rangedData['ageRanges'].isNotEmpty) { + // Extract age range + final ageRange = rangedData['ageRanges'][0]['ageRange']; + if (ageRange != null) { + birthday = ageRange; + } + } + } + } + + // Return both gender and birthday + return {'gender': gender, 'birthday': birthday}; + } else { + // Handle error cases + throw Exception( + 'Failed to fetch gender and birthday info: ${response.statusCode}'); + } +} diff --git a/lib/internet/api/sign-in/google_sign_in_api.dart b/lib/internet/api/sign-in/sign_in_api.dart similarity index 77% rename from lib/internet/api/sign-in/google_sign_in_api.dart rename to lib/internet/api/sign-in/sign_in_api.dart index d0bbf11a..cdcbc5eb 100644 --- a/lib/internet/api/sign-in/google_sign_in_api.dart +++ b/lib/internet/api/sign-in/sign_in_api.dart @@ -1,51 +1,49 @@ import 'package:google_sign_in/google_sign_in.dart'; +import 'package:quimify_client/internet/api/sign-in/info_google.dart'; class UserAuthService { static final _googleSignIn = GoogleSignIn(scopes: scopes); static QuimifyIdentity _identity = QuimifyIdentity( isPremium: false, + // TODO Add Default Photo to assets photoUrl: 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg', displayName: 'Quimify', email: 'Quimify@quimify.com', - birthday: 'a'); + birthday: 'EIGHTEEN_TO_TWENTY', + gender: 'Male'); static QuimifyIdentity get identity => _identity; static List scopes = [ 'email', 'https://www.googleapis.com/auth/user.birthday.read', + 'https://www.googleapis.com/auth/user.gender.read' ]; static Future signOut() async { if (_googleSignIn.currentUser != null) await _googleSignIn.signOut(); } - static Future getGoogleUser() async { - if (_googleSignIn.currentUser == null) { - await UserAuthService.signInGoogleUser(); - } - return _googleSignIn.currentUser; - } - + // Donde va la logica de cada tipo de autenticación static Future signInGoogleUser() async { final user = await _googleSignIn.signIn(); if (user == null) return null; return await postLogin(AuthProviders.google, user, null); } - static GoogleSignInAccount? getAppleUser() { - // TODO: Implement Apple Sign-In - return _googleSignIn.currentUser; + static Future signInAnonymousUser() async { + return await postLogin(AuthProviders.none, null, null); } // TODO: Hacer la petición POST /login(id) static Future postLogin(AuthProviders service, - GoogleIdentity? googleUser, AppleIdentity? appleUser) async { + GoogleSignInAccount? googleUser, AppleIdentity? appleUser) async { switch (service) { case AuthProviders.google: if (googleUser != null) { + var data = await getInfoGoogle(googleUser); // At the moment, returning default QuimifyIdentity _identity = QuimifyIdentity( isPremium: false, @@ -54,8 +52,8 @@ class UserAuthService { 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg', displayName: googleUser.displayName ?? 'Quimify', email: googleUser.email, - //TODO: Get Birthday from Google - birthday: googleUser.displayName); + gender: data['gender'], + birthday: data['birthday']); } print('Enviado /login'); return _identity; @@ -69,7 +67,7 @@ class UserAuthService { static Future handleSilentAuthentication( AuthProviders service) async { - // At the moment, only works with GooglwSignIn + //TODO: At the moment, only works with GooglwSignIn if (service != AuthProviders.google) return null; final googleUser = await _googleSignIn.signInSilently(); @@ -88,7 +86,8 @@ class QuimifyIdentity { final bool isPremium; final String photoUrl; final String displayName; - final String? email; + final String? gender; + final String email; final String? birthday; QuimifyIdentity({ @@ -96,7 +95,8 @@ class QuimifyIdentity { required this.isPremium, required this.photoUrl, required this.displayName, - this.email, + required this.email, + this.gender, this.birthday, }); } diff --git a/lib/main.dart b/lib/main.dart index 06c8dc1a..e4959daa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,7 +17,7 @@ import 'package:quimify_client/pages/sign-in/sign_in_page.dart'; import 'package:quimify_client/routes.dart'; import 'package:quimify_client/storage/storage.dart'; -import 'internet/api/sign-in/google_sign_in_api.dart'; +import 'internet/api/sign-in/sign_in_api.dart'; main() async { _showLoadingScreen(); diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 5df9be72..17454d76 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:quimify_client/internet/api/results/client_result.dart'; -import 'package:quimify_client/internet/api/sign-in/google_sign_in_api.dart'; +import 'package:quimify_client/internet/api/sign-in/sign_in_api.dart'; import 'package:quimify_client/pages/calculator/calculator_page.dart'; import 'package:quimify_client/pages/home/widgets/quimify_menu_button.dart'; import 'package:quimify_client/pages/inorganic/inorganic_page.dart'; diff --git a/lib/pages/profile/profile_page.dart b/lib/pages/profile/profile_page.dart index a637ac89..86c8713a 100644 --- a/lib/pages/profile/profile_page.dart +++ b/lib/pages/profile/profile_page.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:quimify_client/internet/api/sign-in/google_sign_in_api.dart'; +import 'package:quimify_client/internet/api/sign-in/sign_in_api.dart'; import 'package:quimify_client/pages/widgets/quimify_scaffold.dart'; import '../../internet/api/results/client_result.dart'; diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart index d0dee78b..ca894d88 100644 --- a/lib/pages/sign-in/sign_in_page.dart +++ b/lib/pages/sign-in/sign_in_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:quimify_client/pages/home/home_page.dart'; import '../../internet/api/results/client_result.dart'; -import '../../internet/api/sign-in/google_sign_in_api.dart'; +import '../../internet/api/sign-in/sign_in_api.dart'; import '../widgets/quimify_colors.dart'; class SignInPage extends StatelessWidget { @@ -44,22 +44,11 @@ class SignInPage extends StatelessWidget { // Google Sign-In button ElevatedButton.icon( onPressed: () async { -<<<<<<< HEAD final user = await UserAuthService.signInGoogleUser(); - print(user?.displayName); if (user == null) return; Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => HomePage( clientResult: clientResult, user: user))); -======= - final user = await GoogleSignInApi.login(); - if (user != null) { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => HomePage( - clientResult: clientResult, user: user))); - } ->>>>>>> 445f779f3a2148e61ed9ab6c1725dd63f230c556 }, icon: Image.asset('assets/images/icons/google-logo.png', height: 24), @@ -80,11 +69,7 @@ class SignInPage extends StatelessWidget { ElevatedButton.icon( onPressed: () async { // TODO Handle Apple sign-in -<<<<<<< HEAD final user = await UserAuthService.signInGoogleUser(); -======= - final user = await GoogleSignInApi.login(); ->>>>>>> 445f779f3a2148e61ed9ab6c1725dd63f230c556 if (user != null) { Navigator.of(context).pushReplacement( MaterialPageRoute( @@ -105,6 +90,33 @@ class SignInPage extends StatelessWidget { ), ), + const SizedBox(height: 20), + + // Anonymous Sign-In button + ElevatedButton.icon( + onPressed: () async { + final user = + await UserAuthService.signInAnonymousUser(); + if (user != null) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => HomePage( + clientResult: clientResult, user: user))); + } + }, + icon: const Icon(Icons.person_off_outlined, + color: Colors.grey, size: 24), + label: const Text('Saltar'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.black, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + ), + const SizedBox(height: 50), Image.asset( From 1a8b7edcd6a682377c2160508dc3c6006825ab61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Ortiz?= Date: Sun, 22 Sep 2024 13:05:43 +0200 Subject: [PATCH 15/19] Bug Fixes Profile Page & Sign In System --- lib/internet/api/sign-in/sign_in_api.dart | 62 ++-- lib/main.dart | 14 +- lib/pages/home/home_page.dart | 2 +- .../radical_factory_dialog.dart | 2 - lib/pages/profile/profile_page.dart | 102 ++++-- lib/pages/sign-in/sign_in_page.dart | 9 +- lib/storage/storage.dart | 4 + pubspec.lock | 340 ++++++++++-------- 8 files changed, 314 insertions(+), 221 deletions(-) diff --git a/lib/internet/api/sign-in/sign_in_api.dart b/lib/internet/api/sign-in/sign_in_api.dart index cdcbc5eb..2bd3bd39 100644 --- a/lib/internet/api/sign-in/sign_in_api.dart +++ b/lib/internet/api/sign-in/sign_in_api.dart @@ -1,22 +1,11 @@ import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/internet/api/sign-in/info_google.dart'; +import 'package:quimify_client/storage/storage.dart'; class UserAuthService { static final _googleSignIn = GoogleSignIn(scopes: scopes); - static QuimifyIdentity _identity = QuimifyIdentity( - isPremium: false, - // TODO Add Default Photo to assets - photoUrl: - 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg', - displayName: 'Quimify', - email: 'Quimify@quimify.com', - birthday: 'EIGHTEEN_TO_TWENTY', - gender: 'Male'); - - static QuimifyIdentity get identity => _identity; - - static List scopes = [ + static List scopes = [ 'email', 'https://www.googleapis.com/auth/user.birthday.read', 'https://www.googleapis.com/auth/user.gender.read' @@ -24,67 +13,70 @@ class UserAuthService { static Future signOut() async { if (_googleSignIn.currentUser != null) await _googleSignIn.signOut(); + final prefs = Storage(); + await prefs.saveBool('isAnonymouslySignedIn', false); } - // Donde va la logica de cada tipo de autenticación static Future signInGoogleUser() async { final user = await _googleSignIn.signIn(); if (user == null) return null; - return await postLogin(AuthProviders.google, user, null); + return await postLogin(AuthProviders.google, user); } static Future signInAnonymousUser() async { - return await postLogin(AuthProviders.none, null, null); + QuimifyIdentity? identity = await postLogin(AuthProviders.none, null); + final prefs = Storage(); + await prefs.saveBool('isAnonymouslySignedIn', true); + + return identity; } - // TODO: Hacer la petición POST /login(id) - static Future postLogin(AuthProviders service, - GoogleSignInAccount? googleUser, AppleIdentity? appleUser) async { + Future getisAnonymouslySignedIn() async { + final prefs = Storage(); + return prefs.getBool('isAnonymouslySignedIn') ?? false; + } + + // TODO: Implement error handling for login requests + static Future postLogin( + AuthProviders service, GoogleSignInAccount? googleUser) async { switch (service) { case AuthProviders.google: if (googleUser != null) { var data = await getInfoGoogle(googleUser); - // At the moment, returning default QuimifyIdentity - _identity = QuimifyIdentity( + QuimifyIdentity identity = QuimifyIdentity( isPremium: false, googleUser: googleUser, - photoUrl: googleUser.photoUrl ?? - 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg', + photoUrl: googleUser.photoUrl, displayName: googleUser.displayName ?? 'Quimify', email: googleUser.email, gender: data['gender'], birthday: data['birthday']); + return identity; } print('Enviado /login'); - return _identity; - case AuthProviders.apple: - // TODO: Implement Apple Sign-In - return _identity; + break; case AuthProviders.none: - return _identity; + return null; } + return null; } static Future handleSilentAuthentication( AuthProviders service) async { - //TODO: At the moment, only works with GooglwSignIn if (service != AuthProviders.google) return null; final googleUser = await _googleSignIn.signInSilently(); if (googleUser != null) { - return await postLogin(AuthProviders.google, googleUser, null); + return await postLogin(AuthProviders.google, googleUser); } return null; } } -// Later implementation, just for logic now -class AppleIdentity {} - class QuimifyIdentity { final GoogleIdentity? googleUser; final bool isPremium; - final String photoUrl; + final String? photoUrl; final String displayName; final String? gender; final String email; @@ -101,4 +93,4 @@ class QuimifyIdentity { }); } -enum AuthProviders { google, apple, none } +enum AuthProviders { google, none } diff --git a/lib/main.dart b/lib/main.dart index e4959daa..6dbecc4d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -32,10 +32,16 @@ main() async { SecurityContext.defaultContext.setTrustedCertificatesBytes(bytes); } catch (_) {} // It's already present in modern devices anyways + bool isAnonymouslySignedIn = + await UserAuthService().getisAnonymouslySignedIn(); + QuimifyIdentity? user = await UserAuthService.handleSilentAuthentication(AuthProviders.google); + print(isAnonymouslySignedIn); print(user); + bool hasToLogin = isAnonymouslySignedIn == false && user == null; + print(hasToLogin); ClientResult? clientResult = await Api().getClient(); Ads().initialize(clientResult); @@ -46,6 +52,7 @@ main() async { builder: (context) => QuimifyApp( clientResult: clientResult, user: user, + hasToLogin: hasToLogin, ), // Wrap your app ), ); @@ -65,10 +72,12 @@ class QuimifyApp extends StatelessWidget { Key? key, this.clientResult, required this.user, + required this.hasToLogin, }) : super(key: key); final ClientResult? clientResult; final QuimifyIdentity? user; + final bool hasToLogin; @override Widget build(BuildContext context) { @@ -77,7 +86,7 @@ class QuimifyApp extends StatelessWidget { value: const SystemUiOverlayStyle(statusBarColor: Colors.transparent), child: MaterialApp( title: 'Quimify', - home: user != null + home: hasToLogin != true ? HomePage(clientResult: clientResult, user: user) : SignInPage( clientResult: clientResult, @@ -86,7 +95,8 @@ class QuimifyApp extends StatelessWidget { Routes.inorganicNomenclature: (context) => const NomenclaturePage(), Routes.organicNaming: (context) => const NamingPage(), Routes.organicFindingFormula: (context) => const FindingFormulaPage(), - Routes.calculatorMolecularMass: (context) => const MolecularMassPage(), + Routes.calculatorMolecularMass: (context) => + const MolecularMassPage(), }, // To get rid of debug banner: debugShowCheckedModeBanner: false, diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 17454d76..0c6e0faa 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -186,7 +186,7 @@ class _HomePageState extends State { context, MaterialPageRoute( builder: (context) => ProfilePage( - user: widget.user!, + user: widget.user, )), ); }, diff --git a/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart b/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart index 8dc89f9d..59ff7550 100644 --- a/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart +++ b/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:quimify_client/pages/organic/naming/widgets/radical_factory/carbons_help_dialog.dart'; import 'package:quimify_client/pages/organic/naming/widgets/radical_factory/tip_shape_help_dialog.dart'; diff --git a/lib/pages/profile/profile_page.dart b/lib/pages/profile/profile_page.dart index 86c8713a..1f00cc16 100644 --- a/lib/pages/profile/profile_page.dart +++ b/lib/pages/profile/profile_page.dart @@ -10,16 +10,92 @@ import '../widgets/quimify_colors.dart'; class ProfilePage extends StatelessWidget { final ClientResult? clientResult; - final QuimifyIdentity user; + final QuimifyIdentity? user; const ProfilePage({ Key? key, this.clientResult, - required this.user, + this.user, }) : super(key: key); @override Widget build(BuildContext context) { + var defaultLogo = const AssetImage('assets/images/logo.png'); + if (user is QuimifyIdentity && user != null) { + return PopScope( + onPopInvoked: (bool didPop) async { + if (!didPop) { + return; + } + + hideLoadingIndicator(); + }, + child: QuimifyScaffold.noAd( + header: const QuimifyPageBar(title: 'Perfil'), + body: Container( + width: 900, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: QuimifyColors.foreground(context), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + //crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 20), + + CircleAvatar( + radius: 50, + backgroundImage: user?.photoUrl != null + ? NetworkImage(user?.photoUrl ?? '') + : defaultLogo, + ), + const SizedBox(height: 20), + + Text( + 'Nombre: ${user?.displayName ?? 'No hay nombre disponible'}', // Use null-ish coalescing operator (??) + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + + const SizedBox(height: 20), + + Text( + 'Email: ${user?.email}', // Can never be null + style: const TextStyle(fontSize: 16), + ), + + const SizedBox(height: 20), + + ElevatedButton( + onPressed: () { + // Implement the "Gana dinero con Quimify" functionality here + }, + child: const Text('Gana dinero con Quimify'), + ), + + const SizedBox(height: 20), + + ElevatedButton( + onPressed: () async { + await UserAuthService.signOut(); + // Navigate back to the sign-in screen after signing out + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const SignInPage()), + ); + }, + child: const Text('Cerrar Sesión'), + ), + //const SizedBox(height: 15), + //const SizedBox(height: 5), // + 15 from cards = 20 + ], + ), + ), + ), + ); + } return PopScope( onPopInvoked: (bool didPop) async { if (!didPop) { @@ -44,26 +120,10 @@ class ProfilePage extends StatelessWidget { CircleAvatar( radius: 50, - // TODO: Make it locally - backgroundImage: NetworkImage(user.photoUrl ?? - 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg'), - ), - - const SizedBox(height: 20), - - Text( - 'Nombre: ${user.displayName}', - style: - const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + backgroundImage: user?.photoUrl != null + ? NetworkImage(user?.photoUrl ?? '') + : defaultLogo, ), - - const SizedBox(height: 20), - - Text( - 'Email: ${user.email}', // Can never be null - style: const TextStyle(fontSize: 16), - ), - const SizedBox(height: 20), ElevatedButton( diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart index ca894d88..0bf417f8 100644 --- a/lib/pages/sign-in/sign_in_page.dart +++ b/lib/pages/sign-in/sign_in_page.dart @@ -97,12 +97,9 @@ class SignInPage extends StatelessWidget { onPressed: () async { final user = await UserAuthService.signInAnonymousUser(); - if (user != null) { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => HomePage( - clientResult: clientResult, user: user))); - } + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => HomePage( + clientResult: clientResult, user: user))); }, icon: const Icon(Icons.person_off_outlined, color: Colors.grey, size: 24), diff --git a/lib/storage/storage.dart b/lib/storage/storage.dart index 5c3ca3ff..f50de25a 100644 --- a/lib/storage/storage.dart +++ b/lib/storage/storage.dart @@ -17,4 +17,8 @@ class Storage { String? get(String key) => sharedPreferences.getString(key); save(String key, String value) => sharedPreferences.setString(key, value); + + saveBool(String key, bool value) => sharedPreferences.setBool(key, value); + + getBool(String key) => sharedPreferences.getBool(key); } diff --git a/pubspec.lock b/pubspec.lock index f1f911c4..97186187 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: archive - sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.3.9" + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -77,34 +77,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972 + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.12" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "7.3.2" built_collection: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: built_value - sha256: ff627b645b28fb8bdb69e645f910c2458fd6b65f6585c3a53e0626024897dedf + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.6.2" + version: "8.9.2" characters: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "315a598c7fbe77f22de1c9da7cfd6fd21816312f16ffa124453b4fc679e540f1" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.10.0" collection: dependency: "direct main" description: @@ -173,10 +173,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: b502a681ba415272ecc41400bd04fe543ed1a62632137dc84d25a91e7746f55f + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.2" connectivity_plus_platform_interface: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" csslib: dependency: transitive description: @@ -229,18 +229,18 @@ packages: dependency: "direct dev" description: name: dependency_validator - sha256: "08349175533ed0bd06eb9b6043cde66c45b2bfc7ebc222a7542cdb1324f1bf03" + sha256: f727a5627aa405965fab4aef4f468e50a9b632ba0737fd2f98c932fec6d712b9 url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.3" device_frame: dependency: transitive description: name: device_frame - sha256: afe76182aec178d171953d9b4a50a43c57c7cf3c77d8b09a48bf30c8fa04dd9d + sha256: d031a06f5d6f4750009672db98a5aa1536aa4a231713852469ce394779a23d75 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" device_info_plus: dependency: "direct main" description: @@ -253,26 +253,26 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" device_preview: dependency: "direct main" description: name: device_preview - sha256: "2f097bf31b929e15e6756dbe0ec1bcb63952ab9ed51c25dc5a2c722d2b21fdaf" + sha256: a694acdd3894b4c7d600f4ee413afc4ff917f76026b97ab06575fe886429ef19 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" diacritic: dependency: "direct main" description: name: diacritic - sha256: a84e03ec2779375fb86430dbe9d8fba62c68376f2499097a5f6e75556babe706 + sha256: "96db5db6149cbe4aa3cfcbfd170aca9b7648639be7e48025f9d458517f807fe4" url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.1.5" envied: dependency: "direct main" description: @@ -317,18 +317,18 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: @@ -431,10 +431,10 @@ packages: dependency: transitive description: name: flutter_spinkit - sha256: b39c753e909d4796906c5696a14daf33639a76e017136c8d82bf3e620ce5bb8e + sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.2.1" flutter_test: dependency: "direct dev" description: flutter @@ -444,10 +444,10 @@ packages: dependency: "direct main" description: name: flutter_typeahead - sha256: e2070dea278f09ae30885872138ccae75292b33b7af2c241fec5ceafd980c374 + sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.2.0" flutter_web_plugins: dependency: transitive description: flutter @@ -457,18 +457,18 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -481,10 +481,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "0c56c2c5d60d6dfaf9725f5ad4699f04749fb196ee5a70487a46ef184837ccf6" + sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" url: "https://pub.dev" source: hosted - version: "0.3.0+2" + version: "0.3.1+4" google_mobile_ads: dependency: "direct main" description: @@ -505,42 +505,42 @@ packages: dependency: transitive description: name: google_sign_in_android - sha256: bfd42c81c30c6faba16e0f62968d5505a87504aaa672b3155ee931461abb0a49 + sha256: "0608de03fc541ece4f91ba3e01a68b17cce7a6cf42bd59e40bbe5c55cc3a49d8" url: "https://pub.dev" source: hosted - version: "6.1.21" + version: "6.1.30" google_sign_in_ios: dependency: transitive description: name: google_sign_in_ios - sha256: a7d653803468d30b82ceb47ea00fe86d23c56e63eb2e5c2248bb68e9df203217 + sha256: "4898410f55440049e1ba8f15411612d9f89299d89c61cd9baf7e02d56ff81ac7" url: "https://pub.dev" source: hosted - version: "5.7.4" + version: "5.7.7" google_sign_in_platform_interface: dependency: transitive description: name: google_sign_in_platform_interface - sha256: df6c91e74b7bb07335c864434c2064007dafa5539a96adeca6e83811be583ec4 + sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" google_sign_in_web: dependency: transitive description: name: google_sign_in_web - sha256: a278ea2d01013faf341cbb093da880d0f2a552bbd1cb6ee90b5bebac9ba69d77 + sha256: "042805a21127a85b0dc46bba98a37926f17d2439720e8a459d27045d8ef68055" url: "https://pub.dev" source: hosted - version: "0.12.3+2" + version: "0.12.4+2" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" html: dependency: transitive description: @@ -585,10 +585,10 @@ packages: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -609,10 +609,34 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: @@ -641,34 +665,34 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.6" nested: dependency: transitive description: @@ -697,10 +721,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider_linux: dependency: transitive description: @@ -713,26 +737,26 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: name: petitparser - sha256: a9346a3fbba7546a28374bdbcd7f54ea48bb47772bf3a7ab4bfaadc40bc8b8c6 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "6.0.2" photo_view: dependency: "direct main" description: @@ -745,10 +769,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -761,42 +785,34 @@ packages: dependency: transitive description: name: pointer_interceptor - sha256: bd18321519718678d5fa98ad3a3359cbc7a31f018554eab80b73d08a7f0c165a + sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" url: "https://pub.dev" source: hosted - version: "0.10.1" + version: "0.10.1+2" pointer_interceptor_ios: dependency: transitive description: name: pointer_interceptor_ios - sha256: "4282ebfe21b54e21e26ab982c6086f0a67dc63423026bfba8db39a2e22045f26" + sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917 url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.10.1" pointer_interceptor_platform_interface: dependency: transitive description: name: pointer_interceptor_platform_interface - sha256: "59a446ead3be360bde72c3725f5ecacbba203c8a760e3061024c20f7da53f825" + sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.10.0+1" pointer_interceptor_web: dependency: transitive description: name: pointer_interceptor_web - sha256: dfd32b9c6e01a18f80535e7791e9d3add009a3395645d6db2f1e22b2642bfab4 - url: "https://pub.dev" - source: hosted - version: "0.10.1" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "0.10.2+1" pool: dependency: transitive description: @@ -809,10 +825,10 @@ packages: dependency: transitive description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -825,66 +841,66 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.5.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shelf: dependency: transitive description: @@ -897,10 +913,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" signed_spacing_flex: dependency: "direct main" description: @@ -918,18 +934,18 @@ packages: dependency: "direct main" description: name: smooth_page_indicator - sha256: "725bc638d5e79df0c84658e1291449996943f93bacbc2cec49963dbbab48d8ae" + sha256: "3b28b0c545fa67ed9e5997d9f9720d486f54c0c607e056a1094544e36934dff3" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0+3" source_gen: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_span: dependency: transitive description: @@ -982,10 +998,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" timing: dependency: transitive description: @@ -1014,66 +1030,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.3.10" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.2.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2 + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.2" vector_math: dependency: transitive description: @@ -1082,6 +1098,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" watcher: dependency: transitive description: @@ -1094,34 +1118,42 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.0.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" webview_flutter: dependency: "direct main" description: name: webview_flutter - sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932" + sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.9.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934 + sha256: "6e64fcb1c19d92024da8f33503aaeeda35825d77142c01d0ea2aa32edc79fdc8" url: "https://pub.dev" source: hosted - version: "3.16.0" + version: "3.16.7" webview_flutter_platform_interface: dependency: transitive description: @@ -1134,42 +1166,42 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7 + sha256: "1942a12224ab31e9508cf00c0c6347b931b023b8a4f0811e5dec3b06f94f117d" url: "https://pub.dev" source: hosted - version: "3.13.0" + version: "3.15.0" win32: dependency: transitive description: name: win32 - sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.5.4" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.5" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.5.0" yaml: dependency: transitive description: @@ -1179,5 +1211,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: "3.2.6" - flutter: ">=3.16.6" + dart: "3.5.3" + flutter: ">=3.24.0" From 5896f09cec6793c86fe13f6c16047ec63ca1c117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Ortiz?= Date: Sun, 22 Sep 2024 13:05:43 +0200 Subject: [PATCH 16/19] Improved Profile Page functionality & Sign In System --- lib/internet/api/sign-in/sign_in_api.dart | 119 +++--- lib/main.dart | 17 +- lib/pages/home/home_page.dart | 29 +- .../radical_factory_dialog.dart | 2 - lib/pages/profile/profile_page.dart | 188 ++++++---- lib/pages/sign-in/sign_in_page.dart | 36 +- lib/storage/storage.dart | 4 + pubspec.lock | 340 ++++++++++-------- 8 files changed, 410 insertions(+), 325 deletions(-) diff --git a/lib/internet/api/sign-in/sign_in_api.dart b/lib/internet/api/sign-in/sign_in_api.dart index cdcbc5eb..23e003a5 100644 --- a/lib/internet/api/sign-in/sign_in_api.dart +++ b/lib/internet/api/sign-in/sign_in_api.dart @@ -1,22 +1,11 @@ import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/internet/api/sign-in/info_google.dart'; +import 'package:quimify_client/storage/storage.dart'; class UserAuthService { static final _googleSignIn = GoogleSignIn(scopes: scopes); - static QuimifyIdentity _identity = QuimifyIdentity( - isPremium: false, - // TODO Add Default Photo to assets - photoUrl: - 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg', - displayName: 'Quimify', - email: 'Quimify@quimify.com', - birthday: 'EIGHTEEN_TO_TWENTY', - gender: 'Male'); - - static QuimifyIdentity get identity => _identity; - - static List scopes = [ + static List scopes = [ 'email', 'https://www.googleapis.com/auth/user.birthday.read', 'https://www.googleapis.com/auth/user.gender.read' @@ -24,81 +13,93 @@ class UserAuthService { static Future signOut() async { if (_googleSignIn.currentUser != null) await _googleSignIn.signOut(); + final prefs = Storage(); + await prefs.saveBool('isAnonymouslySignedIn', false); } - // Donde va la logica de cada tipo de autenticación static Future signInGoogleUser() async { final user = await _googleSignIn.signIn(); if (user == null) return null; - return await postLogin(AuthProviders.google, user, null); + return signInPOST(user); } static Future signInAnonymousUser() async { - return await postLogin(AuthProviders.none, null, null); + bool state = await logInPOST(null); + final prefs = Storage(); + await prefs.saveBool('isAnonymouslySignedIn', state); + QuimifyIdentity identity = QuimifyIdentity(); + return identity; } - // TODO: Hacer la petición POST /login(id) - static Future postLogin(AuthProviders service, - GoogleSignInAccount? googleUser, AppleIdentity? appleUser) async { - switch (service) { - case AuthProviders.google: - if (googleUser != null) { - var data = await getInfoGoogle(googleUser); - // At the moment, returning default QuimifyIdentity - _identity = QuimifyIdentity( - isPremium: false, - googleUser: googleUser, - photoUrl: googleUser.photoUrl ?? - 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg', - displayName: googleUser.displayName ?? 'Quimify', - email: googleUser.email, - gender: data['gender'], - birthday: data['birthday']); - } - print('Enviado /login'); - return _identity; - case AuthProviders.apple: - // TODO: Implement Apple Sign-In - return _identity; - case AuthProviders.none: - return _identity; - } + Future hasSkippedLogin() async { + final prefs = Storage(); + return prefs.getBool('userSkippedLogIn') ?? false; + } + + // TODO: Implement error handling for login requests + static Future signInPOST( + GoogleSignInAccount googleUser) async { + var data = await getInfoGoogle(googleUser); + QuimifyIdentity identity = QuimifyIdentity( + googleUser: googleUser, + photoUrl: googleUser.photoUrl, + displayName: googleUser.displayName ?? 'Quimify', + email: googleUser.email, + gender: data['gender'], + birthday: data['birthday']); + //formato api.quimify.com/login?id=...&email=...&gender=...&birthday=... + print('Enviado /login'); + //formato api.quimify.com/login?id=...&email=...&gender=...&birthday=... + return identity; } - static Future handleSilentAuthentication( - AuthProviders service) async { - //TODO: At the moment, only works with GooglwSignIn - if (service != AuthProviders.google) return null; + // TODO: Logic of hhtp request (make sure to handle errors) + //* Return true if login was successful, false otherwise + static Future logInPOST(GoogleSignInAccount? googleUser) async { + // If user is null, it means that the user is not logged in + if (googleUser == null) return false; + + // _Important_: Do not use this returned Google ID to communicate the + /// currently signed in user to your backend server. Instead, send an ID token + /// which can be securely validated on the server. + var id = googleUser.authentication.then((value) => value.idToken); + //formato api.quimify.com/login?id=...&email=...&gender=...&birthday=... + print('Enviado /login' + id.toString()); + return true; + } + static Future handleSilentAuthentication() async { final googleUser = await _googleSignIn.signInSilently(); if (googleUser != null) { - return await postLogin(AuthProviders.google, googleUser, null); + bool state = await logInPOST(googleUser); + QuimifyIdentity identity = QuimifyIdentity( + googleUser: googleUser, + photoUrl: googleUser.photoUrl, + displayName: googleUser.displayName ?? 'Quimify', + email: googleUser.email, + ); + return state ? identity : null; } return null; } } -// Later implementation, just for logic now -class AppleIdentity {} - class QuimifyIdentity { final GoogleIdentity? googleUser; - final bool isPremium; - final String photoUrl; - final String displayName; + final String? photoUrl; + final String? displayName; final String? gender; - final String email; + final String? email; final String? birthday; QuimifyIdentity({ this.googleUser, - required this.isPremium, - required this.photoUrl, - required this.displayName, - required this.email, + this.photoUrl, + this.displayName, + this.email, this.gender, this.birthday, }); } -enum AuthProviders { google, apple, none } +enum AuthProviders { google, none } diff --git a/lib/main.dart b/lib/main.dart index e4959daa..e0150bb8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/internet/ads/ads.dart'; import 'package:quimify_client/internet/api/api.dart'; import 'package:quimify_client/internet/api/results/client_result.dart'; @@ -32,10 +33,12 @@ main() async { SecurityContext.defaultContext.setTrustedCertificatesBytes(bytes); } catch (_) {} // It's already present in modern devices anyways - QuimifyIdentity? user = - await UserAuthService.handleSilentAuthentication(AuthProviders.google); + bool skippedLogin = await UserAuthService().hasSkippedLogin(); + + QuimifyIdentity? user = await UserAuthService.handleSilentAuthentication(); + + bool hasToLogin = skippedLogin == false && user == null; - print(user); ClientResult? clientResult = await Api().getClient(); Ads().initialize(clientResult); @@ -46,6 +49,7 @@ main() async { builder: (context) => QuimifyApp( clientResult: clientResult, user: user, + hasToLogin: hasToLogin, ), // Wrap your app ), ); @@ -65,10 +69,12 @@ class QuimifyApp extends StatelessWidget { Key? key, this.clientResult, required this.user, + required this.hasToLogin, }) : super(key: key); final ClientResult? clientResult; final QuimifyIdentity? user; + final bool hasToLogin; @override Widget build(BuildContext context) { @@ -77,7 +83,7 @@ class QuimifyApp extends StatelessWidget { value: const SystemUiOverlayStyle(statusBarColor: Colors.transparent), child: MaterialApp( title: 'Quimify', - home: user != null + home: hasToLogin != true ? HomePage(clientResult: clientResult, user: user) : SignInPage( clientResult: clientResult, @@ -86,7 +92,8 @@ class QuimifyApp extends StatelessWidget { Routes.inorganicNomenclature: (context) => const NomenclaturePage(), Routes.organicNaming: (context) => const NamingPage(), Routes.organicFindingFormula: (context) => const FindingFormulaPage(), - Routes.calculatorMolecularMass: (context) => const MolecularMassPage(), + Routes.calculatorMolecularMass: (context) => + const MolecularMassPage(), }, // To get rid of debug banner: debugShowCheckedModeBanner: false, diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 17454d76..244f5f3b 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -177,18 +177,27 @@ class _HomePageState extends State { padding: EdgeInsets.zero, iconSize: 38, alignment: Alignment.centerRight, - icon: Icon( - Icons.account_circle_sharp, - color: QuimifyColors.inverseText(context), - ), + icon: widget.user?.photoUrl != null + ? CircleAvatar( + backgroundImage: + NetworkImage(widget.user?.photoUrl ?? ''), + radius: 19, // Adjust radius as needed + ) + : Icon( + Icons.account_circle, + color: QuimifyColors.inverseText(context), + ), onPressed: () { - Navigator.push( - context, - MaterialPageRoute( + if (widget.user != null) { + Navigator.push( + context, + MaterialPageRoute( builder: (context) => ProfilePage( - user: widget.user!, - )), - ); + user: widget.user, // Use non-null assertion here + ), + ), + ); + } }, ), ) diff --git a/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart b/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart index 8dc89f9d..59ff7550 100644 --- a/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart +++ b/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:quimify_client/pages/organic/naming/widgets/radical_factory/carbons_help_dialog.dart'; import 'package:quimify_client/pages/organic/naming/widgets/radical_factory/tip_shape_help_dialog.dart'; diff --git a/lib/pages/profile/profile_page.dart b/lib/pages/profile/profile_page.dart index 86c8713a..b92396e2 100644 --- a/lib/pages/profile/profile_page.dart +++ b/lib/pages/profile/profile_page.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/internet/api/sign-in/sign_in_api.dart'; import 'package:quimify_client/pages/widgets/quimify_scaffold.dart'; import '../../internet/api/results/client_result.dart'; +import 'package:quimify_client/pages/home/home_page.dart'; import '../sign-in/sign_in_page.dart'; import '../widgets/bars/quimify_page_bar.dart'; import '../widgets/dialogs/loading_indicator.dart'; @@ -10,18 +12,94 @@ import '../widgets/quimify_colors.dart'; class ProfilePage extends StatelessWidget { final ClientResult? clientResult; - final QuimifyIdentity user; + final QuimifyIdentity? user; const ProfilePage({ Key? key, this.clientResult, - required this.user, + this.user, }) : super(key: key); @override Widget build(BuildContext context) { + var defaultLogo = const AssetImage('assets/images/logo.png'); + if (user?.googleUser is GoogleIdentity && user != null) { + return PopScope( + onPopInvokedWithResult: (didPop, dynamic) async { + if (!didPop) { + return; + } + + hideLoadingIndicator(); + }, + child: QuimifyScaffold.noAd( + header: const QuimifyPageBar(title: 'Perfil'), + body: Container( + width: 900, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: QuimifyColors.foreground(context), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + //crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 20), + + CircleAvatar( + radius: 50, + backgroundImage: user?.photoUrl != null + ? NetworkImage(user?.photoUrl ?? '') + : defaultLogo, + ), + const SizedBox(height: 20), + + Text( + 'Nombre: ${user?.displayName ?? 'No hay nombre disponible'}', // Use null-ish coalescing operator (??) + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + + const SizedBox(height: 20), + + Text( + 'Email: ${user?.email}', // Can never be null + style: const TextStyle(fontSize: 16), + ), + + const SizedBox(height: 20), + + ElevatedButton( + onPressed: () { + // Implement the "Gana dinero con Quimify" functionality here + }, + child: const Text('Gana dinero con Quimify'), + ), + + const SizedBox(height: 20), + + ElevatedButton( + onPressed: () async { + await UserAuthService.signOut(); + // Navigate back to the sign-in screen after signing out + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const ProfilePage()), + ); + }, + child: const Text('Cerrar Sesión'), + ), + //const SizedBox(height: 15), + //const SizedBox(height: 5), // + 15 from cards = 20 + ], + ), + ), + ), + ); + } return PopScope( - onPopInvoked: (bool didPop) async { + onPopInvokedWithResult: (bool didPop, dynamic) async { if (!didPop) { return; } @@ -30,65 +108,51 @@ class ProfilePage extends StatelessWidget { }, child: QuimifyScaffold.noAd( header: const QuimifyPageBar(title: 'Perfil'), - body: Container( - width: 900, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: QuimifyColors.foreground(context), - borderRadius: BorderRadius.circular(15), - ), - child: Column( - //crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 20), - - CircleAvatar( - radius: 50, - // TODO: Make it locally - backgroundImage: NetworkImage(user.photoUrl ?? - 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg'), - ), - - const SizedBox(height: 20), - - Text( - 'Nombre: ${user.displayName}', - style: - const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - - const SizedBox(height: 20), - - Text( - 'Email: ${user.email}', // Can never be null - style: const TextStyle(fontSize: 16), - ), - - const SizedBox(height: 20), - - ElevatedButton( - onPressed: () { - // Implement the "Gana dinero con Quimify" functionality here - }, - child: const Text('Gana dinero con Quimify'), - ), - - const SizedBox(height: 20), - - ElevatedButton( - onPressed: () async { - await UserAuthService.signOut(); - // Navigate back to the sign-in screen after signing out - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const SignInPage()), - ); - }, - child: const Text('Cerrar Sesión'), - ), - //const SizedBox(height: 15), - //const SizedBox(height: 5), // + 15 from cards = 20 - ], + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40.0), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 50), + + // Google Sign-In button + ElevatedButton.icon( + onPressed: () async { + final user = await UserAuthService.signInGoogleUser(); + if (user == null) return; + Navigator.of(context) + .pushReplacement(MaterialPageRoute( + builder: (context) => HomePage( + clientResult: clientResult, + user: user, + ), + )); + }, + icon: Image.asset( + 'assets/images/icons/google-logo.png', + height: 24, + ), + label: const Text('Iniciar Sesión con Google'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.black, + backgroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + ), + + const SizedBox(height: 50), + ], + ), + ); + }, + ), ), ), ), diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart index ca894d88..3ec7d722 100644 --- a/lib/pages/sign-in/sign_in_page.dart +++ b/lib/pages/sign-in/sign_in_page.dart @@ -65,44 +65,14 @@ class SignInPage extends StatelessWidget { const SizedBox(height: 20), - // Apple Sign-In button (assuming you have Apple sign-in set up) - ElevatedButton.icon( - onPressed: () async { - // TODO Handle Apple sign-in - final user = await UserAuthService.signInGoogleUser(); - if (user != null) { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => HomePage( - clientResult: clientResult, user: user))); - } - }, - icon: const Icon(Icons.apple, - color: Colors.white, size: 24), - label: const Text('Iniciar Sesión con Apple'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: Colors.black, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - ), - ), - ), - - const SizedBox(height: 20), - // Anonymous Sign-In button ElevatedButton.icon( onPressed: () async { final user = await UserAuthService.signInAnonymousUser(); - if (user != null) { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => HomePage( - clientResult: clientResult, user: user))); - } + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => HomePage( + clientResult: clientResult, user: user))); }, icon: const Icon(Icons.person_off_outlined, color: Colors.grey, size: 24), diff --git a/lib/storage/storage.dart b/lib/storage/storage.dart index 5c3ca3ff..f50de25a 100644 --- a/lib/storage/storage.dart +++ b/lib/storage/storage.dart @@ -17,4 +17,8 @@ class Storage { String? get(String key) => sharedPreferences.getString(key); save(String key, String value) => sharedPreferences.setString(key, value); + + saveBool(String key, bool value) => sharedPreferences.setBool(key, value); + + getBool(String key) => sharedPreferences.getBool(key); } diff --git a/pubspec.lock b/pubspec.lock index f1f911c4..97186187 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: archive - sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.3.9" + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -77,34 +77,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972 + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.12" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "7.3.2" built_collection: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: built_value - sha256: ff627b645b28fb8bdb69e645f910c2458fd6b65f6585c3a53e0626024897dedf + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.6.2" + version: "8.9.2" characters: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "315a598c7fbe77f22de1c9da7cfd6fd21816312f16ffa124453b4fc679e540f1" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.10.0" collection: dependency: "direct main" description: @@ -173,10 +173,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: b502a681ba415272ecc41400bd04fe543ed1a62632137dc84d25a91e7746f55f + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.2" connectivity_plus_platform_interface: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" csslib: dependency: transitive description: @@ -229,18 +229,18 @@ packages: dependency: "direct dev" description: name: dependency_validator - sha256: "08349175533ed0bd06eb9b6043cde66c45b2bfc7ebc222a7542cdb1324f1bf03" + sha256: f727a5627aa405965fab4aef4f468e50a9b632ba0737fd2f98c932fec6d712b9 url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.3" device_frame: dependency: transitive description: name: device_frame - sha256: afe76182aec178d171953d9b4a50a43c57c7cf3c77d8b09a48bf30c8fa04dd9d + sha256: d031a06f5d6f4750009672db98a5aa1536aa4a231713852469ce394779a23d75 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" device_info_plus: dependency: "direct main" description: @@ -253,26 +253,26 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" device_preview: dependency: "direct main" description: name: device_preview - sha256: "2f097bf31b929e15e6756dbe0ec1bcb63952ab9ed51c25dc5a2c722d2b21fdaf" + sha256: a694acdd3894b4c7d600f4ee413afc4ff917f76026b97ab06575fe886429ef19 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" diacritic: dependency: "direct main" description: name: diacritic - sha256: a84e03ec2779375fb86430dbe9d8fba62c68376f2499097a5f6e75556babe706 + sha256: "96db5db6149cbe4aa3cfcbfd170aca9b7648639be7e48025f9d458517f807fe4" url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.1.5" envied: dependency: "direct main" description: @@ -317,18 +317,18 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: @@ -431,10 +431,10 @@ packages: dependency: transitive description: name: flutter_spinkit - sha256: b39c753e909d4796906c5696a14daf33639a76e017136c8d82bf3e620ce5bb8e + sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.2.1" flutter_test: dependency: "direct dev" description: flutter @@ -444,10 +444,10 @@ packages: dependency: "direct main" description: name: flutter_typeahead - sha256: e2070dea278f09ae30885872138ccae75292b33b7af2c241fec5ceafd980c374 + sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.2.0" flutter_web_plugins: dependency: transitive description: flutter @@ -457,18 +457,18 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -481,10 +481,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "0c56c2c5d60d6dfaf9725f5ad4699f04749fb196ee5a70487a46ef184837ccf6" + sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" url: "https://pub.dev" source: hosted - version: "0.3.0+2" + version: "0.3.1+4" google_mobile_ads: dependency: "direct main" description: @@ -505,42 +505,42 @@ packages: dependency: transitive description: name: google_sign_in_android - sha256: bfd42c81c30c6faba16e0f62968d5505a87504aaa672b3155ee931461abb0a49 + sha256: "0608de03fc541ece4f91ba3e01a68b17cce7a6cf42bd59e40bbe5c55cc3a49d8" url: "https://pub.dev" source: hosted - version: "6.1.21" + version: "6.1.30" google_sign_in_ios: dependency: transitive description: name: google_sign_in_ios - sha256: a7d653803468d30b82ceb47ea00fe86d23c56e63eb2e5c2248bb68e9df203217 + sha256: "4898410f55440049e1ba8f15411612d9f89299d89c61cd9baf7e02d56ff81ac7" url: "https://pub.dev" source: hosted - version: "5.7.4" + version: "5.7.7" google_sign_in_platform_interface: dependency: transitive description: name: google_sign_in_platform_interface - sha256: df6c91e74b7bb07335c864434c2064007dafa5539a96adeca6e83811be583ec4 + sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" google_sign_in_web: dependency: transitive description: name: google_sign_in_web - sha256: a278ea2d01013faf341cbb093da880d0f2a552bbd1cb6ee90b5bebac9ba69d77 + sha256: "042805a21127a85b0dc46bba98a37926f17d2439720e8a459d27045d8ef68055" url: "https://pub.dev" source: hosted - version: "0.12.3+2" + version: "0.12.4+2" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" html: dependency: transitive description: @@ -585,10 +585,10 @@ packages: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -609,10 +609,34 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: @@ -641,34 +665,34 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.6" nested: dependency: transitive description: @@ -697,10 +721,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider_linux: dependency: transitive description: @@ -713,26 +737,26 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: name: petitparser - sha256: a9346a3fbba7546a28374bdbcd7f54ea48bb47772bf3a7ab4bfaadc40bc8b8c6 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "6.0.2" photo_view: dependency: "direct main" description: @@ -745,10 +769,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -761,42 +785,34 @@ packages: dependency: transitive description: name: pointer_interceptor - sha256: bd18321519718678d5fa98ad3a3359cbc7a31f018554eab80b73d08a7f0c165a + sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" url: "https://pub.dev" source: hosted - version: "0.10.1" + version: "0.10.1+2" pointer_interceptor_ios: dependency: transitive description: name: pointer_interceptor_ios - sha256: "4282ebfe21b54e21e26ab982c6086f0a67dc63423026bfba8db39a2e22045f26" + sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917 url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.10.1" pointer_interceptor_platform_interface: dependency: transitive description: name: pointer_interceptor_platform_interface - sha256: "59a446ead3be360bde72c3725f5ecacbba203c8a760e3061024c20f7da53f825" + sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.10.0+1" pointer_interceptor_web: dependency: transitive description: name: pointer_interceptor_web - sha256: dfd32b9c6e01a18f80535e7791e9d3add009a3395645d6db2f1e22b2642bfab4 - url: "https://pub.dev" - source: hosted - version: "0.10.1" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "0.10.2+1" pool: dependency: transitive description: @@ -809,10 +825,10 @@ packages: dependency: transitive description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -825,66 +841,66 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.5.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shelf: dependency: transitive description: @@ -897,10 +913,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" signed_spacing_flex: dependency: "direct main" description: @@ -918,18 +934,18 @@ packages: dependency: "direct main" description: name: smooth_page_indicator - sha256: "725bc638d5e79df0c84658e1291449996943f93bacbc2cec49963dbbab48d8ae" + sha256: "3b28b0c545fa67ed9e5997d9f9720d486f54c0c607e056a1094544e36934dff3" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0+3" source_gen: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_span: dependency: transitive description: @@ -982,10 +998,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" timing: dependency: transitive description: @@ -1014,66 +1030,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.3.10" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.2.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2 + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.2" vector_math: dependency: transitive description: @@ -1082,6 +1098,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" watcher: dependency: transitive description: @@ -1094,34 +1118,42 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.0.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" webview_flutter: dependency: "direct main" description: name: webview_flutter - sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932" + sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.9.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934 + sha256: "6e64fcb1c19d92024da8f33503aaeeda35825d77142c01d0ea2aa32edc79fdc8" url: "https://pub.dev" source: hosted - version: "3.16.0" + version: "3.16.7" webview_flutter_platform_interface: dependency: transitive description: @@ -1134,42 +1166,42 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7 + sha256: "1942a12224ab31e9508cf00c0c6347b931b023b8a4f0811e5dec3b06f94f117d" url: "https://pub.dev" source: hosted - version: "3.13.0" + version: "3.15.0" win32: dependency: transitive description: name: win32 - sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.5.4" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.5" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.5.0" yaml: dependency: transitive description: @@ -1179,5 +1211,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: "3.2.6" - flutter: ">=3.16.6" + dart: "3.5.3" + flutter: ">=3.24.0" From d5d429d33791cce95151dc903745cbb78db39919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Ortiz?= Date: Fri, 4 Oct 2024 18:38:01 +0200 Subject: [PATCH 17/19] Updated flutter to 3.24.3 & dart to 3.5.3 --- android/app/build.gradle | 15 ++++++--------- android/build.gradle | 15 +-------------- android/settings.gradle | 32 +++++++++++++++++++++----------- pubspec.yaml | 2 +- 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index e1bf436e..99b62792 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '49' @@ -21,10 +22,6 @@ if (flutterVersionName == null) { flutterVersionName = '3.1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { diff --git a/android/build.gradle b/android/build.gradle index 55c57112..8f31e8ca 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -28,4 +15,4 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bcf..69d1b623 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,21 @@ -include ':app' - -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() - -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } - -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + includeBuild "$flutterSdkPath/packages/flutter_tools/gradle" + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.1.2" apply false + id "org.jetbrains.kotlin.android" version "1.8.10" apply false +} +include ":app" \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index c3a1a7ea..314396b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -123,7 +123,7 @@ flutter: weight: 100 environment: - sdk: "3.2.6" + sdk: "3.5.3" # Icon: flutter_icons: From 5a03cc1e3dd3617f671218bb4e8b90d1c91a700e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Ortiz?= Date: Fri, 4 Oct 2024 18:55:50 +0200 Subject: [PATCH 18/19] Fixed Merge conflict --- lib/main.dart | 9 ++++----- lib/pages/home/home_page.dart | 9 +++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c07f5e7c..e0150bb8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -33,13 +33,12 @@ main() async { SecurityContext.defaultContext.setTrustedCertificatesBytes(bytes); } catch (_) {} // It's already present in modern devices anyways - bool isAnonymouslySignedIn = - await UserAuthService().getisAnonymouslySignedIn(); + bool skippedLogin = await UserAuthService().hasSkippedLogin(); - QuimifyIdentity? user = - await UserAuthService.handleSilentAuthentication(AuthProviders.google); + QuimifyIdentity? user = await UserAuthService.handleSilentAuthentication(); + + bool hasToLogin = skippedLogin == false && user == null; - bool hasToLogin = isAnonymouslySignedIn == false && user == null; ClientResult? clientResult = await Api().getClient(); Ads().initialize(clientResult); diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index a9b999eb..e5e86a11 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -192,10 +192,11 @@ class _HomePageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => ProfilePage( - user: widget.user, - )), - ); + builder: (context) => ProfilePage( + user: widget.user, + )), + ); + } }, ), ) From 795b45f3f16f8805b583a12ea9b752582a1ffd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Ortiz?= Date: Sat, 5 Oct 2024 19:35:33 +0200 Subject: [PATCH 19/19] Improved logic Formatted imports, implemented SOLID principles, added singleton design patter. --- ...{sign_in_api.dart => userAuthService.dart} | 97 +++++++++++++------ lib/main.dart | 36 ++----- lib/pages/home/home_page.dart | 44 ++------- lib/pages/home/widgets/quimify_avatar.dart | 54 +++++++++++ lib/pages/profile/profile_page.dart | 96 +++++++++--------- lib/pages/sign-in/sign_in_page.dart | 19 ++-- lib/pages/widgets/bars/quimify_page_bar.dart | 7 +- 7 files changed, 198 insertions(+), 155 deletions(-) rename lib/internet/api/sign-in/{sign_in_api.dart => userAuthService.dart} (62%) create mode 100644 lib/pages/home/widgets/quimify_avatar.dart diff --git a/lib/internet/api/sign-in/sign_in_api.dart b/lib/internet/api/sign-in/userAuthService.dart similarity index 62% rename from lib/internet/api/sign-in/sign_in_api.dart rename to lib/internet/api/sign-in/userAuthService.dart index 23e003a5..9d319162 100644 --- a/lib/internet/api/sign-in/sign_in_api.dart +++ b/lib/internet/api/sign-in/userAuthService.dart @@ -1,9 +1,14 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/internet/api/sign-in/info_google.dart'; import 'package:quimify_client/storage/storage.dart'; class UserAuthService { static final _googleSignIn = GoogleSignIn(scopes: scopes); + static final UserAuthService _instance = UserAuthService(); + static QuimifyIdentity? _user; static List scopes = [ 'email', @@ -11,34 +16,82 @@ class UserAuthService { 'https://www.googleapis.com/auth/user.gender.read' ]; - static Future signOut() async { + // * Constructor, Singleton pattern + initialize() async { + try { + // Sets ISRG Root X1 certificate, not present in Android < 25 + var certificate = await rootBundle.load('assets/ssl/isrg-x1.crt'); + var bytes = certificate.buffer.asUint8List(); + SecurityContext.defaultContext.setTrustedCertificatesBytes(bytes); + } catch (_) {} // It's already present in modern devices anyways + } + + // * Getters + + static UserAuthService getInstance() { + return _instance; + } + + static QuimifyIdentity? getUser() { + return _user; + } + + static bool loginRequiered() { + return _instance.hasSkippedLogin() == false && + UserAuthService._user == null; + } + + bool hasSkippedLogin() { + final prefs = Storage(); + return prefs.getBool('userSkippedLogIn') ?? false; + } + + // * Methods + + static Future signOut() async { if (_googleSignIn.currentUser != null) await _googleSignIn.signOut(); final prefs = Storage(); await prefs.saveBool('isAnonymouslySignedIn', false); + UserAuthService._user = null; + return true; } - static Future signInGoogleUser() async { + Future signInGoogleUser() async { final user = await _googleSignIn.signIn(); if (user == null) return null; - return signInPOST(user); + UserAuthService._user = await _signInPOST(user); + return UserAuthService._user; } - static Future signInAnonymousUser() async { - bool state = await logInPOST(null); + Future signInAnonymousUser() async { + bool state = await _logInPOST(null); final prefs = Storage(); await prefs.saveBool('isAnonymouslySignedIn', state); QuimifyIdentity identity = QuimifyIdentity(); - return identity; + UserAuthService._user = identity; + return UserAuthService._user; } - Future hasSkippedLogin() async { - final prefs = Storage(); - return prefs.getBool('userSkippedLogIn') ?? false; + Future handleSilentAuthentication() async { + final googleUser = await _googleSignIn.signInSilently(); + if (googleUser != null) { + bool state = await _logInPOST(googleUser); + QuimifyIdentity identity = QuimifyIdentity( + googleUser: googleUser, + photoUrl: googleUser.photoUrl, + displayName: googleUser.displayName ?? 'Quimify', + email: googleUser.email, + ); + return state ? identity : null; + } + return null; } + // * Private Class methods + // TODO: Implement error handling for login requests - static Future signInPOST( - GoogleSignInAccount googleUser) async { + // + Future _signInPOST(GoogleSignInAccount googleUser) async { var data = await getInfoGoogle(googleUser); QuimifyIdentity identity = QuimifyIdentity( googleUser: googleUser, @@ -47,15 +100,13 @@ class UserAuthService { email: googleUser.email, gender: data['gender'], birthday: data['birthday']); - //formato api.quimify.com/login?id=...&email=...&gender=...&birthday=... - print('Enviado /login'); - //formato api.quimify.com/login?id=...&email=...&gender=...&birthday=... + //format: api.quimify.com/login?id=...&email=...&gender=...&birthday=... return identity; } // TODO: Logic of hhtp request (make sure to handle errors) //* Return true if login was successful, false otherwise - static Future logInPOST(GoogleSignInAccount? googleUser) async { + Future _logInPOST(GoogleSignInAccount? googleUser) async { // If user is null, it means that the user is not logged in if (googleUser == null) return false; @@ -64,24 +115,8 @@ class UserAuthService { /// which can be securely validated on the server. var id = googleUser.authentication.then((value) => value.idToken); //formato api.quimify.com/login?id=...&email=...&gender=...&birthday=... - print('Enviado /login' + id.toString()); return true; } - - static Future handleSilentAuthentication() async { - final googleUser = await _googleSignIn.signInSilently(); - if (googleUser != null) { - bool state = await logInPOST(googleUser); - QuimifyIdentity identity = QuimifyIdentity( - googleUser: googleUser, - photoUrl: googleUser.photoUrl, - displayName: googleUser.displayName ?? 'Quimify', - email: googleUser.email, - ); - return state ? identity : null; - } - return null; - } } class QuimifyIdentity { diff --git a/lib/main.dart b/lib/main.dart index e0150bb8..24927b1f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:quimify_client/internet/ads/ads.dart'; import 'package:quimify_client/internet/api/api.dart'; import 'package:quimify_client/internet/api/results/client_result.dart'; @@ -18,38 +17,22 @@ import 'package:quimify_client/pages/sign-in/sign_in_page.dart'; import 'package:quimify_client/routes.dart'; import 'package:quimify_client/storage/storage.dart'; -import 'internet/api/sign-in/sign_in_api.dart'; +import 'internet/api/sign-in/userAuthService.dart'; main() async { _showLoadingScreen(); + await UserAuthService().initialize(); + Ads().initialize(await Api().getClient()); await Storage().initialize(); - Api().initialize(); - - try { - // Sets ISRG Root X1 certificate, not present in Android < 25 - var certificate = await rootBundle.load('assets/ssl/isrg-x1.crt'); - var bytes = certificate.buffer.asUint8List(); - SecurityContext.defaultContext.setTrustedCertificatesBytes(bytes); - } catch (_) {} // It's already present in modern devices anyways - - bool skippedLogin = await UserAuthService().hasSkippedLogin(); - - QuimifyIdentity? user = await UserAuthService.handleSilentAuthentication(); - - bool hasToLogin = skippedLogin == false && user == null; ClientResult? clientResult = await Api().getClient(); - Ads().initialize(clientResult); - runApp( DevicePreview( enabled: false, // !kReleaseMode, builder: (context) => QuimifyApp( clientResult: clientResult, - user: user, - hasToLogin: hasToLogin, ), // Wrap your app ), ); @@ -65,16 +48,13 @@ _showLoadingScreen() { _hideLoadingScreen() => FlutterNativeSplash.remove(); class QuimifyApp extends StatelessWidget { - const QuimifyApp({ + QuimifyApp({ Key? key, this.clientResult, - required this.user, - required this.hasToLogin, }) : super(key: key); final ClientResult? clientResult; - final QuimifyIdentity? user; - final bool hasToLogin; + RouteObserver routeObserver = RouteObserver(); @override Widget build(BuildContext context) { @@ -83,8 +63,10 @@ class QuimifyApp extends StatelessWidget { value: const SystemUiOverlayStyle(statusBarColor: Colors.transparent), child: MaterialApp( title: 'Quimify', - home: hasToLogin != true - ? HomePage(clientResult: clientResult, user: user) + home: UserAuthService.loginRequiered() != true + ? HomePage( + clientResult: clientResult, + ) : SignInPage( clientResult: clientResult, ), diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index e5e86a11..358dff27 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:quimify_client/internet/api/results/client_result.dart'; -import 'package:quimify_client/internet/api/sign-in/sign_in_api.dart'; import 'package:quimify_client/pages/calculator/calculator_page.dart'; +import 'package:quimify_client/pages/home/widgets/quimify_avatar.dart'; import 'package:quimify_client/pages/home/widgets/quimify_menu_button.dart'; import 'package:quimify_client/pages/inorganic/inorganic_page.dart'; import 'package:quimify_client/pages/organic/organic_page.dart'; @@ -13,23 +13,19 @@ import 'package:quimify_client/pages/widgets/gestures/quimify_swipe_detector.dar import 'package:quimify_client/pages/widgets/quimify_colors.dart'; import 'package:quimify_client/pages/widgets/quimify_scaffold.dart'; -import '../profile/profile_page.dart'; - class HomePage extends StatefulWidget { const HomePage({ Key? key, required this.clientResult, - required this.user, }) : super(key: key); final ClientResult? clientResult; - final QuimifyIdentity? user; @override State createState() => _HomePageState(); } -class _HomePageState extends State { +class _HomePageState extends State with RouteAware { final List _pages = const [ InorganicPage(), OrganicPage(), @@ -127,8 +123,8 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { return PopScope( - onPopInvoked: (bool didPop) async { - if (!didPop) { + onPopInvokedWithResult: (bool didPop, Object? result) async { + if (didPop) { return; } @@ -136,7 +132,7 @@ class _HomePageState extends State { }, child: QuimifyScaffold.noAd( header: SafeArea( - bottom: false, // So it's not inside status bar + bottom: false, child: Container( padding: const EdgeInsets.only( top: 15, // TODO 17.5? @@ -171,35 +167,7 @@ class _HomePageState extends State { ), //const SizedBox(width: 120), const Spacer(), - Padding( - padding: const EdgeInsets.only(right: 15.0), - child: IconButton( - padding: EdgeInsets.zero, - iconSize: 38, - alignment: Alignment.centerRight, - icon: widget.user?.photoUrl != null - ? CircleAvatar( - backgroundImage: - NetworkImage(widget.user?.photoUrl ?? ''), - radius: 19, // Adjust radius as needed - ) - : Icon( - Icons.account_circle, - color: QuimifyColors.inverseText(context), - ), - onPressed: () { - if (widget.user != null) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ProfilePage( - user: widget.user, - )), - ); - } - }, - ), - ) + UserAvatar() ], ), ), diff --git a/lib/pages/home/widgets/quimify_avatar.dart b/lib/pages/home/widgets/quimify_avatar.dart new file mode 100644 index 00000000..0919004a --- /dev/null +++ b/lib/pages/home/widgets/quimify_avatar.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:quimify_client/internet/api/sign-in/userAuthService.dart'; +import 'package:quimify_client/pages/profile/profile_page.dart'; +import 'package:quimify_client/pages/widgets/quimify_colors.dart'; + +class UserAvatar extends StatefulWidget { + const UserAvatar({Key? key}) : super(key: key); + + @override + State createState() => _UserAvatarState(); +} + +class _UserAvatarState extends State { + QuimifyIdentity? user = UserAuthService.getUser(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 15.0), + child: IconButton( + padding: EdgeInsets.zero, + iconSize: 38, + alignment: Alignment.centerRight, + icon: user?.photoUrl != null + ? CircleAvatar( + backgroundImage: NetworkImage(user?.photoUrl ?? ''), + radius: 19, + ) + : Icon( + Icons.account_circle, + color: QuimifyColors.inverseText(context), + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProfilePage( + onUserUpdated: updateUser, + user: user, + ), + ), + ); + }, + ), + ); + } + + void updateUser(QuimifyIdentity? newUser) { + // Update the user state, so the page gets updated. + setState(() { + user = newUser; + }); + } +} diff --git a/lib/pages/profile/profile_page.dart b/lib/pages/profile/profile_page.dart index b92396e2..e3c3e50c 100644 --- a/lib/pages/profile/profile_page.dart +++ b/lib/pages/profile/profile_page.dart @@ -1,29 +1,44 @@ import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; -import 'package:quimify_client/internet/api/sign-in/sign_in_api.dart'; +import 'package:quimify_client/internet/api/sign-in/userAuthService.dart'; import 'package:quimify_client/pages/widgets/quimify_scaffold.dart'; -import '../../internet/api/results/client_result.dart'; -import 'package:quimify_client/pages/home/home_page.dart'; -import '../sign-in/sign_in_page.dart'; -import '../widgets/bars/quimify_page_bar.dart'; -import '../widgets/dialogs/loading_indicator.dart'; -import '../widgets/quimify_colors.dart'; +import 'package:quimify_client/internet/api/results/client_result.dart'; +import 'package:quimify_client/pages/widgets/bars/quimify_page_bar.dart'; +import 'package:quimify_client/pages/widgets/dialogs/loading_indicator.dart'; +import 'package:quimify_client/pages/widgets/quimify_colors.dart'; -class ProfilePage extends StatelessWidget { +class ProfilePage extends StatefulWidget { final ClientResult? clientResult; - final QuimifyIdentity? user; + final Function(QuimifyIdentity?) onUserUpdated; + QuimifyIdentity? user; - const ProfilePage({ - Key? key, - this.clientResult, - this.user, - }) : super(key: key); + ProfilePage( + {Key? key, this.clientResult, required this.onUserUpdated, this.user}) + : super(key: key); + + @override + State createState() => _ProfilePageState(); +} + +class _ProfilePageState extends State { + void _updateUserProfile() { + updateUserProfile(UserAuthService.getUser()); + } + + updateUserProfile(QuimifyIdentity? newUser) { + print("UPDATING USER PROFILE"); + + // UPDATE THE PROFILE PAGE + updateUser(newUser); + // REBUILD THE PROFILE PAGE + widget.onUserUpdated(newUser); + } @override Widget build(BuildContext context) { var defaultLogo = const AssetImage('assets/images/logo.png'); - if (user?.googleUser is GoogleIdentity && user != null) { + if (widget.user?.googleUser is GoogleIdentity && widget.user != null) { return PopScope( onPopInvokedWithResult: (didPop, dynamic) async { if (!didPop) { @@ -33,7 +48,8 @@ class ProfilePage extends StatelessWidget { hideLoadingIndicator(); }, child: QuimifyScaffold.noAd( - header: const QuimifyPageBar(title: 'Perfil'), + header: + QuimifyPageBar(title: 'Perfil', onPressed: _updateUserProfile), body: Container( width: 900, padding: const EdgeInsets.all(20), @@ -45,53 +61,39 @@ class ProfilePage extends StatelessWidget { //crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 20), - CircleAvatar( radius: 50, - backgroundImage: user?.photoUrl != null - ? NetworkImage(user?.photoUrl ?? '') + backgroundImage: widget.user?.photoUrl != null + ? NetworkImage(widget.user?.photoUrl ?? '') : defaultLogo, ), const SizedBox(height: 20), - Text( - 'Nombre: ${user?.displayName ?? 'No hay nombre disponible'}', // Use null-ish coalescing operator (??) + 'Nombre: ${widget.user?.displayName ?? 'No hay nombre disponible'}', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold), ), - const SizedBox(height: 20), - Text( - 'Email: ${user?.email}', // Can never be null + 'Email: ${widget.user?.email}', style: const TextStyle(fontSize: 16), ), - const SizedBox(height: 20), - ElevatedButton( onPressed: () { - // Implement the "Gana dinero con Quimify" functionality here + // TODO: Implement the "Gana dinero con Quimify" functionality here }, child: const Text('Gana dinero con Quimify'), ), - const SizedBox(height: 20), - ElevatedButton( onPressed: () async { await UserAuthService.signOut(); - // Navigate back to the sign-in screen after signing out - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => const ProfilePage()), - ); + + updateUserProfile(UserAuthService.getUser()); }, child: const Text('Cerrar Sesión'), ), - //const SizedBox(height: 15), - //const SizedBox(height: 5), // + 15 from cards = 20 ], ), ), @@ -118,19 +120,12 @@ class ProfilePage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 50), - - // Google Sign-In button ElevatedButton.icon( onPressed: () async { - final user = await UserAuthService.signInGoogleUser(); + final user = + await UserAuthService().signInGoogleUser(); if (user == null) return; - Navigator.of(context) - .pushReplacement(MaterialPageRoute( - builder: (context) => HomePage( - clientResult: clientResult, - user: user, - ), - )); + _updateUserProfile(); }, icon: Image.asset( 'assets/images/icons/google-logo.png', @@ -146,7 +141,6 @@ class ProfilePage extends StatelessWidget { ), ), ), - const SizedBox(height: 50), ], ), @@ -158,4 +152,10 @@ class ProfilePage extends StatelessWidget { ), ); } + + void updateUser(QuimifyIdentity? newUser) { + setState(() { + widget.user = newUser; + }); + } } diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart index 3ec7d722..e6f63ada 100644 --- a/lib/pages/sign-in/sign_in_page.dart +++ b/lib/pages/sign-in/sign_in_page.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:quimify_client/pages/home/home_page.dart'; -import '../../internet/api/results/client_result.dart'; -import '../../internet/api/sign-in/sign_in_api.dart'; -import '../widgets/quimify_colors.dart'; +import 'package:quimify_client/internet/api/results/client_result.dart'; +import 'package:quimify_client/internet/api/sign-in/userAuthService.dart'; +import 'package:quimify_client/pages/widgets/quimify_colors.dart'; class SignInPage extends StatelessWidget { final ClientResult? clientResult; @@ -25,7 +25,6 @@ class SignInPage extends StatelessWidget { child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Center( - // Added Center widget for vertical centering child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -44,11 +43,11 @@ class SignInPage extends StatelessWidget { // Google Sign-In button ElevatedButton.icon( onPressed: () async { - final user = await UserAuthService.signInGoogleUser(); + final user = await UserAuthService().signInGoogleUser(); if (user == null) return; Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => HomePage( - clientResult: clientResult, user: user))); + builder: (context) => + HomePage(clientResult: clientResult))); }, icon: Image.asset('assets/images/icons/google-logo.png', height: 24), @@ -69,10 +68,10 @@ class SignInPage extends StatelessWidget { ElevatedButton.icon( onPressed: () async { final user = - await UserAuthService.signInAnonymousUser(); + await UserAuthService().signInAnonymousUser(); Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => HomePage( - clientResult: clientResult, user: user))); + builder: (context) => + HomePage(clientResult: clientResult))); }, icon: const Icon(Icons.person_off_outlined, color: Colors.grey, size: 24), diff --git a/lib/pages/widgets/bars/quimify_page_bar.dart b/lib/pages/widgets/bars/quimify_page_bar.dart index cafb319f..c9ec7d2d 100644 --- a/lib/pages/widgets/bars/quimify_page_bar.dart +++ b/lib/pages/widgets/bars/quimify_page_bar.dart @@ -7,8 +7,10 @@ class QuimifyPageBar extends StatelessWidget { const QuimifyPageBar({ Key? key, required this.title, + this.onPressed, }) : super(key: key); + final VoidCallback? onPressed; // Add this optional parameter final String title; @override @@ -27,7 +29,10 @@ class QuimifyPageBar extends StatelessWidget { QuimifyIconButton.square( height: 50, backgroundColor: QuimifyColors.secondaryTeal(context), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => { + onPressed?.call(), + Navigator.of(context).pop(), + }, icon: Icon( Icons.arrow_back, color: QuimifyColors.inverseText(context),