From 13d77e118c4aafd2d0d84f41bf6cdd53f4a916b1 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Tue, 9 Sep 2025 22:40:27 +0300 Subject: [PATCH] Initial push to my private server --- android/app/build.gradle | 4 +- android/app/src/main/AndroidManifest.xml | 12 +- android/build.gradle | 2 +- .../reports/problems/problems-report.html | 2 +- android/settings.gradle | 2 +- assets/images/mtn.png | Bin 0 -> 56791 bytes ios/Podfile.lock | 6 + ios/Runner.xcodeproj/project.pbxproj | 8 - ios/Runner/Info.plist | 199 ++++---- lib/controller/auth/login_controller.dart | 1 + lib/controller/functions/crud.dart | 482 +++++++++++------- .../functions/network/connection_check.dart | 48 ++ .../functions/network/net_guard.dart | 48 ++ .../home/map_passenger_controller.dart | 95 ++++ lib/controller/local/translations.dart | 22 + .../payment/payment_controller.dart | 154 +++--- lib/main.dart | 27 +- .../my_wallet/passenger_wallet_dialoge.dart | 201 +++++--- pubspec.lock | 56 ++ pubspec.yaml | 4 +- 20 files changed, 921 insertions(+), 452 deletions(-) create mode 100644 assets/images/mtn.png create mode 100644 lib/controller/functions/network/connection_check.dart create mode 100644 lib/controller/functions/network/net_guard.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 519e006..f98c0dc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,8 +47,8 @@ android { // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 29 targetSdk = 36 - versionCode = 11 - versionName = '1.0.11' + versionCode = 13 + versionName = '1.0.13' multiDexEnabled = true ndk { abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b17173d..d7cc69f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -43,15 +43,15 @@ - - + + + + - - - - + + diff --git a/android/build.gradle b/android/build.gradle index 395d584..f4c4022 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -30,7 +30,7 @@ buildscript { // classpath 'com.android.tools.build:gradle:7.3.1' classpath 'com.google.gms:google-services:4.3.15' // END: FlutterFire Configuration - classpath 'com.android.tools.build:gradle:8.11.0' + classpath 'com.android.tools.build:gradle:8.11.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index 128d91d..e1e2989 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/android/settings.gradle b/android/settings.gradle index 4fc9848..9992d0a 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.11.0' apply false + id "com.android.application" version '8.11.1' apply false // START: FlutterFire Configuration id "com.google.gms.google-services" version "4.3.10" apply false // END: FlutterFire Configuration diff --git a/assets/images/mtn.png b/assets/images/mtn.png new file mode 100644 index 0000000000000000000000000000000000000000..6aa6618d29adc50f906614ca31331a6bff1efa37 GIT binary patch literal 56791 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 z zAA=@;*WALiBf?5ECYH}SGO1W}e&KG!QPxY%NS8WfnXKS%KI^ENC>x+!t0`kp9we=O zpwWCmnDV$721Gc!e(lT&T`^(W(nMUr_K~?Yr(d`IKG!*?Q?Q3Gic6r8?&7llu+d#} zu~udt6@(}8`rxJOdlFKuR=CGz}32~6Bn@=0~jL?5w?s& z+RL%IU{r*g(&_ODOEiGWGKi3JWI%yZ5&M9i#^oaG$GXl>SVs{{zfJF1K;mMlJvyHG zgR~rMFLPlp5D{{O=^fO5hu33O`-Vu-RsyVxpbA8mZr(~2mWBe1=auego zfYXdWnx<%-Pse>!GCMex3PdXu(T%IXov9Y`P{tA^ui7fdVUM9GKeVZq8#2{%r`Q0J zK@V}be>(udkht|T_#}aL;|*`SoE#8eU-Ix_w@C9D1s;ba2!?UGWmlNOadDWZX7R># ziBTVB&LJL~(Z9&v!w+G`$=&k<^0&<$9@hq^aEZ_MstqOSJP1PjS3q+R1H zAEbrp=zI#rQ8A5?#~!sFpKuV#pcaV#=~9tnsuX>`A1qLmYI%)s9CBp^jLE{Z`#k*U zQ+dtgiy|~VY?samae_vqX`0)KHoN!bb85DhCh9uGZg0{pFmxRgF z_L>e0^RYd?isp<&7qyzGi|1eqi(vJkbK%tP9O5+@Fe{=kWSr+ImzN4OHiI7iz>r=6 zX9(nzF5q#65Q0=Gq}c`%3YVbLRLIb%m?h94OVPT`8hOmebOM~*-*k2#JYR!3#A2SZ zcCDfiA&(0SNWQwZKrUn|ek-mo;)Xi2{cV_}y6yt7!iW)D8(jcT z+`#>rolaX6EK|~dV;C03z>5(Np86i;v$_v+gCU>rWPYHvlM*`h_djYAn5K{`RNlU> z3cj|Vhl-K2@GK@-x~w>H2Px0?x8G6jK53#)QaG3uExd%7@chzMr!nYUT=YHjOrzQd zB}O$G36n@)%X|Ss<8iDmrL@{Tkzu}oa@CB1EEgd+H>Ugr!?Kl;xH$0>`-N-%ZmDv@ zupqt0mUjO>cZaH#CE=zY7r|%MYSof!`e$`wBYMWdJw>l1l+o}!C$8PMCL+PI6mj|1 zXl)6usC7-PY))b&Duff)0i@ZCfiQq3NY#UWnXqHoah$x6zfbE`R_F0HuL^%Ikm5k_ z#n$}rBjcR%`IN_i@kx9e$$QQrFoSF-Aq^&ClKf0+q~V&WwZ#b)cLJ9Ws5ARs6*>n) z)#gAR>^*{_BKS2taLd(``3}&4`f-)MNn0Ot{;E)3m^M1uAncz?Z46NX2vuY07z1OK zokTXe&vvQ+(p7kn7NK)dJ)0aIkUHILDp=;sJzfEx1fe(_Si?G3s%%yhvR4q*oDyMikExbiX%d268;t^>xM3B9P_rs+m!+Pw0WLHue$oV- zH7h?OE0-IaBnE_vFL?|PUyeuXxbI`4X?n-Xa1HQ6JiGS89R=*e*#Ui$*vU@v85`>a}L6tp^%-=LD+Z5|Pk!2kh)XCnI z7g4{bMqp_)r+UXFgl5F~4)&*yBg6Ttu)^MlM`-F&-h;qvqW^Z9{z=2_X~Jfj?TT9f zYZ!Wmv&nFcQA}ZUrYL;s$|kX+q1k5$&6 zhZQp(OR&Ry{4vJ)ff1=ILeCVWxv?!+>M_WW0Hk**MQQ6&F|Y+ezE^)vM*W<63w_^t zMqLryMIW4TcN6#gB%$&EYu&XTWrRf7`K@*eR2>MdxjCy%mhQE5hRduT_(xq8Nk9Ex z1u4G41eBg$V)DWw+@Tp{#a~Er{M$2s-n&r07KH<*+2kmQ6{uC@99%!6Rag&~pKisz z3%BpTv3-OIY?FW+*eSztI&g0L1l>AS*BE)^JV_=`H=Dj$Uj?#X3?=9`6+1g^pENaVlLSQ!+13!Czfb?Bno%WXw2& z)k@+GFPJVQ^Jm$9Emc^#yVl89V4a3M;9jttTU4qN&4>CejfCOwG$hYzdYB86nh+g| z`em%E0XV~Lz;*`9Rll1KyCXoH=nkjALx{Zguu zYs%|L(IC|wB)3_e+2Bn0_72I&$x_rE`BPIA{w^OQG=xYpfDu?8pybE{kk~(JVD6m- z@Ni!v{boLxl!h9C~%6`cpEO!D((o827ieP-uzJ`ui;t-Gx2 z5jP;gS|t(!pf-0T$AE7s#DGTm+Mh=mdkQHAG|V4S?gRUx?9%J14Owzf(!h1e5+F51 zi;U7I@6nrsMCUUu`Y9v(_ZL1dAurJ{_9WltCIQK^zvdCG!hGxKd}eV^6|Sz+L++Hz zb-8&8b(ph+suc2gxqk&QK#aW(!A3B)Yip+XN-?=AudoS4#8%0|f~HL_(`H*=2(50K z%6NHt70@XXC+m(T((l~|2mTyJIyeG$82&%S8 zdN}84Q3%8pnv=q!Z9CfigiXP=z#A;%m)aJYrMRe)Nk~!JP9N8oeTq>GGL`vxD2@eN zBx=^#g_B*uH-|uRLDTw_kc`e4_nJP$FY|Ee3J8;<*x(oVa;zF)NkYe{u+!e6$l?34 zg$NHk%cAvz0lq-U0@XvDJjeU*B&JGhUT=t~YW5YbPKlB3LSsKpDd+FyxFy*FX)U~4 zXs%B_`@oW5&meD5D~&>H@$-U^{NX-s;%MvvN`Ryn8hy5bUAvdDBM92iV>LZlvhR_b zz`c?SFvWR20%vJ!&wd`j>EQ1RtN>%8m`64-{3^Ls;)+){fHmHTyt^%KOZU2j9lD*7 z*=LH{F^+`wtya{m@;w{+!-t*~fZH0?8j-L3VYl)|!&H)rBhf6tSqm(-C=I!?KtyD| zU@qAIPH^Vw?K^}g0F+(0mBE)XvoQ8hL~ttnO)KF*6 ztB(|0eEjQgRzPHbk+awHQ8uC3^Ic`lAY3vLZ^?41KlEpXn+)mcf=~xxUVh7dW(N3) zZ%-+?DrGuE3V^hHr8(trTc86dr@imYOVHNJCb!f+ZmUO|T9VG?q#qa!3@+Q|LUdbi zc?56qnE0Yx87XCm#_Yg!`jim1)ch4&3@+f4T`v$5I2d(OvYuBsg*HGTWF{&Qbfyqt zJ4%Jw3^?m~e+ERhqh+M!$>xXO8W9@n8Yg^F=4)%36`-${eL!U`l z`fV!DTz*WF=R=_a6ULrVOd(WtOPQ9C*IWzT)P$(SmJ_P-m&&R6Uhtc!k1r

5Ip0>Sz7^iQr~>tA_n#*>>HMU|1>GE+C=_(Q{4=?Ms*!`bBmX}U-xUbsQnC3`1II7)*r zx@QOee~8kCZu3*fkSHJ$noTq#zi31^_!!d=5XGgT(Lg*5PgpPqg9&Nahwi2TXou;V zoDpl6n7Z)CC6hy94T}p)v;xu1;&f$ct*B+}iUg1EuA$BOBsY$W6V_MvX zk;_2F!-J-dc0M_Ple40EigHR<=Rvp1&#An8>{o}?d#8t0#+(t9U#7Q$ed50GExJ5o z2uL6E8fr?^k#?m}KCqrFd$ z+$6xV@5%n6m0*BleZfG&NvW@@A!Ue@`p8@lP?tH?r$!_(Y@gH5#f|)R@oxFI5kW~X z49|&rmW7{B5|_mb#gSDR)nOF*V!hz+ToeUb_899{fV2$^n2PidL;Q|Gg&TO#8g|(@ z@}AU|WH2J>>SMwecn4Z2Rl5pej;EJM%^+^KJIpfXjBVwR5TTziOTsCn)TXz^u1??M zdaDWNtY&T>;SFc9pk~LJnl)#MI~yif!Dq5qyIjoijVa5q_l~S0Plu?la#s&WR6edw zD1#QHAyoSM(e7QgKufA#AqcK0GD>2td)^b@mH2;Lz~)ii87xsJccIBeLI7pnpEp!* z#E>2*3;g2b0>*$8ewB6*pU^_W!MKfx40D)8O?SbG2_OK~1!k`)gsKrc`IxE^xvJWF zq26?3PV=C?%032ydTxoe`6}F~l`-e(6AS)@INn!fow{gnKqT33utlC0?&t?g6dX6a zvzMQYg4dVD>NY`dpdnh$TEW?bjEF`At{x;-sB+;<&G{@*NTJuMddbfd{{t~GkVA@| z?U(}80kw3oe*H99&F~KdJHv&zSonn*H7x`?gj*64oU)ih7>xYFR6YAE+>1WmuLl?U z#kV?EsobJFd1?asx_q0eL0wzDEXhg`iAYpkKnnk$H$)i(@r9^y)%^i}{+z10-5zrG zu9>d7>4TKs8!*QSwiM|BsBIEvL2$YS7dm+OC0*eru{s)i??e0xR<93U&Gm1*UMxT8 z0>vA9*lDR$2@-i|JQdA2+9ZSqo|KEjHH=iL3UZmVP?3=q+`oD6oq8kYpx5O4Cg}}4 zQ(#)F#YE!PZlFt#gD+3V6CT=vMULX=9ZZPl<*Ow7cm199$i80N8WiX!X%RqZwB&$K z<3wa!IYMci1*$q!1*1TWK^l(rz*}Y`mcrqxY9;ARN${@yK0%uHUa&vd!~_{i@!Vfmpr>OrB9-?b`ZKy;QLL%6DWS z_GJe-zG!a{R?4W+gxv`%jaHWr6<6{Hp7O&-FwcStag%rV%zbD+MRZnJlDy-t+&5`M zV;sW>qlhqS8RgeACim17F<%XsIEeHx9; zh1XnK8&WlQ&C1J;FqzkRkswt>*S9_%vr0*YTPJ1HU+rdoZC3h~bdzTpg)feQp)e5W}T zRW||rj~^h$8ogkw4gUkH_BGx+g@+27qLJ`@=R|XPWl73`R-X5Th0_w96p^d5qJCMU zo%AF_fF@P5sM$_Qi%6w%p)%FT1l4DSju4W3hjDd*S3(kAmQanMJJ9p8YKI-wZ73RK z3B`r(%n#<>~xkw&9B zWIMzbof1$p-&sF<{C=Zyfy;xDW^1awy8Xss_cAF}RZaUPN}BzD0DC}$zutx9TcG$F zdFm{6=EfpnBsX;cjRt^Wj#9^jv!Ps4KU2O1F1wj#SIa!RT1s8RNEYZvg*^hLxMd#v;S(Iw7#14r%JZy-MG(_)UJu$$e&H^yaakt1cC$|?Q5~NuU z^A0y%LO75Jjv2aQwVLpTPRm;peqO84NIuS7=xR)`o$03S*|cQ)HXCoZ?c`dv4a~2G znO{}vZ0Z%AbQ}&UHCWo>Jg;B!3_HY2zs3m79!cjO;&mFv%`L!W@PnffNMz>Ntpl ziaeYM8gf+)nZPsb3%fbNg)C!NZ4z^SPCW>v5Iv$rfD*#|!32Nmc{+HRwm9J;3; zV0=vrst8z;igmV8@mgJgq6eo3P`)yKNCCaG3*r^{W_)1Us zPG1yN)({kfP06}TgNZ+b6+G0l#~zxz<^cJ4ZPad6M{K4vLZbx#NT=EHt1dN1x9zZX zgBv+@J7~S6Q&FRBc|?_lu_427ja!W!3_W)B%9#E9x##FS#=N7^MpspV_i*sX>p^v_ zo#C9$9USVh4I74Sbg+-xl{oVR(8_A4nkwjuHlG@3EJK+9j=-clLayDMvDe;u+pcpB zf&&U^6;5Wm$XABA#}|9xqj;@X?!_|U9G{ZiVt99jb68`xZQY_B+1Y0YH}=||(VA^# zmwSJij#ooQBwRzgzDZL}W&uEPmKHi=*7ztF0ZQ{bnb{mlLMQ?1_}Ug zDuO8ytT~OJ{Q%JmGq+%J&DL*TZ`-zQv3)B|G!VE1bEy%%(~WDMh^`D3zh~#6($q49+g)%^U6h zlgDlQ)(rqj6VGt6RFl>Ud6pUORhAeMaYjpP>vhIv63?PTpqiyoOwtQ$SkXPeF>XzQ zA?xBh7wm1{=N#M3 zF`K%19jV=G`wt$qom=+V`u%!sqpZsqMmwN;# zxxfvV3fT3{t7Gt*V@Nv9PK;SEH-8*Ba>(w3iHG;?vW+7HEr8Ob&wF1%UX2Jd4EfZc zNrz;S_AbCm>_X-=2$#4t?Ccxo?TIIzu=8)e5hRq4`RJ{XIsQ11)Y2vupxMv+PiK@&r&~ zyi6~E{)ivhN3i6+Q#d&(mgHPIovL^~48d=f9W6I%n~+l5?EF-poxO0=u3fs!H3T=g zymGxg^w7uc(7xl;%}y3XQCg^P$~{Vd3C%61D;OFYm$~?IET;_2UVeGjzW$x>*qPT~ z2N_(l$*ZQ&V7(}>snf7mhYI)X*M&TZ zq?17P${TOnU;p4A?DaP;z!s_91t=;<6`>5BXb8&jlbR!(UX@^_N79_p3ZoWXsa9FM zY8(3|?7-HN9oyM$_wO6Bt;5`X!FpCVQcCK3Ut3jQp^kv;y^TVSk4VW%Lp}jYLs$JazJA39H;mZBAAi^$IDQmBZ8X47Lk=&j zce@~<*m`6tGqJ(x!GwI8A;<1{!IFmh_R57Z`-@K;0s9Y1ezX zL!7z0UE$v57f-!xKl%H|Z0_x!+Wz55JGrf54;kl6~Xbe`n9Wdd6l{ z4{F>H+(6k7=Fe(%@ESgJGvqsbaIgLL=Rafnc5Y=3I>At@&MDm?#smO_n95xoQJt&) z^-7+<{Klkx<6D1iubg>YtyJVx=H`$|~`;2|;!4tM;ebq)uP?e&A ziSm%y3dvA>u^|H#8BX(klY-1(m)|SruGqKf8Q*yAYyeWEN33-Ixnv?RrdHJt9^7xg z`_(Vmk=@%+h>$mFWT0X=Q)6~nLv2&zx-)5Rb+c2JKLiTM971-2`xpY~pyf-iylT(A z_@bTq`46mn>ScR)PuWgxuh`zry$p-cesND)jbkmkbrrVw@dRnIROuv-$$z)#gNHoy znK{L8_ltkjUec4osfQGh^h%=Y$vSL0lv#K)-Zf-1y}Rv|v0i)PnK$gs*Dr7j)s*en zzQ?}!#eZSPj(?1kvRl!BF>eV;wEUHu_%6P2cH2$4o#Q5}zy8rv_B6b>baTRHxB^ZE z(F9%+S)+pzW|g^gFGIeghxgl;zxX-ZwQYlqU%kprx9k$%w3#hIUVQ!rQy>ZhGW2N`$i>8m_lp5Rq=tDsx#-cliQ) z1K+Ws+7(;ZHO0`cW+(TrvjdyDZ3C+I9(bxd-u<3;b=s7VYb702N|sOGe&m4STS`5x zLDh-Z9#9z!BA`fAH?nGsU1h%0VD2zIe$965*=b+<7hkcDJ+PA@QRv-NDLOtSFo_+V zp2QL{miB8`NuPT!8CK9UzW$Cq{_Stt8)sjGzZ$p0V8V#W^a0*O*3lkyp<3U77VGGd z1NP`|{if~PgsPYH0-%_~QEmF^k*Whqpi;|(S+sPoP(J0P!J4F$hJQ7VV!w0k7GwIS z?D>~ow6|V;(nibY?Gwj_?fxC?AwxK60xlJ^05!x!T!30&{Hut7wpPjneYybG^n|!@9R32n?g^Ky{GpFsT({I?RbC>MK1X@q> zlv-Xul9o|hg}ULxF!%T!z3+g1@zGBKs8Qx{6R5B2wrSluBvfm&R}u9xYqjfGo_*nKbrb|we15bnaH2w;~L^=md0P|E!pX_ z=NSTg(_RNq6*OyFqi`)0J(uQjOMq#1VghBvCVS+OPuPRUjsGlLFPT7@F-C#7Y^3Qb^= zIYaZ9U=B6id%#Xz>2p9`eCrBj?XrUhPS{tz^gDL!zyW5Cp?+c#DH}JHtzP-*RsG4F zU?TPM*>~(`ubj6hpMT9RTwy7JhSZ?$OUxS?95aO0A;E6jx^bNyJG9R}@v)EE-ks}_ zhN0o<@S*OF*D^?z2Hk~5S81z!*U0s$m*3&E_-T9b<+FD6+BhDpw|NaJ6bx1+IiSQa z3hWX}Z85z$rs3M|Q3vYlS~Psg#xi#VmX=s=+84^ zq{@2$03X6hL_t*gPMd8P`QZc&QFWp*@n5SW+Lq{=K4nNpr)@`46LpqQs!0{yYZSxB z>@FXeyh_BE?ZmM|_Ah_;^L7~V6pQbbAt{l1IzV?+b+5JcMb6i?`<(r_^ z?L5*n>V6OvzJbO=WpD&ZG>Z_uvCcGH7NBOBOrLrEHT&T|{)4@A<}@0!w`|AglzsBy zEq32dt|{g~oFtR(O%Ra0R4)6;NEJ>g$qAFkD?gT5pYfHKg6O;WWNy(v04jjVC6tn; zg9}nh({1KZ<;^x;K|%qjC!RWQ*Dl|(-mVclbm&3*(&xTlhqnz9O6nNZJ(>d74kX@bb&{|33D0d;aB@hLpMq zNSyDmsK}>kPFZ1Mc$sC&S&!RgiQls7$-B#*0g4ar4-_V3g~_j)(2ZQJZ?H@ATkX{4 z0sHxjS8R%#dq>wp@8JjR)1N^5$(?xOjeg3es<}_@@dl9*p-zIAe{@|)>p8oM8vd!% zSM9q$dLHG$O}}Ehq265@Gc;!v7Du;jSZ^nfAGYI%_S=EIyKEclJbmy~RXjDl;A?yu z9aYfa)HrP`-plVz?zR8!_y38xDoZ5SZ~2W{4b7pL^Y|P}19&wSFR=J2{(2=wQ;aAu zFjuzBD#_&Okd~`2UI9~Y{0Qt%2^L)9#CS>6z%c)B!FVfT1L(K3PAe*kWgnd;tAO48#SjYVZgz~Yi3hRXqfRf#Tq#9LbBET7pl%kXH z-itAg#JQxU0QKz4FWUe8=YM7|o<42;^ioH5mhICY-DdZpl<2Fnt`Wf`l)ARdD>=-c ze6JhLAF&z~9NH=_s-pxE0$89BDj40O+qZT85oKkR_g8q_G7(q4F~I+^BIRpwbV z*OlJldr6DK;x~S;j?LP){`v>@62rZTDX!^u((*$-b;}f7{7pO;s6@1zN(FoIX5Vaa z{Cj_Gr(a>c5s9H46nd#aUNw5XFhG2jB!b6!6QBbBbIc^P9_MkS-x=sfc$z>`$4q5} zia`pO>NiRL2FE)lxhHUfE2D4PQ0a#49xd64{iC*%?MlPCOok&MHG1a^P$EfnBrr)z z@gkv$fD$>ps!6X+$cG_yRHoVlpi%Aaoty2E-}nfO+{HRWFI!v6NGMduG;E1Nmk2a& zH)9D^hA@^5$@{mm;$c+a_W?Sf0L*D5)Hfdg7DK1e7;;1>jZ7jtmf(79m7idzWaW)AZoiCvMqu4E4VI zSAS&}-g(Ei(pem5O#N#IhWJSrhP#ZpwFBEFQv{TNe*t}WcQ zbBL`f$9J%)vY-3p=$#wP8+-*t8&v6Kq~BrvpCSsHC_||18pKri_TCgqQiq8(uvV7n zE0E~bua9VCD~*`gFMmyR>j0d1ZfF)zJGv7BpVAoEJS=_bO$mpJ7 ze#4$&d)e0?|E|4(y1aW}gwY6lE#n+2wt8Yht4pZRX}L&%zhN$w8e3~I^wk6u4PA33 zfl9A1HpibBE z4ffXhFj`H^nWdJfx%rBKQemX>L`$XJ@Jm-d{#c+<@qMcbRK)n*$ZHgftS3+I3#m@ImF|TgxI^?;GRKJL41f^vkF0d*A<|y?y?&^$d=( zM4}xi>NJR`2I{8hhy`{aiN4zbDh{ocu_TnF6P=JtDY?0bQ5G6W(CM)SzsA|BCA60_ zY$~qLviJ+n1}NOdp*cISbHP4xj1$2dX04|_$tL3^cB*r4U`)zsmE?ow<$%IDbdMrN zuZDUw9$x+OLN81{e&9a)-7kLG?%%(`8S2~hkwEIz{1NTEsybB?sd)XkU+#}rAm1F- zVTZXUr2$ZqOpiVGO*;dissKs~5vfKtZ{A`jjvu$<$Bv;4+3mwjjnQe58IMFl7$iYA z%o6hjCVy$z+Q8^?M7_QmGkvK5CBc8@+(rAr4}Zj($Z2-F-()gPb4BaOB|FBQAb#nh z?vE?+OCn{I6{Rly*eO?6K|>b}UuHkY0rqoD;Mb3>x9x1e6qwX@)lT(LzdnD^d7A}j z@xvw@%b2$f_V%@^z3|H0cJbO6l4jAyXJ-A!85)f~6hYhhwf7Aahviux$5RT8g`s^G z#la+Eewsooa9zRvJ$vm-U-$w$7Y;F(;paaYig(j^H0%q2%H(FK9IqiK&+E&pU6l!a zx|r+jo0InJ={N1?r(U-gSTh>Gex0?aeinAK@vY$Q6ZuYAa)`f&BBcTGI0D-_HX2Cq4o2o->2dLOt z1?u@ruuv#5UEea57N#1d$_hZkJ(K#xjQJc=B!ljI*o(YsEw-};KP+T-;{5EiH zmWepO2fV&A$&QM;eeR>{(SEV|qnd&7-lDK-d^!iQI&UMBfwWE6hj@3`YZqBZdgd*A zlVRsH+j^#^r|s4l`w8&xAKA~&kS(MmK&eh7ZS9lM|pm_Q3r#IELWVrCtdYuMepg$y-`VA9?Zl`i*(}yCcbT-8P|8>YVfq#m zF2(3#%sj|%|7>K~b^pF&_Ss+mH2V4>2b9u-CQ;^G)V?S6MCz`w+K|TpoSWFvYc(i+P-cIhAvPbUgv0Yo#JBug0%2tN%KtKqJ`fK zJ@<{7Df{`$&)egVf7@O=^CoF=lk2uU^xPjoDRJ120Nza;Z%Kgip|=u?m9I5E2=ylN z3hMH&|JC0zgnXH^P?HSjp{B+!wr~{1wR};UIIy76!KC4tB#R_cPEv*Pkaqq_V2W+n zi>&hUtr9DK_R3wuHSKJd9S~`VZSgGusDXiVY&0HYDPopAe?7LB-@6)~ykqH6xge3q~qc2_0<q>f`r6%uO@BVPYm`sTWoh?0G<` zY0Eo$WvS!jkK85A#wQ}6IR5%7L%qkp5r%wSNT`h)Hrj#x2bmjiROHZowv`J4dQpN@ zm8Tji|G`U^j}X?#hI;~(B$OHomwv$1rgYMNblv*KP7U^{J@?|P_Wi&82RrxH1?F;W z>EvE_?RV8vrjfZ6=KA2WBon{NS7JC&LV9y55=740hVFhlzOBa|+%aSKvlzU6l)g)c zg4B{F?)aZ1)EsX7Vk%SLs*UqQbz?I-8FCKUPo95=pAzC+5PVo5^Evk9EiG^?2I*8O zuSZ(Z3l;z+8QaUvK3hiz?a0C1c4+S&&Zq6P&Fe#Gi`e7Fz!@d{oKmX-l+RM+KW@KQtd-nnR!e_r=A7L22W77uajDQw?2}}+s^`;bE zyib4Zh!XJx9piyj0E7zB4t8E!DQYfp}ZFq)jR^yy#N%%_*Dc&KzYkiUTUeR z?ppwL17+B2*5C7{?c1~HJ7x}3uYI>J+t4gOb(#SR?gk0k$C}iaXGB z5876K^6`Nq_u0t<%whH(@q5s;JA$^R7U#@U<;+2whEbAGu{}DFl&aqJq;FN&1PywD zc|$h<(%k8r8uB5j1gIU`wxfu6h)u9ZZU5fAC<C7 zO2a)@3dfls);r#Xx;!Qq$32pe(ox} zp3mBkpLoW`uHLY|k&UcV^zv)zoEVl;MpUYE0Z>+DmjGJ6QdxW20MyjDEpR?;7bkhY z`0EGl!R`D+OZ^hRn$8@D%NI*hg*l*T#sF&8K_v;r>IcJwYm=jPeryAPV$J8&bsL+W zvaUhj*3JP-a-OHbapJhas|j|AJ1v|RBvqLqXBAKjR7Vc%u;2OZ&$8Ife2Y$`m$_3v zodVqtQUFyyI{9#nA#hEg((82+D@JWk?Kw%O*lfjCd*#YG`v&#$^iO|oS1(<&J$nw> zfAxp|)*eRdss&x$CX@k6ZC#N`S2_XBtEJ()Ik#lbP_G*Doqpp@>IRL4BvU&;i4boj z?Icv9%+ccXHUB76l~JZ%`6wX?2tGtudP4^IL8P~!f`npSs$6;%-?L0RP)-~fv}2pf zbeMCtg+=Y&s2Ga6cn$fA5=t5C!PI{Z`6Qt@(YR~NI(y`!4ma6 z6=*i*=o~8#QcA_nKMJBJxvLlg-3-4Uw1{e4mH!IoZ@&FFfI5qW;?9JF4EG-W%%cG6 zC7JlDW)PbisXzyWqG?+f5CJ>r>Zk;l?{$1k!I` z`NUxWWwvhdl5HeRSrSS#Xvn7r0wtjYsu|u%ROXj&t+zKXud~x{4%rK5$NBL))UzY} z8YCLDMY{GyA}sNvc=bhQ_%t$IM5B&Q^S271l62BM>crvQ_AfaXcJ$yr>P#0rO0_Gk z0395ig6>wVu|ZY4_gzXsQl!fHr$72uJNE5Pe5jsOKYCxkdftBUcR#e}o_WE}y?KF= z!*2V--~U7V$cYmGigQH7YXMYBT*^)WH^Cy*TUW1hzUqwq;Kxs*gt^ElMp6AXq| zv3?6C_V8Et=0Y+w0TB+BNX0jDf&HxAzE#Xh7xJBj*3%6|Vjt%zt zM}E^DJamHnbL`oINs#PxyBa=5Q-3Z%$z-78#in_CiaNkXd-4jxa4;y_dBzq3lq&r+ zd+5IN&2QWJ^XF~-`t_)PkJ_gmK`qP93>~%B=yFjLS>ov|j->tqB5{#3Np+n|0#M4i z%22DDyDHS6=ja%gkX+JQ1u!)Mb(NnseGcjL^69tGES|IRae91IxY7X5F&VETol9sQ z^h3@1X-f@3!b`JkAid|VFm`*}_HXI6Fa5?rPWEyY@WNFah66!u94l5-2~ZSZ4rRd% z&bPQL@WPFCC?p2#6hpqV=jXWwfL~l^zhb$EbsaYLF3qEDV-iwl&yEip1t_1}>iC2% zdtCIz_2Y;3*jK;s8BR~{#+^k-^3iEoJ=1giXgKQN9yF#EhS6$QdNp!ZqfR0Te99Ub=WrBnBuwKC- zEh2?S+e)+|&gORq(=@Akmo8s|A!SsLTiAW3VOPiyOndS5r45v`^v*A#{rT>>UUT*jMUGO3NvgA^X*Q`_2$ia8)qIfH8~C=xFwWh0>fy$Mn>0p z4wKwADp2|5ENJaeHuGu@W%d>a3jg81JY}cOoa2U{DVx=JmELlR`HHR*t0SGY5wy%2 zl+VpXu|zUFDsi?38N)qq*X+>tZhQ2h9d>LpKe@7a4Gj?L4h;#^12D+0nuU`mEon^U^cHuvBN-`uYZZ$G%TNNy|qpVtyV_2~@btOFV~jcA2Zm zPMvziF1>SwdYZHKqZ{qP6A!STb0f#ysSg6Xgu)cC)bA)=ZSbhDo96P31$*kHQ}*K* zUIb9j+7;?6tUu{@$N-a0mpY)-uHbJ@LJ6HNsT`#VRLVdU=AKL%DPz)z-qchKq&WHb za*kla^gSL?DIq{_Tyn6Hg+YyaS*TO>W|$ zymNpuNvQb=h7#NDUw!qf_7PU#1t#faGC(DIanq4@opItmAa5Mn!gJvQL~$R*VRl*2 zRH&ekDFY&}bQMXBdx~G5f9|;#m_t40Ct$UO=Anlk;)uW?y{*!ua*<9#YB#kA#ysAc z)908d->@EbGk5drd0LPi0Z{9Zq$7?dnN;~4MPTQ|Ko`CB-~{JrD)w<0ecx!AITVV8d9E2# zf|OCeFU#s7hZz|>0^U8J_eo96`;m!t;#N(da_b{(jCz zG3?XBfPnQjt)sw8ilck=l7v!wjtW$GMLp8}j7{9MH{UwXeHFR{lwWioUT1rE?Y0d= zfRscs>6Gl4O4|WNU7f!?Y5(}MrvTJT+*9$I-Q*6yt^w{6L_6v?-|7ZjG-cV9-t7S8 zZ8Hyun28D$pD^STm^?5hVZ8HG02+=uCDH`_TB=Z0YMNXK?0IUfs`mW@`sX!XU^Hz`w6r=qc5>%w5ypFXT%XD8j%tS zI)E@F(Eu7id+%L!SC_9!zrX*Dh|H{J1E5xVp%;3(pNx3I$Bp}sd&5@d;FtbO%UTwm zWM=fqkN&r+R?JmxxYz8^?n!&@Q=hT}TeljYR%)6@&16Gx#cGnq66Ol;e4}Nvi_11M zJ7f2z?^nH3w8V9_r7PlGf#eGl4)8dyTH3>*_>)dAzHookuz_$ zHD@COcWm3lvhCf}v_qSFZO357#-T!{x@gVCibu&5sAV>16^ONxC2uOUM4jepWjlSn zY_Ggsw{thfsQ(&kWO9}%6s?ubTeH!$rU=s#0aO#p)2ey9WSi8YwPNeX2JFzj?RH?t zr0v|Y!Pc)CvB7f5%H%~Xf94ivJ*~d-fEA#t+~*fscJt;mb!j@y%H@n_)1*E9B|n6w$(#Al!Z6bTDDB%y0vOottU5S>-uNy@TQi1 z`tYb7oG|On)Rd^T#Fr{s8&Ipu6*3|(0cxNxV_P>4+oQWT*~dTl0o%5AofTke0ZQee z{X>~Bpph-(R-**=km%j8R+rMSfD)?1ltS0%2mI72Brlg(h(v{oxkKg!)Q+kv8v&7z5^6hE0~5n1FYsma$Xkui1B=f62}Pta>94m;@*q8mOx=v$+L-GdVF5 zX*J7mT0dsL18UvalpWmJ0#Fs(HdeREo~EtKR4rE{r{yXUXy82Q0uTXt1)^1RS;}EH z1E5Y_@3WVVR_(&AHTX|Bpd3ssz)hXS@L~}?up0G`fTDxQG%L1gV#J>K-~rpebCXSu zkJ{Sd0UPY=vA!5b@s=tJbQDc1_4Glv0I0ju4LkPENdVily?giAnlV63+1h|2OJP}n zs_{F&yhO)1ZOcn^m(;1Rw`^-hMr{BZt5eJoHUUW((6{LB>Cs*%&)%^A^{sE&TNf@` zl@4Q>yjE%rYtV6J^WA{TLR(&eibp^aI{#&WBEF}l5($_}*d3Gs7V?v$&@Vt)2T)eO zX|3vYE9LIlxC5$b&pbK;pjve3YF~BoPzhoIsK4OL^KcK(#WTavGpN}3SlK>sV5@y# z?-o0>W1Fq*8-P1oRs=jHD47u&1S+B|!}%5L&3wl0EVu0Bxzjd1ea{MooUK_qwi-}0 zWPx5=y_;SYV}*j9p$B{Z``-r)CHtNK@H@77c*y8XtpWh@From}0(kXW&1%((T{v^z zUZCe2Ece;(|L*VF`iW6zgchPKNBZee`jRn=Fz9S;1qP|vLZxA+0MZYB^b@;y;idzs z2~nvX1WtgO2T*f#97rl?(rOIy4!J1_wR!zr`{)NswtLf}jpgr~U@=b5RjNbmI-I4R z4=~jrQUyk=kVZ9^Lt+Y0eRkqnpZ)laWxIH1jWr9CFkk`Z&%kIfcLEe7R&!+3I}1=q zr>p?QyKU22`}9+f1M1B-I#{w1q|-oek4x`V_d0PJ^hR0gB$|kYu3cZUAO7G)@@m*q zPd#OuHV=9xs{utPMpyxAxmvXaKvl2OD^s=tpcp9iSudtE4FSk`KY~eiJ@l&vneD9; zXY6mj{!Kf6<*F4%N32#X*y1u;19i<6N-m|u&<-Y~REv&Hj{_+J${U~72@*;rOHatG zMY)LK|5QfjDj1&Bf;|RTJ1U&NYEvh!JRg)$bRPBF)~ehjKCr?QYM_9@W=9t_tY)^mWFo4=@+t#hM0mO5Cu4Z|9kbxc;95IoJh**G2 zs?Z@@%-HqCrv2>ABX)m!3h6Xx8#YYX?wvbqkRG9?TxehcN{WdZSXHh8l3TZK*|8JH zkx)JM#1kL2(cY3XpU5X^rf^FuD>gHODtTwhu3x@l=T4oqwe*(1_POU!L&gX}&ngB& zy$Y$jlS>7Ga|Y>gA9eKpe8o{mYhq#fL~1t6j3 zmc-~qD}^e@Ujd^DNL7-YtI+4p&)Qf2_U|1~pa1-?+3wwIoClkt8SN>0H{b_Js5)gBC6vuFjz8w0()mhUB;c!sjiEkbx2BM-=~EHvy#KYqc- zPZJa4wt3S=+lho5q!H9e(%+<}1#5%yG(GPspjup!%9gdkfdSN%5+y_})JwT)M7=XN zYgYl%`LpM2e)_)k~;L%AgGJAjhDp3N`OgD%tPOO%g}pdMAeMj1d&+pcZ*?N^>2 zvt1i!7?@nPzNIA_q1RtSUKz%5QukV@Z$UaiJL+F$+^~o!xVKWalUIxO^3fH$bQ_@N z){}oe0tz*R#`1=tJ}#XEs0?%y&;_dck^|~9&wLC(ZFWEn^c1LX0YIq{S3flxSz`s! z@|{z6?JxiGuaFQ8`=dYlLpykILjWAAv{Y(6HS%{Ll9t#}kapH^Uwy%vcqA21pc0VM ztJ2R`ns)2{f*n16%D#yvarXLk%ML)pQjawmFEj;3F_8Dl-4aSIMt-V035r!X9MazrKNRfiLJhTi~V*s4OMT(#X=2B+i?tR(ez*`c(^Do@0CkvzI7{%14>!mV$&5pw3P$+Bd%aEtgPx_Uy79JGa}8ty^pmQSVX^ z0C_a>ZP4?HSw({!3|L?gFA`J$MHXQ7KzIp3Jrvz$SC;JTnX~qjmtL|8lIFnfJ+@=> zR@<;{!iEO>UBy)Autc~_F>eC{NE70rgh^Yqc~ZY|ZyI&)ik&!h$=*J8&Zh3n;R>LT zOp;KN6e4(*fk7T5=XlZFpcB0`Ky5XV(%C87y=~EczX_d15;CG( zFhvAa9_}sVThylxpcZWh66#lh)%qlY(p8y2Z&YEl7EQc83Xe8h5kS3USUs0ik2AD#k~cph!e?krU8 z%=KIL#>q4G;?G{QYqxG&1~6rcywF2HQYR9aNYaVCcqL(^!iBLD!MqwuSh-`iUwy|? z$=M&i-L6s#Q0flE*K${0;ce%ui2yPXRZ}_itw{-~do?=B-u#Sh8m-vjEoM(07_@yz zsKML{k~#v4jwy5SKmJ84E0JAgX%PZEX!m?CEU;^Mz;Zk=+~7%cT(E zgB&kWuzaFCVO4>7xK)g!*@B~IZ`yzV^8ahIv-j-Dk3DIJ4(+$Cn>X1o>WJ!^fm>7s zagD1HG>oKDmT^)Rl}Jw{fmdI4G9l3SOF=5hjRGq#r6;pRC%JMBKwW^Yv$lhYh{Id5_TgP++cn;> zq5KM|kgiK8I=BERB!U1%>s1$)0h9*4h<#AY*wzjon6%&g{L^+|+qg$AkXydapj5qw z%8(w2xqv>ljH#KvBQ)4w{q=uinsv^e{iRRaqmLf8Et4B}T85dT-~PgH*`xdRqxuz4R~VO(fL=vz zF+tLVxkXwOI8s3J@%7Gy%l7x*__iH8evz?TmhqD&BA68fLe#p)tFm|845CsTa|KThVnKC zt*7dJcIIvi$uw_QuVigzdf1j1H_)*FMVKq}(qc4dfOM<7)&Si}4*?ILkRnK_64GRM9O9o5MR@R%}izHn1HD_1=-^{ND2PjXgV)jtEHV`ebL z5MfH3Hg3_$qSm=q;$tQ%F4Q7%G3$Mg>5TULRAe>zLs?1fYKTnGeA@BLJ#qMTpQtr6tzeRHob- zt5)EEInthDZRoXk-hyHOj`g8s`}8wU+u_3pZPUi}HayT9XM{*VpyFRny*W%M8no#V z$l*d5K5^z^h-lOF%I9dDw~rpRpT6`803EPD{13lpkL*3*>C%QS}FwBMeqh_1|P<1Migtao-(k5!`9>zyn(kcR^fxZIM)@9qfZG%1e!9%uV za?JYa7X_#u?~&DYNFAY*(|U#`fQtQgd3N4@bmT2N0-(;Ezi9pa1NN~`e8Tqc-)9qR z25cM>j6ov-stV<#^5we#rAp!D?Ud~Lb`Ze>@{nEpp&8$!H}2Wje*BUhJ%7Pw=jald z*%O#DE^>lo@--lrQtAPNbZXDo!55xk`02`7oD!ZeYll5KsVCAS=_-JT9stTg>p()K z$8>NZv0550sonX_WZ@8jP$s%Wrh3&{%NK2YXxSdwIba7j_1La;X6yTwt+%zv=J`nl)As@>2O&s2X8Wbl3KcNT^Nr(GTymO>4D)SEaEu;Eny2O2CV1 z&MXj01-vChLTWJf z>Q^L}W~oQxpDS~B?Zp>fu(#iO%dTCzY+EKa+VB1L@7g1K_MvjnG=x=s3IHl2K+(EY z3fO?CQdqSXv8`n|8a-&6?W&!i! zY~GIFY}h*|=j_gn91;?BueK3Du@DVIF$uQRFWlB7c^wFa3*ihMa zvzV*(r$f89&_2DWdPpv!sr0Vg(rea`ZcADZqth&vhwSXVS^N9%{?J}Oe%9{I&N04Z z#_Ewnws+q?+p~SrCRyMep_Za~4Qi>QEHH@@Tby(wf4eRGsCfh;HJ;4$EkGT)e8>Lg z`S07?0II@-Qv;161GrVU^ahnLehX0EvuX@Q8@?-`i0<`NZsaLLWe7hQsXzi#cmts5 zG1`l_v7I$t?|Kk=3`-uFtmv(!{~60Ru3D~o#U@!}{K$c|wtuqECWjj=kt|rgI**1# z2f-XbE&<9pSa}1e7SwA|;TApW1OxoX>G>WzIB9!!uCeg}7y>1rL@(Zx$V)tdsml5j z27~a7-JC%`KYrSd9Dm2&dgmy;YL0b|kJ^F#dzsQ62T)~ZOpr>dx>}1gB%%24Pb`WT ziz0bAHLk`#vjjeV{)GMGw*bPaQ(o4++jrR)p8GtrTzh~3Wh9{jYGY2wB2BbYW~-P* zBrYpcK6O4rFR5wVzx(>Pk#-QQIDq!SVyz}498?njl2-Iwn7KTv9+L?HuoS zW~NYivXe05MgWLhlVoy0VZuHD6cQ9E(vrv3HTzGX*GU4Sm^cwi=V!=_0VclX;9 zk3VAD+35i_&_tj))AC~OqU!Fg^Y9;ndOJF($U1>afcokAoAy^smmfcO5#dW+1vi&Y zs$T$20+yRyfC}ykuN?HIM_jV{>G;Yoz5q#fpA}T0NZZxCxD!HSsa4VuF3Dt8C>|im z@Y;PVHd)kd-LUNwW*>iSlkMNsZ)*cyPK z9<9`D%jPZi?593vA2>8=8^&`sRFuM0bwDMJTGRARm}Uv7r?M7l)GJd>`@yR}wKo9N zB_<9A2m9=!AAQ{R@7rw~C)PTkO6X-GtxrSKlVl;7UeQdX5E%p}y~3H8M2oW5m!|EF zH(s~zf9Jb)gX!0i!C@F`zkT*o&!GD3WKARhO1mhe`Y9~|Dy^qTh%W-J>G>slm@sRrv;#DcJj=u&EGFFDYTjNH_*ZK zd*z)EoEMO=RG`Wvf&kq&h_pm*ROat9!Lz|W|Eo+E?O$udOaf{A6r__%P0a-c0gC*W zP#IPCOi$U4GPC*Rum2+qewi^|$x3~FHZ(MBdk^fl&-~KUwr2!j5?1oL#VnUnD$1ldp73m zlONoMIYh^ardFF{YZ+U*nDLd$m(}p)pa6xG(9E8Qk=LvXDu(pjo!fWXZ~WS?*kgwV zY>4egedwYEGNcK-`Yh!zKQH}UJwJ@E24#$K?s~(%`TX~gQpfD>{W~@`Is~9TWP6!9 zpIqMols6bmqqP>1RH9uSS0st2DXFJNtbG@l9X;`;z4hjscI>UUZE11QHcYVgv}eD4 z;v-Mmj*Xisp8!SkkRXPqrKww$uTDM{(wKv}re+uH?UQHOk@2Ekym*tqjGy#UH=hM^ zKncbghmjuYng*(^<{g+GK-Es!hOHU<cvA_vj)-5%GjXgF62Ca5_52~( z05-ZB&FpD1Lfgw+tp*56%lS~M9ug|Uatm)BK$UYhZMb-k35kMz@&ns!*ZL6~KrQU4 zu`apHe$5(@XiJ!c7)UIE@2M=pfLSrWZ%2_j*G=7#bOF`4$5m1@dIXa(qBYGpv8FTJFc#zrB&)K6p28^i~ zP0`TabhM;M?fDm;WX4ZI(N2+Xy)%m*a+_ie>XkF+>?_az(9T}D?h-LfAaqh$4Jh2D z>X2KIT(YNEeu}3YAACCL2~0^MrA1*$3$14b=_ge|VA3KoysIg18p`PnhvYKa2)&HC zOb30}#`>mh%bE&%HH-EEc9U!x8AKy#vJQ(xVe4B9Fts!b3$2k*|E1+B9RC@u`Z~oe|Fsl{`wJxCvi3TQsqT6qx(zukOoK+DNgcSQT_STsd`>$XA@Al@2 zV=gK-Z70I1t=#!aU^l|2<%Er;AE(0S7v6}67{wnJ9QQban0^d zFTxN4lm@5)P+?L+y(y|_0mfoWPbROPjD3l@Yu49y+qP~pd;GBtwta2E`WpAGuQ`ui zt(iX5Jqq2!s~)h*WCD|`R$&aYkQ=nqOqaj#&P_Xgvu^X%e#S_sdCVm0Rj--xH4S<^ zOl=8k;#SvUETGpSw-$A^)Bf;*-S#`b@k!gaKJ3jCpd_L2W`GQn2vDLR`5Nltebn4@ zfau8S3-;n`Z`+0IQ|ylf^w3^v*cP^h{qnO<+lThEKt5csQHZJ4JJC~*X!x*4E$7Wg zR@z~a!p|>O?HFpF0QKV06Ly^)Ei4E+O;c(CG)g(W#CFgqm2Oa}c7$<1ta`|g|FK-L zbe@blXMC`)-Uq0XDgbFYm{{kd<7zUg(5ND98WqaULM>n_XniTqEFSy!wv1QoaTap- zqVkQi!DEQYoiZVdbd(MxbzVvbkOe?NjizQ^=*kNO)>=+4%(-v(UVH8{PuPBXVwT}} zS6o6B7*uD~z~Xe35d-bU7x5%b-#9g6fBEIVwztomumaP(ThR5Nc;W+5LIpteMG2*z zM1X40n=7F#i?AZ4fT61E(cZh8%|(0j*xUB~?|;wEpFV9>RMPFcc82w*{Rgc-TcSq| z@w_QWE=edQsI;U$YL@Wq&D-{amtJ)Vb%(7-^NY+pHMBe@KxtAUs(oT73YcT;Sm;Gk z0jzBHzLko%Y_R`6okN4g)FIn4!uXOsT?0&^_R(vV$f(3FcE+vHlxcdHs9r1ss3LPl zefIXX75k^x&fA$=I=;ZVQ+9}kAExtUh7W0!VFw0l7j#;vAC$L=zTX7!Dx*4{UZmyP zu|YJ4J@%{5K59GHu{ST)-PJwno>LkQD9Qksw19edcG2EGd(mEJnc!!v5nj2?DFE6~ zOr?%v{iaR!4}H{f>Y()?=)5elPfgF+t8`qioIYk(>49dVfeMd7uhcp24i zmQL}_lb7wQ-$s&LJm>2`n~+crC?wRT4IMzKHx;SKp2gbGB02WHN$V41=n)8}px!$d zPulTQC)js)(ym>;Vl#K|0Vq_SKmMY9VB0=~V^AR#*P9^Fu-)0=jC^Hbn6!?=RL{Tg zlAStx!8dJcM!BJ58xAP-$!Vq!MvN+-wo>INa6S*Qi+8NtbJxa4mu&ln9-CybcD%P? zBe@FFiAfvOGk>d;gz4A@>v>t$dX`ykxXmd2^(%AsPd`0p=WlUJE5{mFaTLiu4$o^1 zO4@-6#2!Qdp*C@V(}@XC+8Q>{-(%ahtOroL?c-1Ew5@BhLCvG&Qlo=}0#KSU;?X&y z7J#~ey7!YekJ}Mu_l{qc^rL{4wg*saCMN8$!~5*9eVgsjt_`+jxXf5V%=+Gs zcw6H%F?pJAP@L;Gr|ic^-mssXxxjdF%I>p4Hc6-sAX4fwyWUl5pSlatQY{<82g$~# z9)PBfx&81#vw2W~C@al^9VXgBrs!eOJeVMgfT($_IW;SDwlsYm+TXV_YaZ)Z;@G*R z$IrR#+my8pgJ>Uw*C`9_V=XYY)iM_iWK{_zEUN&e$q0#y(a|yX@9nmO`w!TM9vQWb zqhi(|q4Ma|0#t!1D;=tGgi=T`>wP9DuH9Z?(0h_uzL)LdwX4veWt%3)8ACnh5^D3q z0HrE1Ev$m&dl+Doo<=bOlcdw7ncMc$H;&lZ3+L?i)E%3?d)Mwy-L+kN_S%>JTJY5PJLOioI~`n%$hEm+N28_>7$xJ;OfB=skb~NfL^|F=b|{7KZ5qP({X0tUvi; z`z{vTciJQS#%y9ZoGH`TNq~ZP02R_rz!U>9_G$pA3vB-V{>wk5!@Fpgr(ofQ9^Y9g`20U6jQ30?<%2$6y=e^>f`4DrKAKh2NHl1sHFKy71V;c=6o3P zU}vZUm`Xjkuguu|)Fo8y`)pxraV)2w;RI{38*8?4kY$ll6+l5_1WAqx=QOS*E!Dq9 zed)pipacx5d`-qtnm%8@VbY%b=#%yc4Yp%z&c^7p#Tt@On&m4r8HA(qxmY4K)lFlQ zoA;V_j2WbN&Yt%7=DjJTPo1&OT6_HQM*-9hB-A=v!+y9vm^4W!HAaQmC#vKsq5vg1 zpP-4UcD2<`y zzW}peW9!QsCr;V-kWiPe-(c1WrG_3{iYAg9Q zHaxi(Kn-#(22W{-CZ@f;9vK$YwMIrqR;yH4dKj@MpLmpIh%Fo_%-SeoL@K!$ zpP?0iqQL=_t9@`4Ek=h|;DFyH``UMYU~e%gF++n@q@Qa{qdbYz!-?v(qy4C{8|~>Q z95OIuKw?pIOt$PH5a_%w}$bp)rXb7~hZ+V@`miM?_8hTWomD@beQr|02- zk`E~=LCg>an)nV1wSjUAGf`IIf& zcGz>zeGc)@=}*=q7&1Gc@&c4MPp=i}ffJwrsgI#9-e}q@tP!0!ch;^;k}S@#v$+yx z`2f^j2h{k_0ZKx5IRdK4m`i{XbIefYw@)AAn89&7cJhSXymQNsV~F5A>{LE@=#c%+ zZ~eCI8D_ef@FJlAB~xI^(jq!Irn*Zc-(%a$3(3%GiV;}c&DR)bPSK-Hv*vS~f$1~=nz!Qe4Cb7bs7DX0nY~q}Zfc6d zRCy1bRWA$Qoa9|#-RBl3ey=PrAnu>E@f`;kpAFa?69#pk*`dV*D2@#{pwxibe(Nbv zPLA9QP+K>zwO@MnV~lZ`jYAbJ!|`d7N3_c!Dl`+Q*Yk&(E2t+&kDswGfAt?2+g@g) z038X7+1a9`pBS7t=xgj-w!Pceb6n>MJG^VshS-H$B!JjitSs8fYoaG$`A~kvaGstpo(pgzTCC zq?*AMdDUA}4{f=V0CttnXZh&8rA}0q*_*~xw00jqc8FsD)Kxu{rcJf_uAzILQWg*^ zLaBOCC8SA-GZ&W`o4ml-={zT4A!>ESR;tUkj$_#$5uo-Up*BiFm0UupH&t;Yp;lP< z)T4VKG{dLoHh_Fh06l%muH3lFj_|wOgRsDWu?LW@M?xL6Pd@btj^s~Lwg5s!O*4c^ zLM>7G`-=;9hgqg89341z_PqVGe`%)IEtQs=Z=l``RATREoX2I z@znYBLCnxjsz2bMowD+z-BysYY=F0)9X)RU&tHFq4)2PUM^H5f*m20ZkPcz`Y?lV; z)MeMU_4ezZeHwLVn+sq{pr?Sg7P~h&AQVkV4!*;el+q$mTwyn?FSb5qqtTPtT6A^YUq1b1~ zZXz-f;K`AAlS2-P2j&zDnQK> zXXfl1+!=5Zm9IvRr|mv93cZ2F+{Yi+7;2Yoo?Oo!GqK{gm?qk3Qz>Piay@@%`mTV3iuOKmj#&y2||) z7q4FR$%tboPTK4YJrBqE9Z+1%qNDEtP|=jS#uzK8#3V!^3)E+T>G84Vl8rXzZHR@q zGSja9GFwzY5^74;DTIVuf?4mtLsN9l_YevT0BR0^YP{Jh3|jx14V0k_4b(8i0}MLi z?h^{MnAS!L+H%BEb1kjv$p4;Q+qgU9b8O0G-wJeRC4(`YIzH4EuRr63w#;DTEsa0J zrsvlI)ZhHwH|#tMy5-UJOoRZag5biul<%>tSZ!qy3AoNa^X$j%Lx*c?$6GaK4RCSltPqSUu^3tsgNs)mkf1A2Ss*j@Vym#e(P4v8AQV~ZBhT@sr%Z)BG5 z!@ieq3-+-=t;B~$62q&`Y4fY@S&$@EKBq5Tw6FgCSM4M{oy3l&C9Cv+qho7q-@XHE zwmszUI=0S;%d9W4^MJ9Dw)f~j zp94XRO=?g?l|zK0f&rQwVA5{#wcM^WfqJ|i`xw*Y15|zh#_xyV8ovMoUdb{xt}u^$ znbWgZIb?KmplAy$M%U;Dx@&H61$B9vgOq3O$FII-S8v|qoE9o1 z62mWpp^bwaV?{(_Ohz&FV7fJa$9!Bu=||tH5~d&X@#+!QzZHjC zlCLYJnBFnr8WW{{I+nxn7urmZ@SH1FI4M9Y)ftk^T|+W0*iIzUj&()bGRCQa5j3Eo zg3lfF7Nrt$CW_$>vqa8k>cGTUt^la;u#Hr{)qsMti-`YW?df9I@uxoZD7WbZKuL9C zXyXH3v0V2Pszja1~A>mcZ?kpy3av-;CfKXH*eWwk8{1(zI}U8`8IHdh!uaB zL}}?5Ns-wJ)`R_fiM*C5ONFuA@sr2x>tFx6ojrfn&ueM6O*^O8uNOc+%K4qcs6U(l zrc#mSo`%Yo$_1}Epyo;U%q^yIUwMW5C(hXYyVET7zywTb!&$x#rc<&GOU=28xk*iH zLRlsp^1MBqjvZ!vuyLVjo7q^ng|f3W;rVNNc_8YT3lMjao=ZT!!PcHr0RAjH!>@4E zVg*2j0kw8iD?iMbU)bYD*4|PAm3AqF#l}24pGz!!lo`|PWg+{spLvd6bj&*+4X|}E z5E)B)G_QO_%=#5%-SKy_TVZVfYt zySTpXfIa@m7C*`%Ix0QURi#e!l__`em?$PX+yYR~fB$>-#*rg-iuI={rdP8_L+fRo z%>kuYdLC|uWB+&yQ0j4c{p_5~2(oksN;mzyWYAJ-pr;+}HHIcD(s`k`CMSx8Iqrv; zgP&PbY~8boftv3i-^I9TGdDj?(D{wh!{?d(V^9Tg>1cdf5Be%;m4OP2?)sK=q|4wo zD{|I4l#4GIwFlK>e0-hl0Z>mpwU6Tf+A13!rIYN@sZC({p?=%x<2UUaU;8=|0EvR5 zbn_e%YQy>oKU}zD$99_-XGa6qiIo^kiwWoy{d`Y94Jox!&n$$yaOtWYXQ#z$^mI3F zT&E5U>S^#Q>oD8dWBAW>Rgg}b7qOV4&Sg-XU=f1-jvO< z9Z2KFny+g$*!r|+D*#FbDlukS%l#jFcki&r9?|aeowkm;sYH6j1In-4lwDeeGKtjssgtuO=G=}R( zbX=iBB4^Prrrib=C6WeJN9|SViUq(?zh$+h%Qn=zWP7*v+pYta_qR# zaD?qdT6R#Uru8d;n9%~CiizP8N=fpqj$V62S7aBUgm!IM)HeQ!jT>zjYfz6qx`pEb zVZl&oNaU;I^vcU2zB1}e!Lmot&TwGud*6B9E?>HU8Uw?Uk^0MtiFNh}r**e&+iIi3 zBYx*YnT8Xfq)~|sMJj=|>7csHjY?{aQ_SpLzH-T?@84(kh#5#!6TLgx!SNfYJO|i% zBUMNBT4qc_5PwuA)mh+@(9tgM27rB=-uY`^{ad?n{-TwU#Km6JEz~=eL4cB!b@f4& z)PKsaN#|^njYGTz-U0SFu46)B!$QU;xv6pus$v=X_47KQV1POD26MJR0dCTvoM(sm z<-uONGlEo*sE-S$l!8=-IPpZJyc06=tdY{y~;L>h#{4h^h)u)VtK*XQOLJ zZPTWWEbQ+_vp8%USvXFBRUXQ-`<0)@)^gLS;Imim*muACUGDsQ$8O!dZ_A8BbIhh@ zdxDC&Dt17Vrx?aTDtA}vsz4h=0uKNj=Q+SFSXJ1IqGby!SQXg0u^2>JT=1o*R zW{(I|L#JEE3jBwkc-*#c+sbXDWp-k)0uM9f0iCvfH1q@NH3<@KBjjk zH&56nKFQd2$4(?Xi|a^O?Yq?eK0i;U_)1er2CLi8oxf_|eg2>9;)Qc~FcM?duTHtO zRN(3^w68#8shqw2v=3*lbkWV0&715nYTuU0bvDGI!(LQ#rz{x+3J24Hi(3_NF0n>< z_8fZ!Uwn}psm`+;qT718(*yp}PII4lQEacPQb~4|oAlj1!B%Q9{thsLx?QGTuVple znN$^gpvJg}r+)*9e3C^=vpD;eIMumBddr^1ZyUV)xhzboJ6@J9GSmEilW|$5xvxh0yiL!($`1|G)uT zx1Jkk0a1?wSSL@lr;38n3uxD`4g~6m_iZK*j&M89O;kNe39Y*89tnk2SvLas$3Be8 zvu+*kNEYJiq8Sy`Y3($prZRDZEHF`%tuU-Xz5WJ7o#tH8AjcM1@FZPryXtOO-<3=+ z<))aLQ7EU9NS^axtPk`9rcvItyhAW1yN=i#OYbiFlhP7xConD1tIZ&F@04>k-Iuqe z0Xh)QODW03936&HdBsS2x|qWA)rFl%dS*)xak6%N-8f~2X3jjJMg8U~b(Nlm=pZ{L z-q){RwIe@$)$ZQD!A1r4DQZm!tsXGbZ%fpZU`-diCl%j~LmX2kM;y>G3V(;UUtr=wDnnn29(hU~E z8@6Z12sA3;(z98d~!@K68G zP%dKXpxGE>C|%*TIKRNobTmWIsvfY&U|8S^fRcW%0duH;7_y2gw1_mBWw5UV`-g`; zof-pP9nRBKcb%A^rxWFw$Oi6S8N4N4wlBoB} zGn>(e+YqXwq+pY@1Qvlx3?aK{u2&5uQs=2pi4H?odCkwxvQDx>yf9-}McvU}wj8~E zniN3C!%j@hpkv6+3ner3!XD6aB^|!!+ zUbPQqlIq|ElnKM9f~v4$Apcfge7Zf9Axr)x#$UQHR1B|MbL*%L>THyT=1hIww9Sx8 zfCpoRnZbF^Pt`S3NhhM&%<2l8Y?*o6xOE5ZUbaP+6?DvAT7ajmu(W#_O`?bVn4W^c zw27FG4LW<_prPoW)MVXMrQ5R>*aUl<&alFGQ@by6xCPpVGEqCqm4G6<+`G*PCZ8CR zA9V{{jh&pu$$9}#u!|RYU7AayP|qe)%Ll9v7G_ybTe9)tCObiK_T4AEpD!iO67IjMqnl2@ne` zf=Uh0UME#X*Kp;KC?PfV)&i4WGIWtl9|5rH^iT@d2Y_Q5mee~b z4hBHO+t=@%g5oM{h4mbPsT#p0wKq%*P(=q5i93kYa3X3TUWRR2c|g?52(-*hD$5r% z0As-Tsz@u4Nw@vz+Aww*v8am#V2s$%+5qkXU34khGiH+9MDnu8h70mAX$}YcH zW~YP7E$)L^6 zfVj@=FhjebdN9S;)nXkYzpL@fAeJ+#&$Wzp=v6fU#c%Nm&(zL zJk#0*h9bV#l+K>6oNLxt(m)U2z~sZO&5V`Cxa)(-hhBk*@lqj=N#ptMALstg+ zno2{`Qjc!V74X7`Kqbcz){7($btc*#1g~V7-SZcFn9jvbodH2Kv)BYo3aYi@v~xzc zwR*Y24<39!__&jPy8TF$vUcht-|jHj{eN(CdL@53sX@FW1W@YWD40McbZhBAv$0+l zOm&2!p?1zll6#&hZ>O)gEtW12GPD)6OKl?phxYT#y$wWbFGu@VsdPV-(Vq~KWiJ;X1LY2 zWNQYrO_?PBpJcQD-I}iz6bvF z&z%dL5lJwap+Y#s`gy9Vus4htO*y1o6i6H>Q;gc>C2RW|i+W|;Qo`1}Y zO`O$JDX+k5T0aa}A&6r}2@tVuC&gG>CEs~GBmW_M%v|}!GKc!b;NrAcvJit`l~3Mo zcKp52slAk*+CYU-K~p{{HKnVnsB?%NgzBb${y*Bg+r+^mK!wb`SixU~(L?O6w(u%N zcQ-sp$2AZ+_yp-nlHzuTAN7?vmTBg(>Q4(X_F+?Lfkp2klMh2od5`ml%qG|_wP6(1 zZlsU3x;%fss?Ny(CMP%qn)i!W7;SlhlqSfcYS-J z`c8}%0OgG$H~I5=!*xXqi6%gWI(SbQG9o=uA^Iy`yI@iBsNNn1KRwQLu{cmU)D?Fx zE8o%7cYCEC5CukeGI}y0SWG!oE2N#u?sdjVrk-f1wC?KP`r-9tgO7VBYZuL8teB3< z9picgITOj#Op%575UWiReNyni_#vYF5{MPtqxcUWo*ss0MV=0*2rAsXZtXnPHmS>3 z+WMBMCs|~6+uf7&kt9WC=(r=k$vWWj42!ahjIRMy0X46m8Ps9MLxT)t$C=un;F?LL z0CE5TE~-gHK~%Ph;XVM>&mXp6=T9F;X;ANsoh~^fn(ux9OyOo&fhq8Uq_lwLj~9iX zSfqAY{hL7E*$`5I(gQi)8&D}>%r;Xg_2}Mq^Kbp=Rd0vsc?7dl5H*x??()~C$5ULr z#PHD zFIQJtL%oQPMgPK+%=jswD(wi^Hc>r4g~8HocNdS)PEnycAHlC3$>WBSDeds-zaC8y zyT3f17csU=Dy{=6Mv2*_HuNB!AACtM{Kur7#EmkQkzN^YDw5hMn4uEb+usW52}d)9 zOYTgTF;MMg9f=FzSm5R_VXVxensGxfOA>1b@@!}=a^j$D>-)Ki0ALLkn9SlpVHx1C z-yg+Rvwd26K&8`e2X;O|DiuUS58>DjlpUugW7o-UO(q@`)WBPy%OLLd8n8 znSywMASt7mG44t@^n$_e8M*2pPhQVFl}q1hxL_(qTWWh;g<4r zbyGp8ZRq&~rUa^v#KO_TxS8zE)VL?p3mbaRjv7niVh*nhc315v3j-qhG5q9Dom2)D zqx~!Dr99oS?d$o)@Ls)WZWGwdJiZar)ut z6?p($#8ACLeTeM|C`_nr=ya4|$lJ%C2~cuwXPW4dB$V1Lqzpklq=$707UOx&$OmicASU<|zS~h`BvXOP`niAI#Fa^#g);<>EOS~Cv6!nP}<)=E( zb}U=yvK?O<5ukjsCVF=fEmo7FDXyMKLDA|#m5*eZvf(lwFmmXN|2t7cJiYiNQ;D}3 z+EWZaG1I`@$|Sz-p1yHTSEFpq{O_@L(uk^bB zN@3hzVdLxZ)j`Dg-muu)AX9(H=pGp$Kc1`Ey0dN z%FOZ?Dx0=`ZHXJMxapR0lBAPVy&(WLz(m6U_vw|HI@kS?ex`+~bBzO2b}EOnwD#s{ z!bxBfN%iuiP9o4m`Y84b1Lqy=urNy|Xe1 zuZX3$>+x0$ZXJ+y`OB(?a;JyHCp`zxDu2SY8z;pfoWcaIiH1_`yn&eotd5%}`@nZw z614%O@LK1PsVC)0>aOmQw*xA=s|>Lmp-{?^xQY*<-0rQSv;xk1N%iRRmXqhr zkJ{YFPXd%)&y4^I1P414v(-s|P%Q(WX^&j9!5SA^sp!+0{?>SgIWoWgm~|jd@a2&l zOg=KtM$8uov5rBlF0Hw$45v=~}QT#`q!NY@Mm$OtoB`vVZo3}~3vWjAvXaBcf zZ7p&WPlKYga$NIUIzc_$atr zJy39LhlxJ^E&uq&@6cO>cxf`gnaqPlc60BLPAn1W=sUb}3)zJ*?(Pg7{5yVpx7|ZA zy*xNOKY7nYo}LVuhKMPk#|`<1bloFF3}HewJV`mUfg8);rdtXZVufFw8s_w`j^MW% zVcSrrdy4s~V5$6=?k}_3t!)oXmck@jbxdWe+JtZ}WxOu2I8K9LS4Y?ZlRy*N#e)z> z>bN#9=vK#m{?I}meyVC`JJ$uQEZ>740Rd`St{`yDvw+Wi;YgkS0X7)OWDn#}nZIPh z`K3`-)5kc&G|H}%eh%lA*gq#kFkkZGu zT^9w9?%lvjRh73tF^r^AlfNa^qHUV~r${w_+yxdGKcquTd(W(LL^svT-O_gq5l6@7v~ z#4yIS(CX^_9(>epbqZl0pLn`6UNZ@G%rtH+& zBNETGRniy9lx_l)=>30vip>mo#s8%(X(^h}jq-fTz(P?@|ox$1&|7hOLdEjoE_p#{3qQ{=9+$h1@3 z=|(G;+FU4y;-?dlBS+JM48YyrSc`N#I?235D+ z0maBbrp64apBG#9P;p5oEevZvrUMEO?Jv~n*eZt%Yurp#<+QM*>)QS`+*?>?n-<%b zkVe%yQ{U{f%kYQ52RVndezI(9*k?B~%D8BxXv4gm`bVYXzIb-qkD=NPBkpvsL!g!D zCZff{B*##+_HW<=J_)Lj`sTb+u*Wu0Dh?_uv}+yq=vhw^L2^S7;(R zlY63}+##IOKtqVPF8R$%zU?A~=>~<|`1bT<$Le_`=@k8A9>GuP$2i@lS6G|_puDFP z9raCkflGPtU)zne!$g46W@kBSFJP5Z&CBfNtD)Xi7g6PK-#FB76I^^Tz?60o)lS#3 zWw;pBvWx8F|@8q0BMvl3+lo3h-Sgo`Hf!P z9_|za28>YpuJ4#s7oc!aTl(-u_7qt5@V+OYE}i+^)(RZ^vaR2egIv(F+IJ51kS z@xMEJr&sKxR41?5yZ^K?p@7t%BmzznO(J;`X(Xloe*h|h^PCjb%O5jqad~BUAX+;LmS|6INm8b7j}ybhcA$Wi%cJT?p8nzPF_Yky zKH3rGpzu6OLy5#UsBij)d*r4d#FNU^R&hMe193f|!s`tgwdoV{ZGS7Q9;L7UDm!mj z(+hS8g&VIn#R^3JGI&UU!cWO+JmdS|kWhK;IFHiG)j=Augj%<@XU4V;uh{1PW%kpx z_*+yxsC8`2WtT&a{dekYwF`m+H5A7OC|)+zSE`sm6xL${h!|-DWb7&cezAI~IymB% z92F$mgGoL0%_}o{1@bGRll>O}R2oMqqo`iH%6H{w&qP*0>HsXQDc6~NpmSkzLVt^v zKeC+XybQB{_mN(@{%UTHza~DR6wOi>wA2eq{aWvBOr-^qN4*zVR#)xFoNH$fQsh#1p)xV>a3A^a`BRw zUZ5&*GC(J?2RK|h#Cf=W&aI7d;r@7O#@6-B+M2=)XOEcTW;$F`;#sE5wL#U_j2KY3 z>GTLdRlBQ>o<}U6ASFP#yME&1q)Q*kEqW{JgJ25ujsXdvpdS2TfQosCb|$R^Cc=88 z2qyA$ALDk$*Sm7DAS~*zRc_lyRf)ln;uAuYAQ!0MyhD2O{Zq^7JkBb zOd!yN2r8t;WqQ2!sityBcK48jKmNzH1a73I|LrA>qx?}g;)DV?Q~75sg11;ZT!`Vn z%0`Xe0X&R@ ziPNb57x^%?NyZ-qBW#*%a6plciqX!Nr1j@r@~i{pbBnAAEwQVci2;Ce}f&;_BDbIJXG#_mg;a`-C`CAP1y?USmMjQNw3g z%{$dqV!_YMq8k|D7E)b?0+2YqpegkRQ|!$; zR~a({Rbi%Zk^8!4XX-Y?7;IjWjO&H(a69L1ZgH7|h)tbX<%#9P47OytkAkEZ5E)k& zx0rfu@fX?km*?cyjz#75A;TZ27<#st0Na!^NLB$-<2nbV!gvND9q*K>S4Z_BXljj+ z=;}{*OBqHpmLn#cjCQt)QdOsZ{SgBzTTQicRWVbn zy3j?B>fkk0J(%p!A0(@;@Q2B&oF?`=#zQ;>i`4Z9&a`xGVdsSipX^$6r13dYjmGbB zoDh6@8T8|lS!MFvLm2{@+u4Ld1^-w|_w&+3XKy3@5Z4>ttsREwxDg`85r1`#OURM% z5jb81)k$5pyQxm{=bNCky@Nr3XnG^dffOhMp$NxG)ciDK6>j^<-^U$kr84nID*38E zcHQTpOJ7QWL2n<24|_RuS<{~$TB)@D(Rw{lCm1o^WsNW1!p;8ZcIdQL$NiNb#V)7;X>W+q9RT~_<3%>Z}ezX#rasq~v1vAIuO$zpL!=%iTM6MGQ{P z?6RuSy$CYHs{y?1Zl)9%r|5XMuE5s483Kly-s=z&pq?Zo0LA!fK~D!amqyT6rpn-H zF#z`aK$ybjx)|s)!&J3)B5+|Rmo~QZS6!*MF5}VIXOKU+Gt3Pj_4RYNZe|NWp zx>hQ1xl8sYYTpbLWLE;fmFAw&WhUHrCw|gB7exmYZ9=)IY2aHLQ`(Q-ES<2Eo@`}0 zU&~Z|12^%NzC{ClMH71@hg$*@%U_Q?gRS%1?G`fRH-w9TN^v}DER!=q8wdC(lE0B( zWS0OEzX$-zMg$;Gjou@3?WfPSy!XJ%7c1H^E04BtQZ8{NHKWp5WE z#v$JnHC?0`1PiW_$sv^@hHv;p?jA<262o{opf0=#pg^2%deN?S2bCf#1tq4OD7;@l zqe}329!lFk>GJ`J-ys3_xyl%KW!KsNw*=u4TBv%gVIf5tO2`>Rk;Nbyi#7P`8?0Ms zMkKm9?B&Ng(i~IvH>Ob^nf%aS8`O1Gy6Rc!=mHSMy~m|4%czYaS}MNS+; znwmP-5-xO>5)Gn{5zPi zAzp4*RMJ-#>FKftr*2PNQYWch5;0*nLoNysfs3~TMa4*rE;oHA=AtbnrPQWMD@n^5 zI&J3Uv0rM1TYC`w1QMWBa1q=SB%O9G=&d~u$)xv9cCWA8 zLTR1aZ9u8gyiRP=Q@Rc&l{vY~On$iQ)Tl1`)Ky`+0!vfU2{2NH1RQ7k3-wfsw^C6Acp*D7mCWL8=}S1J(qz+D)CqI8a9Q(il$w5?Ek(0%ip+>?Ju@jFPN@EyDYKvhF;2!Y8P#S`=&eIt;GDc492t z{Tq z$5Q~4zc^0zhd$j2C_w!7{6b`R6HZdl#OpG}cisy)aEPBG%G4`95tzg%N+a}CK|)>s z8PnzjQsKPuuzQ}0s7GeXRiD`9)cx`qg0#UTm-Hydltcht@=00fTaDu1-a4`?qrluO z`}to2l&QPQ`R@Z?58`o&so*hTnf~&lF`XaMW1(7`cxBfU!o-i9JG?1hVcJ$&F(B_@ltYf{)8cS&b@pg>nCA28NQGKozRv7;hB}A3DvY~(g8E37hnOBXl?l@cjAM-B2I(i$>m3{3*JV?Z zsS;>(^jpv=mN=*dez9q0>Xl~|EGCw!*Ul>?6-)w#B*LfLlY^ILb@24nhEWQW;-mnp z%oHd%Drl-lGTmo&uyzU$Pny7Ok50t4Q#*F>>vj+Jhq!l=@z@>8>9ae9koYPWkGslc z6?1eao$M)!%r+QP5}58D&5)yhSC>jIDZTghQ%F52gYf0Qodkko{O%4ZyMOcJ%$1@O zHib*3yn?;kMXn*?l$~QBx{9CFIk=yqxrLt;xI3xjh8fC&r`!37elikh9ZZU^{v?8` z9Z(S>kX(v&t9*b;Hw9;=S=c4|yFSvP-|I=fUba|351%|zWQmo3>GM8i`IjcYy7eED zTB=i5Rf1D+NZ#IHDLP(lciC2br$E8%O19(Sk%N!_#>CoTLz1fi`9S2(=YzI(A`g1} zg5M8`ABz|HG8HxkjD}oOxRjHBCr84qN*ed{Ez{rRra11ILZlSC%oGVHmrgRfL~;VF z7b;5gRe+wt+h!N-4N&16@|t|T9b7(bP63KUImV(gx!Pp+gqaB)ZCXlw*veEcNf_!AXv2U757 zhAO15G1R|Hs6?nP^WOp0MT}p-nwYgKXyVBS%{F7lun&?t&X=@P>Q-oy1o7-r!i0QC z_91>(7F*5DKz+#dZ>&=j`woo2IsJ`<1H z4%qDsT?xp7Tw*NGMCv6D3?$npj-I~`(6ncCnY4E^p{os^#P^AzuWU~9#9Z1 z5%t~bko;pqC|Gh*>$pAowcQ{5RwOv2?XPbh5B8vA@QUqD2>fGm_;k&3;rjD{dY8mv znr?Iw>vsLc?V(5y4ezuFQpc|5rBn>O{z!Go@9{SYZ7 z(G4p1X?wJjb4xob{G*)m)K|N=GmMenC9&w+Hrk1_UH*9%;y)}WcZ+F1%)dL6?y%kN z-S%GyQ16vdcRgiSxufa%5Vsvf={yLiSje9Tlst&3yMLte{X3wXoPj7#wDkQhW(w1$ zeiuXG7QE#dB~Vwmt|sd+VFFxCcUAE2BH$nVLfY+c-SMKmJKFo&+X=mI$cG0Y{)5^2 zcTD#|zlSFh(|C`vyf2{MFQe|XW#?`8pV(6~c=?ppdjP7f?zx1D83QD?s#xuP0j11- zj%4pfmiKZ0w=ALB)P0{iq=tho>2as{oxJ7O?ViFWSOov>(A_?r&}|z*yRz)qA8`L? zHC1J5Ibj0pY(w!~zGXup$)%%{me)@nX(&J+ zRj*G!*P^;}YqG#k-1!j)MwAT0diHmjeG8if=>d5Vm)QME%HH+RY<~+fy@MEnNq;aX`#3HNG0=-Pf}Q* z?@XGG)*19UDMxjB4+2-nK1Up6!iP#5N*d-~L&=}OzqOH+q5OENp&ZXgElyWOckodh z&$zk!nXy1J{7-49Gn}vZXl?favChT%HEI%{5Cbe}^EppGEhO0$rOpxLTuqK_n2SG# zcwmRph7v7g3&y6DUPE;|)Q|uDx26x=M}>Da6qD4d)a8h>syOBq*^UCkhuqlirfPR5 z17q7={5H<`!xn2|$EK|6M{(ia0)%xZ1cbzeY+iL9!CemMEWmdrb#o9IT}(~o4TZl? zLKh)Op1)s1MRmVWh$FH;@$}#oD$AXV0}PiNhEfHp>NqYeF9?*Vg%*3Oe<5WGHE^O4 z=1vzz2n{i>xi?k#6PSt;tD`khT=D@^o~fby(-H`zMmP{#0wPOhBd@5IEQZg7z9(T^XY zaWI!B2JNbNV^=f?!0mE*>TUCnYA9VXR!tAB5Y2yrsI-JfNAC=s*%FfeE;tiNfWr{5 zX5+cbYVrx=os`eUH}u+?CMptC=SmA;3J8bssS}-wvo^Lj6t*Qi<)NQ9glr9kKjF|A zxn=%24V9{;>+XN(%6rpSXydZuVzNrR&I9gj9U`C+415Pw^<^HMc7aWfM#Vc*U^RZ7 zic_jwxTP2Z-!A{88N?~=QUKzHl6f=}Y#-H7TD4TBa}z4&xqyegB>!FN%w)NYR};)Q zvfc>PREbwL@&X!p*-#ZLCCW8b*9kvOE4F}xpZbo5id_m0^2FV|?GxuBo1QQEbcX^) zy5#$_+~tdmO5wR5Pu%5uf}0lcxn-S3lDtd$)i~ifQExOV&Sp85I*zQfgJa8l&N&5RKkb`c;C?;ita%fluw*6r(-)ro_IX#0i&w2wygo*w;rM4mTd(rJ+Dw z3wf}H>Lc}g`5~A)n4O|zAs!?aC!ac4m&knx51lfU}b`6>?@f|r>1O5oClM4D_g z6r5Fs5rKGW+l~f?nyXg{M<6NN|msT~&-@$#$hQgvIYZ&XTD-q|aY70Zk5~AUhGcNxqWFbBX5wjs#|8Pt*%B4Rn^6_u7qfK;0qkd9CjpY z7;$Om6@>h&DdqyiZY+n73Cvw(#3~W{Jte`It{Ms*HQwwM=V&Mi!c7TzFAD#E+V&c1 z#fI3~TUn%8i{pd0LehQ;K=y3f|9J{L|IiwtV)Q6Qvv)0 zJc6nAN{{CRpTo{@mtb~dIfHG@1HG@qzC?I5kp;93h208|Swq#(^F;$TyKQx{G3fB7$RwV@1H zzbhWi~(}S6W!PAD~yzoNk`6RvPZYjoPr*FcYwvXy^Ca}SJn<|mJ zkOk|Y*t)FUjzqqkrN+5U-N_i?oz8=JBtKRsIY67=9Tq>L|SeS|qT)cU`(f&Z`c@B(tdLU(f^QS2K3DsaWSJ9mON<6{|u6Y>=-%kQx|^@-pF zJ<{OzZ#Ju}5kFB9ZYoCWjB~sT|#1aG}`;m$KZH1T zDDAE0wt0MW><{@bK@*W*Y|@gqqt@DZed2fIqfp{xkTELEQ84Fvr|jfz-H)M*ks|V?S5VMikml5<3pp0A|2X>4VfB@YFgj{*>=N z#i2&w4SYfM5GOetkxh2FDM5+#y(pi{7{?Yd@*KO57-)yWd8V(S#N~4@`MfApX|5y7 zQdjPuh` zoR$qG_CG%MrbKgeyl5yb%Ta2JX>H+@1%yP=-E;-OW8OJ7wLrQ=Q+(3=oqPz}YX{YY zCVAfwyG144$Qrwp5K`I5fS-zpk!RabV)EBe`uc~Mmj>6{76!`Y2xxBC%oZQ+y!r16tt$|Z-qZ$@jrvIhU{ z{61oHWUn1Oi2gj_s--JL(rq^-MjgZ`M0|%*UPCp0>*$?6acJJy)wO(^9m=Yi)_Ssd zd{82!IXaBZa*b(i;gkiW@%)$vr5J~g@?+f@9Lzwv6gWlI2guBY;|+&&^KJ% zr;eV&PYRs}Q1yOr?ui9*mQMBL_h9x%p}ckgeS$96Bk)-1?5#l2Oin-uxh_-?#9z7A z_o7w}g}5*KaBQr0e^S_5LFe${2gJu^z#^x|MeWJgZQba3)jfhc&j)qJZI z3(D(<;FBev=9$%F7myscOFo>5W0%?@oD)Xn+BKBeBof<{JZS`s>`?d 2.4) - audio_session (0.0.1): - Flutter + - connectivity_plus (0.0.1): + - Flutter - device_info_plus (0.0.1): - Flutter - Firebase/Auth (11.15.0): @@ -251,6 +253,7 @@ PODS: DEPENDENCIES: - audio_session (from `.symlinks/plugins/audio_session/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -323,6 +326,8 @@ SPEC REPOS: EXTERNAL SOURCES: audio_session: :path: ".symlinks/plugins/audio_session/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" firebase_auth: @@ -396,6 +401,7 @@ SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e firebase_auth: 50af8366c87bb88c80ebeae62eb60189c7246b9b diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e0e8b89..976b1f9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -322,14 +322,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -343,14 +339,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e346993..f042e64 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,100 +1,117 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Intaleq - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Intaleq - CFBundlePackageType - APPL - CFBundleShortVersionString - 6 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d - - - - - CFBundleVersion - 1.0.6 - FirebaseAppDelegateProxyEnabled - NO - GMSApiKey - YOUR_API_KEY - LSApplicationQueriesSchemes - - googlechromes - comgooglemaps - - LSRequiresIPhoneOS - - NSCameraUsageDescription - This app requires access to your camera in order to scan QR codes and capture images + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + + com.intaleq.app + CFBundleURLSchemes + + intaleq + + + + FlutterDeepLinkingEnabled + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Intaleq + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Intaleq + CFBundlePackageType + APPL + CFBundleShortVersionString + 7 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + + com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d + + + + + CFBundleVersion + 1.0.7 + FirebaseAppDelegateProxyEnabled + NO + GMSApiKey + YOUR_API_KEY + LSApplicationQueriesSchemes + + googlechromes + comgooglemaps + + LSRequiresIPhoneOS + + NSCameraUsageDescription + This app requires access to your camera in order to scan QR codes and capture images for uploading and access to connect to a call. - NSContactsUsageDescription - This app requires contacts access to function properly. - NSFaceIDUsageDescription - Use Face ID to securely authenticate payment accounts. - NSLocationAlwaysAndWhenInUseUsageDescription - This app needs access to your location to provide you with the best ride experience. + NSContactsUsageDescription + This app requires contacts access to function properly. + NSFaceIDUsageDescription + Use Face ID to securely authenticate payment accounts. + NSLocationAlwaysAndWhenInUseUsageDescription + This app needs access to your location to provide you with the best ride experience. Your location data will be used to find the nearest available cars and connect you with the closest captain for efficient and convenient rides. - NSLocationAlwaysUsageDescription - This app needs access to location. - NSLocationWhenInUseUsageDescription - This app needs access to your location to provide you with the best ride experience. + NSLocationAlwaysUsageDescription + This app needs access to location. + NSLocationWhenInUseUsageDescription + This app needs access to your location to provide you with the best ride experience. Your location data will be used to find the nearest available cars and connect you with the closest captain for efficient and convenient rides. - NSMicrophoneUsageDescription - This app requires access to your microphone to record audio, allowing you to add + NSMicrophoneUsageDescription + This app requires access to your microphone to record audio, allowing you to add voice recordings to your photos and videos and access to connect to a call. - NSPhotoLibraryUsageDescription - This app requires access to the photo library to upload pictures. - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - fetch - location - remote-notification - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - + NSPhotoLibraryUsageDescription + This app requires access to the photo library to upload pictures. + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + fetch + location + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/lib/controller/auth/login_controller.dart b/lib/controller/auth/login_controller.dart index bcc9157..84b3a8f 100644 --- a/lib/controller/auth/login_controller.dart +++ b/lib/controller/auth/login_controller.dart @@ -242,6 +242,7 @@ class LoginController extends GetxController { if ((jsonDecode(token)['message']['token'].toString()) != box.read(BoxName.tokenFCM)) { await Get.defaultDialog( + barrierDismissible: false, title: 'Device Change Detected'.tr, middleText: 'Please verify your identity'.tr, textConfirm: 'Verify'.tr, diff --git a/lib/controller/functions/crud.dart b/lib/controller/functions/crud.dart index 2c736cb..2db9301 100644 --- a/lib/controller/functions/crud.dart +++ b/lib/controller/functions/crud.dart @@ -9,238 +9,326 @@ import 'package:Intaleq/env/env.dart'; import '../../constant/api_key.dart'; -import '../../print.dart'; import '../../views/widgets/elevated_btn.dart'; import '../../views/widgets/error_snakbar.dart'; -import 'add_error.dart'; import 'encrypt_decrypt.dart'; import 'upload_image.dart'; +import 'dart:io'; + +import 'package:jwt_decoder/jwt_decoder.dart'; + +import 'network/connection_check.dart'; +import 'network/net_guard.dart'; class CRUD { + final NetGuard _netGuard = NetGuard(); + + /// Stores the signature of the last logged error to prevent duplicates. + static String _lastErrorSignature = ''; + + /// Stores the timestamp of the last logged error. + static DateTime _lastErrorTimestamp = DateTime(2000); + + /// The minimum time that must pass before logging the same error again. + static const Duration _errorLogDebounceDuration = Duration(minutes: 1); + + /// Asynchronously logs an error to the server with debouncing to prevent log flooding. + static Future addError( + String error, String details, String where) async { + try { + final currentErrorSignature = '$where-$error'; + final now = DateTime.now(); + + if (currentErrorSignature == _lastErrorSignature && + now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) { + print("Debounced a duplicate error: $error"); + return; + } + + _lastErrorSignature = currentErrorSignature; + _lastErrorTimestamp = now; + + final userId = + box.read(BoxName.driverID) ?? box.read(BoxName.passengerID); + final userType = + box.read(BoxName.driverID) != null ? 'Driver' : 'Passenger'; + final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver); + + // Fire-and-forget call to prevent infinite loops if the logger itself fails. + CRUD().post( + link: AppLink.addError, + payload: { + 'error': error.toString(), + 'userId': userId.toString(), + 'userType': userType, + 'phone': phone.toString(), + 'device': where, + 'details': details, + }, + ); + } catch (e) { + print("CRITICAL: Failed to log error to server: $e"); + } + } + + /// Centralized private method to handle all API requests. + /// Includes retry logic, network checking, and standardized error handling. + Future _makeRequest({ + required String link, + Map? payload, + required Map headers, + }) async { + try { + var response = await HttpRetry.sendWithRetry( + () { + var url = Uri.parse(link); + return http.post( + url, + body: payload, + headers: headers, + ); + }, + maxRetries: 3, + timeout: const Duration(seconds: 15), + ); + + if (response.statusCode == 200) { + try { + var jsonData = jsonDecode(response.body); + if (jsonData['status'] == 'success') { + return jsonData; + } else { + // Log API logical errors (e.g., "Customer not found") + if (response.body == 'failure') { + return 'failure'; + } else { + addError( + 'API Logic Error: ${jsonData['status']}', + 'Response: ${response.body}', + 'CRUD._makeRequest - $link', + ); + } + + return jsonData['status']; + } + } catch (e, stackTrace) { + addError( + 'JSON Decode Error: $e', + 'Response Body: ${response.body}\nStack Trace: $stackTrace', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } else if (response.statusCode == 401) { + var jsonData = jsonDecode(response.body); + if (jsonData['error'] == 'Token expired') { + return 'token_expired'; + } else { + addError( + 'Unauthorized Error: ${jsonData['error']}', + 'Status Code: 401', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } else { + addError( + 'HTTP Error', + 'Status Code: ${response.statusCode}\nResponse Body: ${response.body}', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } on SocketException { + _netGuard.notifyOnce((title, msg) { + mySnackeBarError(msg); + }); + return 'no_internet'; + } catch (e, stackTrace) { + addError( + 'HTTP Request Exception: $e', + 'Stack Trace: $stackTrace', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } + + /// Performs a standard authenticated POST request. + /// Automatically handles token renewal. + Future post({ + required String link, + Map? payload, + }) async { + String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]; + if (JwtDecoder.isExpired(token)) { + await Get.put(LoginController()).getJWT(); + token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]; + } + + final headers = { + "Content-Type": "application/x-www-form-urlencoded", + 'Authorization': 'Bearer $token' + }; + + return await _makeRequest( + link: link, + payload: payload, + headers: headers, + ); + } + + /// Performs a standard authenticated GET request (using POST method as per original code). + /// Automatically handles token renewal. Future get({ required String link, Map? payload, }) async { - // print(r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]); - var url = Uri.parse( - link, - ); - var response = await http.post( - url, - body: payload, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - 'Authorization': - 'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}' - }, - ); - // print('req: ${response.request}'); - // Log.print('response: ${response.body}'); - // Log.print('payload: ${payload}'); - if (response.statusCode == 200) { - var jsonData = jsonDecode(response.body); - if (jsonData['status'] == 'success') { - return response.body; - } - - return jsonData['status']; - } else if (response.statusCode == 401) { - // Specifically handle 401 Unauthorized - var jsonData = jsonDecode(response.body); - - if (jsonData['error'] == 'Token expired') { - // Show snackbar prompting to re-login - await Get.put(LoginController()).getJWT(); - mySnackbarSuccess('please order now'.tr); - return 'token_expired'; // Return a specific value for token expiration - } else { - // Other 401 errors - addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); - return 'failure'; - } - } else { - addError('Non-200 response code: ${response.statusCode}', - 'crud().post - Other'); - return 'failure'; + String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]; + if (JwtDecoder.isExpired(token)) { + await Get.put(LoginController()).getJWT(); + token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]; } + + final headers = { + "Content-Type": "application/x-www-form-urlencoded", + 'Authorization': 'Bearer $token' + }; + + var result = await _makeRequest( + link: link, + payload: payload, + headers: headers, + ); + + // The original 'get' method returned the raw body on success, maintaining that behavior. + if (result is Map && result['status'] == 'success') { + return jsonEncode(result); + } + return result; } + /// Performs an authenticated POST request to wallet endpoints. + Future postWallet({ + required String link, + Map? payload, + }) async { + var jwt = await LoginController().getJwtWallet(); + final hmac = box.read(BoxName.hmac); + + final headers = { + "Content-Type": "application/x-www-form-urlencoded", + 'Authorization': 'Bearer $jwt', + 'X-HMAC-Auth': hmac.toString(), + }; + + return await _makeRequest( + link: link, + payload: payload, + headers: headers, + ); + } + + /// Performs an authenticated GET request to wallet endpoints (using POST). Future getWallet({ required String link, Map? payload, }) async { - var s = await LoginController().getJwtWallet(); + var jwt = await LoginController().getJwtWallet(); final hmac = box.read(BoxName.hmac); - // Log.print('hmac: ${hmac}'); - var url = Uri.parse( - link, + + final headers = { + "Content-Type": "application/x-www-form-urlencoded", + 'Authorization': 'Bearer $jwt', + 'X-HMAC-Auth': hmac.toString(), + }; + + var result = await _makeRequest( + link: link, + payload: payload, + headers: headers, ); - var response = await http.post( - url, - body: payload, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - 'Authorization': 'Bearer $s', - 'X-HMAC-Auth': hmac.toString(), - }, - ); - // print('req: ${response.request}'); - // Log.print('response: ${response.body}'); - // Log.print('payload: ${payload}'); - if (response.statusCode == 200) { - var jsonData = jsonDecode(response.body); - Log.print('jsonData: $jsonData'); - if (jsonData['status'] == 'success') { - return response.body; - } - return jsonData['status']; - } else if (response.statusCode == 401) { - // Specifically handle 401 Unauthorized - var jsonData = jsonDecode(response.body); - - if (jsonData['error'] == 'Token expired') { - // Show snackbar prompting to re-login - await Get.put(LoginController()).getJwtWallet(); - - return 'token_expired'; // Return a specific value for token expiration - } else { - // Other 401 errors - addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); - return 'failure'; - } - } else { - addError('Non-200 response code: ${response.statusCode}', - 'crud().post - Other'); - return 'failure'; + if (result is Map && result['status'] == 'success') { + return jsonEncode(result); } + return result; } - Future post( + // ======================================================================= + // All other specialized methods remain below. + // They are kept separate because they interact with external third-party APIs + // and have unique authentication, body structures, or error handling logic + // that doesn't fit the standardized `_makeRequest` helper. + // ======================================================================= + + Future postWalletMtn( {required String link, Map? payload}) async { - var url = Uri.parse(link); + // This method has a very custom response-wrapping logic, so it's kept separate. + final s = await LoginController().getJwtWallet(); + final hmac = box.read(BoxName.hmac); + final url = Uri.parse(link); + try { - var response = await http.post( + final response = await http.post( url, body: payload, headers: { "Content-Type": "application/x-www-form-urlencoded", - 'Authorization': - 'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}' + "Authorization": "Bearer $s", + "X-HMAC-Auth": hmac.toString(), }, ); - // Log.print('req: ${response.request}'); - // Log.print('response: ${response.body}'); - // Log.print('payload: ${payload}'); + + print('req: ${response.request}'); + print('status: ${response.statusCode}'); + print('body: ${response.body}'); + print('payload: $payload'); + + Map wrap(String status, {Object? message, int? code}) { + return { + 'status': status, + 'message': message, + 'code': code ?? response.statusCode, + }; + } + if (response.statusCode == 200) { try { - var jsonData = jsonDecode(response.body); - if (jsonData['status'] == 'success') { - return jsonData; - } else { - return jsonData['status']; - } + return jsonDecode(response.body); } catch (e) { - // addError(e.toString(), 'crud().post - JSON decoding'); - return 'failure'; + return wrap('failure', + message: 'JSON decode error', code: response.statusCode); } } else if (response.statusCode == 401) { - // Specifically handle 401 Unauthorized - var jsonData = jsonDecode(response.body); - - if (jsonData['error'] == 'Token expired') { - // Show snackbar prompting to re-login - await Get.put(LoginController()).getJWT(); - // MyDialog().getDialog( - // 'Session expired. Please log in again.'.tr, - // '', - // () { - // Get.put(LoginController()).loginUsingCredentials( - // box.read(BoxName.passengerID), box.read(BoxName.email)); - // Get.back(); - // }, - // ); - - return 'token_expired'; // Return a specific value for token expiration - } else { - // Other 401 errors - // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); - return 'failure'; - } - } else { - // addError('Non-200 response code: ${response.statusCode}', - // 'crud().post - Other'); - return 'failure'; - } - } catch (e) { - // addError('HTTP request error: $e', 'crud().post - HTTP'); - return 'failure'; - } - } - - Future postWallet( - {required String link, Map? payload}) async { - var s = await LoginController().getJwtWallet(); - - final hmac = box.read(BoxName.hmac); - - var url = Uri.parse(link); - try { - var response = await http.post( - url, - body: payload, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - 'Authorization': 'Bearer $s', - 'X-HMAC-Auth': hmac.toString(), - }, - ); - // print('req: ${response.request}'); - // Log.print('response: ${response.body}'); - // Log.print('payload: ${payload}'); - if (response.statusCode == 200) { try { - var jsonData = jsonDecode(response.body); - if (jsonData['status'] == 'success') { - return jsonData; - } else { - return jsonData['status']; + final jsonData = jsonDecode(response.body); + if (jsonData is Map && jsonData['error'] == 'Token expired') { + await Get.put(LoginController()).getJWT(); + return { + 'status': 'failure', + 'message': 'token_expired', + 'code': 401 + }; } - } catch (e) { - addError(e.toString(), 'crud().post - JSON decoding'); - return 'failure'; - } - } else if (response.statusCode == 401) { - // Specifically handle 401 Unauthorized - var jsonData = jsonDecode(response.body); - - if (jsonData['error'] == 'Token expired') { - // Show snackbar prompting to re-login - await Get.put(LoginController()).getJWT(); - // MyDialog().getDialog( - // 'Session expired. Please log in again.'.tr, - // '', - // () { - // Get.put(LoginController()).loginUsingCredentials( - // box.read(BoxName.passengerID), box.read(BoxName.email)); - // Get.back(); - // }, - // ); - - return 'token_expired'; // Return a specific value for token expiration - } else { - // Other 401 errors - // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); - return 'failure'; + return wrap('failure', message: jsonData); + } catch (_) { + return wrap('failure', message: response.body); } } else { - // addError('Non-200 response code: ${response.statusCode}', - // 'crud().post - Other'); - return 'failure'; + try { + final jsonData = jsonDecode(response.body); + return wrap('failure', message: jsonData); + } catch (_) { + return wrap('failure', message: response.body); + } } } catch (e) { - // addError('HTTP request error: $e', 'crud().post - HTTP'); - return 'failure'; + return { + 'status': 'failure', + 'message': 'HTTP request error: $e', + 'code': -1 + }; } } @@ -248,6 +336,7 @@ class CRUD { required String link, Map? payload, }) async { + // Uses Basic Auth, so it's a separate implementation. var url = Uri.parse( link, ); @@ -261,14 +350,10 @@ class CRUD { }, ); if (response.statusCode == 200) { - var jsonData = jsonDecode(response.body); - // if (jsonData['status'] == 'success') { - - return jsonData; - // } - - // return jsonData['status']; + return jsonDecode(response.body); } + // Consider adding error handling here. + return null; } Future sendWhatsAppAuth(String to, String token) async { @@ -706,4 +791,7 @@ class CRUD { ); return json.decode(response.body); } + + // ... [Other methods like sendWhatsAppAuth, getAgoraToken, getLlama, etc., would remain here as they are] ... + // For brevity, I am omitting the rest of the third-party API methods as they would not change. } diff --git a/lib/controller/functions/network/connection_check.dart b/lib/controller/functions/network/connection_check.dart new file mode 100644 index 0000000..82bc47b --- /dev/null +++ b/lib/controller/functions/network/connection_check.dart @@ -0,0 +1,48 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:http/http.dart' as http; + +import 'net_guard.dart'; + +typedef BodyEncoder = Future Function(); + +class HttpRetry { + /// ريتراي لـ network/transient errors فقط. + static Future sendWithRetry( + BodyEncoder send, { + int maxRetries = 3, + Duration baseDelay = const Duration(milliseconds: 400), + Duration timeout = const Duration(seconds: 12), + }) async { + // ✅ Pre-flight check for internet connection + if (!await NetGuard().hasInternet()) { + // Immediately throw a specific exception if there's no internet. + // This avoids pointless retries. + throw const SocketException("No internet connection"); + } + int attempt = 0; + while (true) { + attempt++; + try { + final res = await send().timeout(timeout); + return res; + } on TimeoutException catch (_) { + if (attempt >= maxRetries) rethrow; + } on SocketException catch (_) { + if (attempt >= maxRetries) rethrow; + } on HandshakeException catch (_) { + if (attempt >= maxRetries) rethrow; + } on http.ClientException catch (e) { + // مثال: Connection reset by peer + final msg = e.message.toLowerCase(); + final transient = msg.contains('connection reset') || + msg.contains('broken pipe') || + msg.contains('timed out'); + if (!transient || attempt >= maxRetries) rethrow; + } + // backoff: 0.4s, 0.8s, 1.6s + final delay = baseDelay * (1 << (attempt - 1)); + await Future.delayed(delay); + } + } +} diff --git a/lib/controller/functions/network/net_guard.dart b/lib/controller/functions/network/net_guard.dart new file mode 100644 index 0000000..7c4c1f1 --- /dev/null +++ b/lib/controller/functions/network/net_guard.dart @@ -0,0 +1,48 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:internet_connection_checker/internet_connection_checker.dart'; + +class NetGuard { + static final NetGuard _i = NetGuard._(); + NetGuard._(); + factory NetGuard() => _i; + + bool _notified = false; + + /// فحص: (أ) فيه شبكة؟ (ب) فيه انترنت؟ (ج) السيرفر نفسه reachable؟ + Future hasInternet({Uri? mustReach}) async { + final connectivity = await Connectivity().checkConnectivity(); + if (connectivity == ConnectivityResult.none) return false; + + final hasNet = + await InternetConnectionChecker.createInstance().hasConnection; + if (!hasNet) return false; + + if (mustReach != null) { + try { + final host = mustReach.host; + final result = await InternetAddress.lookup(host); + if (result.isEmpty || result.first.rawAddress.isEmpty) return false; + + // اختباري خفيف عبر TCP (80/443) — 400ms timeout + final port = mustReach.scheme == 'http' ? 80 : 443; + final socket = await Socket.connect(host, port, + timeout: const Duration(milliseconds: 400)); + socket.destroy(); + } catch (_) { + return false; + } + } + return true; + } + + /// إظهار إشعار مرة واحدة ثم إسكات التكرارات + void notifyOnce(void Function(String title, String msg) show) { + if (_notified) return; + _notified = true; + show('لا يوجد اتصال بالإنترنت', 'تحقق من الشبكة ثم حاول مجددًا.'); + // إعادة السماح بعد 15 ثانية + Future.delayed(const Duration(seconds: 15), () => _notified = false); + } +} diff --git a/lib/controller/home/map_passenger_controller.dart b/lib/controller/home/map_passenger_controller.dart index 533e6db..e359ac2 100644 --- a/lib/controller/home/map_passenger_controller.dart +++ b/lib/controller/home/map_passenger_controller.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'dart:ui'; import 'dart:convert'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; import 'package:Intaleq/constant/univeries_polygon.dart'; @@ -13,6 +14,7 @@ import 'package:Intaleq/controller/firebase/local_notification.dart'; import 'package:Intaleq/controller/functions/encrypt_decrypt.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_confetti/flutter_confetti.dart'; +import 'package:uni_links/uni_links.dart'; import 'package:vector_math/vector_math.dart' show radians, degrees; import 'package:Intaleq/controller/functions/tts.dart'; @@ -56,6 +58,10 @@ import 'device_tier.dart'; import 'vip_waitting_page.dart'; class MapPassengerController extends GetxController { + // --- START: DEEP LINKING ADDITIONS --- + StreamSubscription? _linkSubscription; + // --- END: DEEP LINKING ADDITIONS --- + bool isLoading = true; TextEditingController placeDestinationController = TextEditingController(); TextEditingController increasFeeFromPassenger = TextEditingController(); @@ -280,6 +286,92 @@ class MapPassengerController extends GetxController { update(); } + /// Initializes the deep link listener. + /// It checks for the initial link when the app starts and then listens for subsequent links. + Future _initUniLinks() async { + try { + // Get the initial link that opened the app + final initialLink = await getInitialUri(); + if (initialLink != null) { + handleDeepLink(initialLink); + } + } on PlatformException { + print('Failed to get initial deep link.'); + } on FormatException { + print('Invalid initial deep link format.'); + } + + // Listen for incoming links while the app is running + _linkSubscription = uriLinkStream.listen((Uri? link) { + handleDeepLink(link); + }, onError: (err) { + print('Error listening to deep links: $err'); + }); + } + + /// Parses the incoming deep link and triggers the route initiation. + void handleDeepLink(Uri? link) { + if (link == null) return; + + // Check if the link matches your app's scheme and path + // e.g., intaleq://map?lat=31.9539&lng=35.9106 + if (link.scheme == 'intaleq' && link.host == 'map') { + final latString = link.queryParameters['lat']; + final lngString = link.queryParameters['lng']; + + if (latString != null && lngString != null) { + final double? lat = double.tryParse(latString); + final double? lng = double.tryParse(lngString); + + if (lat != null && lng != null) { + final destination = LatLng(lat, lng); + print('Deep link received. Destination: $destination'); + initiateRouteFromDeepLink(destination); + } else { + print('Failed to parse lat/lng from deep link.'); + } + } + } + } + + /// Sets the destination from the deep link and updates the UI to show the map. + void initiateRouteFromDeepLink(LatLng destination) async { + // Wait for map controller to be ready + if (mapController == null) { + await Future.delayed(const Duration(seconds: 1)); + if (mapController == null) { + print("Map controller is not available to handle deep link."); + return; + } + } + + myDestination = destination; + + // Animate camera to user's current location to show the starting point + await mapController?.animateCamera(CameraUpdate.newLatLng( + LatLng(passengerLocation.latitude, passengerLocation.longitude))); + + // Ensure the main menu is visible to start the booking process + if (isMainBottomMenuMap) { + changeMainBottomMenuMap(); + } + + passengerStartLocationFromMap = true; + isPickerShown = true; + hintTextDestinationPoint = "Destination from external link".tr; + update(); + + // The user can now see the destination and proceed to get the route and price. + Get.snackbar( + "Location Received".tr, + "The destination has been set from the link.".tr, + backgroundColor: AppColor.greenColor, + colorText: Colors.white, + ); + } + + // --- END: DEEP LINKING METHODS --- + void getCurrentLocationFormString() async { currentLocationToFormPlaces = true; currentLocationString = 'Waiting for your location'.tr; @@ -3190,6 +3282,8 @@ class MapPassengerController extends GetxController { print( "--- MapPassengerController: Closing and cleaning up all resources. ---"); + _linkSubscription?.cancel(); + // 1. إلغاء المؤقتات الفردية // Using ?.cancel() is safe even if the timer is null markerReloadingTimer.cancel(); @@ -5719,6 +5813,7 @@ class MapPassengerController extends GetxController { await initilizeGetStorage(); // إعداد سريع await _initMinimalIcons(); // start/end فقط await addToken(); // لو لازم للمصادقة + await _initUniLinks(); await getLocation(); // لتحديد الكاميرا box.write(BoxName.carType, 'yet'); box.write(BoxName.tipPercentage, '0'); diff --git a/lib/controller/local/translations.dart b/lib/controller/local/translations.dart index 4925bef..edefc8b 100644 --- a/lib/controller/local/translations.dart +++ b/lib/controller/local/translations.dart @@ -179,6 +179,13 @@ class MyTranslation extends Translations { "Contacts Loaded": "تم تحميل جهات الاتصال", "Showing": "يتم عرض", "of": "من", + "Customer not found": "العميل غير موجود", + "Wallet is blocked": "المحفظة محظورة", + "Customer phone is not active": "هاتف العميل غير نشط", + "Balance not enough": "الرصيد غير كافٍ", + "Balance limit exceeded": "تم تجاوز حد الرصيد", + "Incorrect sms code": + "⚠️ رمز التحقق الذي أدخلته غير صحيح. يرجى المحاولة مرة أخرى.", "contacts. Others were hidden because they don't have a phone number.": "جهة اتصال. تم إخفاء البقية لعدم وجود أرقام هواتف لديهم.", "No contacts found": "لم يتم العثور على جهات اتصال", @@ -1363,6 +1370,8 @@ class MyTranslation extends Translations { "Edit Your data": "تعديل بياناتك", "write vin for your car": "اكتب رقم هيكل سيارتك", "VIN": "رقم الهيكل", + "Device Change Detected": "تم اكتشاف تغيير في الجهاز", + "Please verify your identity": "يرجى التحقق من هويتك", "write Color for your car": "اكتب لون سيارتك", "write Make for your car": "اكتب الشركة المصنعة لسيارتك", "write Model for your car": "اكتب موديل سيارتك", @@ -1458,6 +1467,19 @@ class MyTranslation extends Translations { "يرجى البقاء في نقطة الالتقاط المحددة.", "message From Driver": "رسالة من السائق", "Trip is Begin": "بدأت الرحلة", + "Verify OTP": "التحقق من الرمز", + "Customer not found": "العميل غير موجود", + "Wallet is blocked": "المحفظة محظورة", + "Customer phone is not active": "هاتف العميل غير نشط", + "Balance not enough": "الرصيد غير كافٍ", + "Balance limit exceeded": "تم تجاوز حد الرصيد", + "Verification Code": "رمز التحقق", + "We have sent a verification code to your mobile number:": + "لقد أرسلنا رمز التحقق إلى رقم هاتفك المحمول:", + "Verify": "تحقق", + "Resend Code": "إعادة إرسال الرمز", + "You can resend in": "يمكنك إعادة الإرسال خلال", + "seconds": "ثوانٍ", "Cancel Trip from driver": "إلغاء الرحلة من السائق", "We will look for a new driver.\nPlease wait.": "هنبحث عن سائق جديد.\nمن فضلك انتظر.", diff --git a/lib/controller/payment/payment_controller.dart b/lib/controller/payment/payment_controller.dart index 580c313..aff1439 100644 --- a/lib/controller/payment/payment_controller.dart +++ b/lib/controller/payment/payment_controller.dart @@ -664,154 +664,154 @@ class PaymentController extends GetxController { Future payWithMTNWallet( BuildContext context, String amount, String currency) async { - // استخدام مؤشر تحميل لتجربة مستخدم أفضل - Get.dialog(const Center(child: CircularProgressIndicator()), - barrierDismissible: false); + // خزن سياق علوي آمن من البداية + final BuildContext safeContext = + Get.overlayContext ?? Get.context ?? context; + + // سبينر تحميل + if (!(Get.isDialogOpen ?? false)) { + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + } try { - String phone = box.read(BoxName.phoneWallet); - String passengerID = box.read(BoxName.passengerID).toString(); - String formattedAmount = double.parse(amount).toStringAsFixed(0); + final phone = box.read(BoxName.phoneWallet) as String; + final passengerID = box.read(BoxName.passengerID).toString(); + final formattedAmount = double.parse(amount).toStringAsFixed(0); print("🚀 بدء عملية دفع MTN"); print( "📦 Payload: passengerID: $passengerID, amount: $formattedAmount, phone: $phone"); - // التحقق من البصمة (اختياري) - bool isAuthSupported = await LocalAuthentication().isDeviceSupported(); + // التحقق بالبصمة (اختياري) + حماية من الـ await + final localAuth = LocalAuthentication(); + final isAuthSupported = await localAuth.isDeviceSupported(); if (isAuthSupported) { - bool didAuthenticate = await LocalAuthentication().authenticate( + final didAuth = await localAuth.authenticate( localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع', ); - if (!didAuthenticate) { - if (Get.isDialogOpen ?? false) Get.back(); + if (!didAuth) { + if (Get.isDialogOpen == true) Get.back(); print("❌ المستخدم لم يؤكد بالبصمة/الوجه"); return; } } - // 1️⃣ استدعاء mtn_start_payment.php (الملف الجديد) - var responseData = await CRUD().postWallet( + // 1) بدء الدفع + final responseData = await CRUD().postWalletMtn( link: AppLink.payWithMTNStart, payload: { "amount": formattedAmount, "passengerId": passengerID, "phone": phone, + "lang": box.read(BoxName.lang) ?? 'ar', }, ); - print("✅ استجابة الخادم (mtn_start_payment.php):"); - print(responseData); - - // --- بداية التعديل المهم --- - // التحقق القوي من الاستجابة لتجنب الأخطاء - Map startRes; + // print("✅ استجابة الخادم (mtn_start_payment.php):"); + // print(responseData); + Log.print('responseData: ${responseData}'); + // فحص الاستجابة بقوة + late final Map startRes; if (responseData is Map) { - // إذا كانت الاستجابة بالفعل Map، استخدمها مباشرة startRes = responseData; } else if (responseData is String) { - // إذا كانت نص، حاول تحليلها كـ JSON - try { - startRes = json.decode(responseData); - } catch (e) { - throw Exception( - "فشل في تحليل استجابة الخادم. الاستجابة: $responseData"); - } + startRes = json.decode(responseData) as Map; } else { - // نوع غير متوقع throw Exception("تم استلام نوع بيانات غير متوقع من الخادم."); } if (startRes['status'] != 'success') { - String errorMsg = startRes['message']?.toString() ?? + final errorMsg = startRes['message']['Error']?.toString().tr ?? "فشل بدء عملية الدفع. حاول مرة أخرى."; throw Exception(errorMsg); } - // --- نهاية التعديل المهم --- - // استخراج البيانات بأمان - final messageData = startRes["message"]; + final messageData = startRes["message"] as Map; final invoiceNumber = messageData["invoiceNumber"].toString(); final operationNumber = messageData["operationNumber"].toString(); final guid = messageData["guid"].toString(); - print( - "📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid"); + // print( + // "📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid"); - if (Get.isDialogOpen ?? false) - Get.back(); // إغلاق مؤشر التحميل قبل عرض حوار OTP + // أغلق السبينر قبل إظهار حوار OTP + if (Get.isDialogOpen == true) Get.back(); - // 2️⃣ عرض واجهة إدخال OTP - String? otp = await showDialog( - context: context, - builder: (context) { - String input = ""; - return AlertDialog( - title: const Text("أدخل كود التحقق"), - content: TextField( - keyboardType: TextInputType.number, - decoration: const InputDecoration(hintText: "كود OTP"), - onChanged: (val) => input = val, - ), - actions: [ - TextButton( - child: const Text("تأكيد"), - onPressed: () => Navigator.of(context).pop(input), - ), - TextButton( - child: const Text("إلغاء"), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ); - }, - ); + // 2) إدخال OTP بـ Get.defaultDialog (لا يستخدم context قابل للتلف) + String otpInput = ""; + await Get.defaultDialog( + title: "أدخل كود التحقق", + barrierDismissible: false, + content: TextField( + keyboardType: TextInputType.number, + decoration: const InputDecoration(hintText: "كود OTP"), + onChanged: (v) => otpInput = v, + ), + confirm: TextButton( + onPressed: () { + if (otpInput.isEmpty || + otpInput.length < 4 || + otpInput.length > 8) { + Get.snackbar("تنبيه", "أدخل كود OTP صحيح (4–8 أرقام)"); + return; + } + Get.back(result: otpInput); + }, + child: const Text("تأكيد"), + ), + cancel: TextButton( + onPressed: () => Get.back(result: null), + child: const Text("إلغاء"), + ), + ).then((res) => otpInput = (res ?? "") as String); - if (otp == null || otp.isEmpty) { + if (otpInput.isEmpty) { print("❌ لم يتم إدخال OTP"); return; } - print("🔐 تم إدخال OTP: $otp"); + print("🔐 تم إدخال OTP: $otpInput"); + // سبينر أثناء التأكيد Get.dialog(const Center(child: CircularProgressIndicator()), barrierDismissible: false); - // 3️⃣ استدعاء mtn_confirm.php - var confirmRes = await CRUD().postWallet( + // 3) تأكيد الدفع + final confirmRes = await CRUD().postWalletMtn( link: AppLink.payWithMTNConfirm, payload: { "invoiceNumber": invoiceNumber, "operationNumber": operationNumber, "guid": guid, - "otp": otp, + "otp": otpInput, "phone": phone, + "lang": box.read(BoxName.lang) ?? 'ar', }, ); - if (Get.isDialogOpen ?? false) Get.back(); + if (Get.isDialogOpen == true) Get.back(); - print("✅ استجابة mtn_confirm.php:"); - print(confirmRes); + // print("✅ استجابة mtn_confirm.php:"); + // Log.print('confirmRes: ${confirmRes}'); - if (confirmRes != null && confirmRes['status'] == 'success') { + final ok = (confirmRes is Map && confirmRes['status'] == 'success'); + if (ok) { Get.defaultDialog( title: "✅ نجاح", content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."), ); + await getPassengerWallet(); } else { - String errorMsg = - confirmRes?['message']?.toString() ?? "فشل في تأكيد الدفع"; - Get.defaultDialog( - title: "❌ فشل", - content: Text(errorMsg), - ); + final errorMsg = (confirmRes['message']['message']?.toString()) ?? + "فشل في تأكيد الدفع"; + Get.defaultDialog(title: "❌ فشل", content: Text(errorMsg.tr)); } } catch (e, s) { print("🔥 خطأ أثناء الدفع عبر MTN:"); print(e); print(s); - if (Get.isDialogOpen ?? false) Get.back(); + if (Get.isDialogOpen == true) Get.back(); Get.defaultDialog( title: 'حدث خطأ', content: Text(e.toString().replaceFirst("Exception: ", "")), diff --git a/lib/main.dart b/lib/main.dart index a978ab6..c5d9880 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ +import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:Intaleq/controller/functions/crud.dart'; import 'package:Intaleq/controller/payment/paymob/paymob_response.dart'; import 'package:Intaleq/views/home/HomePage/contact_us.dart'; import 'package:Intaleq/views/home/HomePage/share_app_page.dart'; @@ -133,7 +135,30 @@ void main() async { ), ]); - runApp(const MyApp()); + runZonedGuarded>(() async { + runApp(const MyApp()); + }, (error, stack) { + // ==== START: ERROR FILTER ==== + String errorString = error.toString(); + + // Print all errors to the local debug console for development + print("Caught Dart error: $error"); + print(stack); + + // We will check if the error contains keywords for errors we want to ignore. + // If it's one of them, we will NOT send it to the server. + bool isIgnoredError = errorString.contains('PERMISSION_DENIED') || + errorString.contains('FormatException') || + errorString.contains('Null check operator used on a null value'); + + if (!isIgnoredError) { + // Only send the error to the server if it's not in our ignore list. + CRUD.addError(error.toString(), stack.toString(), 'main'); + } else { + print("Ignoring error and not sending to server: $errorString"); + } + // ==== END: ERROR FILTER ==== + }); } class MyApp extends StatelessWidget { diff --git a/lib/views/home/my_wallet/passenger_wallet_dialoge.dart b/lib/views/home/my_wallet/passenger_wallet_dialoge.dart index 610f426..dcd541b 100644 --- a/lib/views/home/my_wallet/passenger_wallet_dialoge.dart +++ b/lib/views/home/my_wallet/passenger_wallet_dialoge.dart @@ -9,6 +9,8 @@ import 'package:Intaleq/controller/functions/toast.dart'; import 'package:Intaleq/controller/payment/payment_controller.dart'; import '../../../main.dart'; +import '../../widgets/elevated_btn.dart'; +import '../../widgets/my_textField.dart'; class PassengerWalletDialog extends StatelessWidget { const PassengerWalletDialog({ @@ -264,76 +266,143 @@ void showPaymentOptions(BuildContext context, PaymentController controller) { }, ) : const SizedBox(), - box.read(BoxName.phoneWallet) != null - ? CupertinoActionSheetAction( - child: Text('💰 Pay with Wallet'.tr), + // box.read(BoxName.phoneWallet) != null + // ? CupertinoActionSheetAction( + // child: Text('💰 Pay with Wallet'.tr), + // onPressed: () async { + // if (controller.selectedAmount != 0) { + // controller.isLoading = true; + // controller.update(); + // controller.payWithMTNWallet( + // context, + // controller.selectedAmount.toString(), + // 'SYP', + // ); + // await controller.getPassengerWallet(); + // controller.isLoading = false; + // controller.update(); + // } else { + // Toast.show(context, '⚠️ You need to choose an amount!'.tr, + // AppColor.redColor); + // } + // }, + // ) + // : CupertinoActionSheetAction( + // child: Text('Add wallet phone you use'.tr), + // onPressed: () { + // Get.dialog( + // CupertinoAlertDialog( + // title: Text('Insert Wallet phone number'.tr), + // content: Column( + // children: [ + // const SizedBox(height: 10), + // CupertinoTextField( + // controller: controller.walletphoneController, + // placeholder: 'Insert Wallet phone number'.tr, + // keyboardType: TextInputType.phone, + // padding: const EdgeInsets.symmetric( + // vertical: 12, + // horizontal: 10, + // ), + // ), + // ], + // ), + // actions: [ + // CupertinoDialogAction( + // child: Text('Cancel'.tr, + // style: const TextStyle( + // color: CupertinoColors.destructiveRed)), + // onPressed: () { + // Get.back(); + // }, + // ), + // CupertinoDialogAction( + // child: Text('OK'.tr, + // style: const TextStyle( + // color: CupertinoColors.activeGreen)), + // onPressed: () async { + // Get.back(); + // box.write(BoxName.phoneWallet, + // (controller.walletphoneController.text)); + // Toast.show( + // context, + // 'Phone Wallet Saved Successfully'.tr, + // AppColor.greenColor); + // }, + // ), + // ], + // ), + // barrierDismissible: false, + // ); + // }, + // ), + GestureDetector( + onTap: () async { + Get.back(); + // final formKey = GlobalKey(); + // final phoneController = TextEditingController(); + + Get.defaultDialog( + barrierDismissible: false, + title: 'Insert Wallet phone number'.tr, + content: Form( + key: controller.formKey, + child: TextFormField( + controller: controller.walletphoneController, + keyboardType: TextInputType.phone, + decoration: InputDecoration( + labelText: 'Insert Wallet phone number'.tr, + hintText: '963941234567', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '⚠️ Please enter phone number'.tr; + } else if (value.length != 12) { + return '⚠️ Phone number must be 12 digits'.tr; + } + return null; + }, + ), + ), + confirm: ElevatedButton( + child: Text('OK'.tr), onPressed: () async { - if (controller.selectedAmount != 0) { - controller.isLoading = true; - controller.update(); - controller.payWithMTNWallet( - context, - controller.selectedAmount.toString(), - 'SYP', - ); - await controller.getPassengerWallet(); - controller.isLoading = false; - controller.update(); - } else { - Toast.show(context, '⚠️ You need to choose an amount!'.tr, - AppColor.redColor); + if (controller.formKey.currentState!.validate()) { + if (controller.selectedAmount != 0) { + controller.isLoading = true; + controller.update(); + box.write(BoxName.phoneWallet, + (controller.walletphoneController.text)); + Get.back(); + await controller.payWithMTNWallet( + context, + controller.selectedAmount.toString(), + 'SYP', + ); + await controller.getPassengerWallet(); + + controller.isLoading = false; + controller.update(); + } else { + Toast.show( + context, + '⚠️ You need to choose an amount!'.tr, + AppColor.redColor, + ); + } } }, - ) - : CupertinoActionSheetAction( - child: Text('Add wallet phone you use'.tr), - onPressed: () { - Get.dialog( - CupertinoAlertDialog( - title: Text('Insert Wallet phone number'.tr), - content: Column( - children: [ - const SizedBox(height: 10), - CupertinoTextField( - controller: controller.walletphoneController, - placeholder: 'Insert Wallet phone number'.tr, - keyboardType: TextInputType.phone, - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 10, - ), - ), - ], - ), - actions: [ - CupertinoDialogAction( - child: Text('Cancel'.tr, - style: const TextStyle( - color: CupertinoColors.destructiveRed)), - onPressed: () { - Get.back(); - }, - ), - CupertinoDialogAction( - child: Text('OK'.tr, - style: const TextStyle( - color: CupertinoColors.activeGreen)), - onPressed: () async { - Get.back(); - box.write(BoxName.phoneWallet, - (controller.walletphoneController.text)); - Toast.show( - context, - 'Phone Wallet Saved Successfully'.tr, - AppColor.greenColor); - }, - ), - ], - ), - barrierDismissible: false, - ); - }, ), + ); + }, + child: Image.asset( + 'assets/images/mtn.png', + width: 70, + height: 70, + fit: BoxFit.contain, + ), + ) ], cancelButton: CupertinoActionSheetAction( child: Text('Cancel'.tr), diff --git a/pubspec.lock b/pubspec.lock index 34b2a6d..44d64d7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -233,6 +233,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.dev" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" convert: dependency: transitive description: @@ -1112,6 +1128,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1+1" + internet_connection_checker: + dependency: "direct main" + description: + name: internet_connection_checker + sha256: ee08f13d8b13b978affe226e9274ca3ba7a9bed07c9479e8ae245f785b7a488a + url: "https://pub.dev" + source: hosted + version: "3.0.1" intl: dependency: "direct main" description: @@ -1336,6 +1360,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" octo_image: dependency: transitive description: @@ -1908,6 +1940,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + uni_links: + dependency: "direct main" + description: + name: uni_links + sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + uni_links_platform_interface: + dependency: transitive + description: + name: uni_links_platform_interface + sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + uni_links_web: + dependency: transitive + description: + name: uni_links_web + sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" + url: "https://pub.dev" + source: hosted + version: "0.1.0" url_launcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 8f1cbe1..a3a4edf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: sign_in_with_apple: ^6.1.0 firebase_auth: ^5.1.2 device_info_plus: ^11.3.0 - # uni_links: ^0.5.1 + uni_links: ^0.5.1 googleapis_auth: ^1.6.0 flutter_confetti: ^0.3.0 # intl_phone_field: ^3.1.0 @@ -75,6 +75,8 @@ dependencies: shimmer: ^3.0.0 share_plus: ^11.0.0 asn1lib: ^1.6.5 + internet_connection_checker: ^3.0.1 + connectivity_plus: ^6.1.5 # home_widget: ^0.7.0+1 dev_dependencies: