From e734a21a5b52bb823dbae12b5286b6c8cb1071b4 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Fri, 16 Jan 2026 10:58:09 +0700 Subject: [PATCH] feat: Implement OAuth2 password and refresh token grants for authentication, update user registration fields, and add new documentation. --- .../UserInterfaceState.xcuserstate | Bin 53446 -> 52314 bytes .../AppClientBaseSwift/Models/User.swift | 79 ++++- .../Services/APIService.swift | 2 - .../Services/AuthManager.swift | 132 ++++---- .../ViewModels/AuthViewModel.swift | 64 +--- apps/app-client-base-swift/README.md | 301 ++---------------- apps/app-client-base-swift/docs/README.md | 33 ++ apps/app-client-base-swift/docs/en/README.md | 207 ++++++++++++ .../en/architecture.md} | 141 +++----- apps/app-client-base-swift/docs/vi/README.md | 207 ++++++++++++ .../docs/vi/architecture.md | 234 ++++++++++++++ 11 files changed, 908 insertions(+), 492 deletions(-) create mode 100644 apps/app-client-base-swift/docs/README.md create mode 100644 apps/app-client-base-swift/docs/en/README.md rename apps/app-client-base-swift/{ARCHITECTURE.md => docs/en/architecture.md} (66%) create mode 100644 apps/app-client-base-swift/docs/vi/README.md create mode 100644 apps/app-client-base-swift/docs/vi/architecture.md diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate index b83ef307bf469d349b13acc693cb8df742ca7783..0e6d5ed249bc04ea313d7ccf3276d741ed08bf0c 100644 GIT binary patch delta 21999 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∾j!Uq;Thps;Zwt>hv$TshlB80;dSA2!dt@I z!rQ~QhMx_;9{xx8gYd`U&%)n?zYqTufka>t!U(Mh^9ZX5=ZNtUkr6QwnGw??aw7^N zN+Zf5Dk8)Y%@M5;iz9j>`XZJ`tch3`u`yzE#KDMD5qBc)MLdjn67gId@haj?#QTU( zkw_#SsTHXcsTXMwIV{pB(j?L>(jwA2(l*jQ(mm2Ea%|-INdL%)$mqzp$i&E$$h63e z$gIfX$eEFqksz`*vLSL#WOL-o$X_BaMP7@%8F?r2LFA*zr;*R2kSIJ#IZ881J4!EV zXp~8mS(IgzO_W{Ks3@N(acopVRAy9BRDD!qRD0CCsGg{0Q7fa?L~V@P9JMWKN7Rw1 zb5ReX9!EWkdKvXP>Rr^wXr*W@niow*>qQTZ9v*EJJtEpP+9KL2+9ujQ+A-QWdUW)- z=n2t2(ZSI%(ecqq(W%iH(OJ<`qo+rgMT?`WqHCgOM>j>!jc$ux8+}?FeKq=4^xf$D z(NChEMZb*xGlqx}#Hh#U#OTKii!q8ZkFkoejd6%^it&n>9FrK65;HAkM$DX;mY9Vx z-7&o}{V}Uz*2Juj*%-4w=0wb$n0qk~W1hr3k9igICgy$2r&uHwkLAbe#SV=f9%~dk zBGxq4BGx+AF4iH|Io36HRIE57HaE5)wm7yl7R1huZHk>6D~+8OyCC-a*oCpnW7ov4 zkKGizJ$6^@p4fe{SK^R3HcmB8BTg$$FU}xtc$`t3eVkL=xH!K!|G1#I(72em__(CF z)VTDxoVfD1mbkXK&bVc9JL7i89gVvXcPZ{l-1WFyad+eH$Gwhw7xy9Vvp61&C*t|> zWV~Lydwg_!dHjO-E%AHekHw#gKO28O{`dH+@i*dc#XpPxl)z6ACTJz-BHguIUzV9EFm%>E+HWyIUzNnAVHGQk+2}4GhuN;cS3JMf5M7{)d}koHYWU( za5Uj~!ij`a31<_|C;XOhDd9?j_*%k^ga-+a6P_i!NcfOQBnlF#L?%&`sGO*pXpm@> zXp(4_Xq{-6=$Po77?xO?Sd%zAu_ zN790%&ZNaj-ATPk{Ykr$_9X2~I*@cI>8GTlNyn2;B%Mw=n{+PeYSN9Q+evqmo+iCb z`jGTF8BHdV`N?E5ovfLxn{1FgJlQ1KEZH*IIyodcH@PBtR&rhP?Bu58_T>4=-zP6h z?n+*kygB)&Q<$TI-DK}DXr~Hv}KjlTrhm=pLN~vfnp2|yAP1Q`*PSr~to@$&rGSw_~qBwPG zYH4akswA~KwLW!rYEx=U>f+Sy)HSJ_Q@5q=Ox>M&DD|h*qp827o=p8M^-k*B)DLMy zno62cnn{{N+L*NQX%o}@()`nc(n8ac(o)mX)3V^gTuxeET0xp5ttPELtubwGnlx=* z+JdyDX&ch^r0q{Tl=f5F(X``f7t`*hi65ptNqe65I_+)RhqO=WO!|=Yq3MR{BhpRN zZPM-2ozh*?N2U9u`=w7#4@{S(uT5W{zA=4Eh9E;dV{nF6hE7Iy#>|ZJjEW3##+;0% zjOL8F8H+L&XLMzBXROIspRp-pYsQX@T^UC*j%FOsIGH&yGb}SaGa@r8Gd(jab6RGO zI5R)9FcV}}XVzraXFkY$lleaLQx=kiXYsOBvox}_vUIZavxa6_X1Qf~WR1=mmo*{F zH*0cMbXHteVpd94dRA7})U4@QEm>=_wq~8pI-hkh>vGn$teaVPvhHO)%zBdbJnL1~ zo2>U)pQb2Hai3B-WzUp9r+Q8;pL%qfV4B}F@$BiD(-%&^m+h84F?(`$V0K7$WOht; ze0E~CG}%PNvOi>h&OvjC96=79!{#XG7~~AgG0GW{ zW13^0W0hl*W0x~3$1`V4&bXY(IpH~xIng<>Iq^A(In#6UbBc0Gb1HHqIn_BeIkKEX zIj6-r=W{ORT+X?Xb35m5&b^#BIq!1$xooa-u3GNkT)o_(xrVtTa!2Oc=6dD^=Z59R z=T6P7%mum4xzgNux!>h3&Rvq*le;u`Q=TACJ#R>!Zk|D&QJzVjS)N6nXWpc|n7sJB zq`cI;jJzp%)AMrk#CafZR$gshLtbNEbKcxMaa&$z-r~IOyq>&Od7JaL=55d0mA5-@ zZ{C5tlX<`9UC6tXcP;N`-krQZ@`-$te20A3eD{2>{PFn{@_q9s<;UbF=1eYS{@eTy`JW5$0$u@GKo<-t7+zpp zFtWf*Twq@iR1jJaQ4n1aSCCkcQjlJdRWPj}ry#$esGzi9WW1|7jPoKl=xoK~Ds zTv$A#cxG{BadmNRaYJ!q@q*&-ix(DmmFSh2l~|Y9l{l8Tm3WkRm5eEgENLlOSkhG@ zD_L5yOkA>}WOvEYk_#o5O0JY#FS%86x8#1w>ymdRA4`==u~J?sSxT20l};#4EuCGu zymW8r@zS%U=Swe^UM{^=dbjj`>7&voGlFNVE>kYkD;r*BTsE@IvdpH;zRa;~QdxZ2 z(y|R@`^pZM{aE&M+3~WIWoOFHmEA7;qwGQ1x#D(A1XdqB9(Y0 zuToH{Ua48BU8z?&v~qZ*cV$RrbY)y+Vr5F@l*;Lqxs?T#MU|D6jg^ZlyDL{zZms;e z@_6OB%HJw~ue?%uxAJM_i^^A(Zz|taeh`ylR;(;m6Kjce#A1E%P_ebxT|7oSUOZ7e zSsW-15r>I0#D(H&ajm#PJV)FjZWDKi7l`}CE5xhC>%<$xo5Wkh+r&G>KZ=iuPl!*8 z&xWr?apU7{%&BGHkIl$cAbB(@R!87m@{0ZJvu~p-%f~&%+BCBGm;;Ry?3adcX?5d`!xmD7tc~xCivZ~&y{;K6w zE2|Dw-KZ8+>sOmsTUFauJ5)PYyH@8^i_5D)^{nc;>N(Xd)os=7)qT~ws*hA(sJ>Kv zrTTjHt?Ikg_p2XOf2jU63z>z@;>{AwqGvI)M6>K>1@tQk_HQ=?a7R%2OXQ)6G_RO3<;Ta#6jTT@U|Tr;CaQd3=1Thma}ShGo7 zbElSHJE&H-*1FcNc2uoTZES5q?X=pw+QQnB+Va}UT2NbE+g96A`+M!Z+K07IYM zs(oFD*YWBEb#z@#U0Yp$-HN)^b?fRj)@`ZVUbm}mPu>2yLvYmm;uX|bdv0kYjt0(H&`oZ;D^o?c$u0L9TrT%*T{rcApR0Gp6q(Q%7Sc6f6X@fl zZrIuIL&L+_+Oz#;ht7_e9X&gC_Vn2$v&&{z%$Cfqp8dWNZ&YqnYaHA-q*1p~zi~w4 zsK)V)X^qnw=QPS1w>F+?ywZ5P@sGv_jgK2&HNI(l-}q^c(i~=vXpZt6wK;8by5@Ax z>6z2lWY}cWWY^@-*y54lF>2A~griV>$n|aM-v#?pz ztkSIBtl4bTY|?DjY|(7pY}f42?9}YlJhs`pd17-&b8K@$b8>TP^YrGj=89%Xb9HlV zbA5A1^MdBi=Ecq3%{|TQnty2C+kBw;aPyJoW6dX;e{a6pe53hR^WEnA%@3O&H@|Lv z*Zi^hv$#djLbtFjgIaW33|fY_7`KdUF>7&dacl8t8Qn6j#k(b>C9x%?CA}r9Wm-#4 zOMZ*ErK+W-rM{)HrMacGMcT5YWogT@mX$5*S~j+9Y1uYceQx1gFt>JY!`wM@Tj#dV zoj>=xxhv;>ZWXp_x9YVHZ8dBi(Q4Xi(Q4i5**dyaJhs)l)u+|3b#iM!Yfx);Yg6m0 z)|0I-+cetj+PvDvw@qyGYYS`(ZVPLRXiIM^Xe(`-*;d(B-B#Py(AL30q5GU~DwcR6%9ce!_aQBh!W8EjZPj{c~zR~?!rX<5;gp4m! zlMR+>$#i7;vY|3lnT5^@tOC3VSMg>Uw7P{Md7{=SI)Hp65NE zd(mE^SI|rMGQGaNA-%D^3BA*LLGP^Iy58BnO}%q_rM>fdzw2Gt+ts_NcT4Z~-krTa z^zQ9F(0jP|Nbj-U6TPQ;fA4*v^pEQI>>u4frGG*H!T#g@C;ETyzuAAM|6c#Y{ulj!_P_1_(En)} zv5Z}&vC3$b$ts&wo~yi9On!JtdnJ#>n&lr3O)cYEz_z1Fq6yWcar z;Pv;{=Y9Xr@4e5Radzg+;hfJmPt4z4*uidWdK_Y~HvR!3vd0`SM@)!0VV>A9%nJ*` zg0T=R6br+`u?Q>@i^Wo~iP$8p5G%rpu@X##m1EUd4c3fJ$6ByfOoGkAW@D?cb=X#H zH?{{mik-yHVt->-v1`~}>>l<3`+}o5hI4Tqu7s=N8n_;=j~n0?xFzn1j}YO$xE~&X z2jbCqJf4hC!YAWX@LW6(FTjiOGF*g<@k+b~Z^EbH5_}Fm7oUgE$Ghtfyp143ER@A!u~{UG&yr^iU>UPaSf(s9mO0CUWy!K) zS+i_dwk$`M8_S*L!5Yo-W{qL_vjSLwtRPklE0z_=^*n`;W zY+JS?+l@VvJ&HY=?avNohqL3^3G766CVL{gie1N^&Te71vS+eqv1hXvuotoyvDdIS zu(zvhN zUBns18O`zLOyKx)0yt5eI8G8LkCV?S;1qI-IK`Y2&Qwk%r;1a}so~Uerg54%(>X1i z8JwA%b(~F{ot#~qgPcR0!<^HcGn})WYn*>LcR5cv&$tAa%a!L2;3{wjan-pRTm!BN z*MjT7b>s@U9$Zgu1UH78%$>v)P3BJF7IKTY#oS76Ew_<7o4bI!kh_Sxg1eHtk-Lez zhx-@z0QU&@DEB1y6!#qW689?i7WW?aKKC*ACHF1&9rp_l;SJy^@DzD^Jbj)4&yZ)t zGv=A_OnGKJbDjmyl4r%U=GpL^c`iIJ-f-RsUI;IO7tc%QW$-dZyj)%$FP|soRq|?i z(|IktR^CkBEZ$1qTHY4kR^DFTKHh%bG2U_B3Em~%Ro+eB1Kva4Q{Fe;cis&@d4ar0{!QK>?~@P67vxJm%IEO8d`131zB+#hUypCZ zx8ytWh5Rx6@%#z=2oXPqpTN)HXY!};3-}_wnBU5u$)Cla$6v%>!C%SW%-_!6&EL=e zi+`AZntz6WmVcRlg@2EKpZ|dWkpGDPg8!2Lk^hPRS-=+v1eAal$O{Gt6ajfJGTLilVy9Ij$2L*=&hXp4DCk3YjR|MAuHw3o@cLa|FF9bg*jKV1n z#ieLUnbN0>DRat}a-zJc36wt-Kt)iIR1`ItnnGn!*;EddOXX4dR2e0rYN%QfRY$c@ zt<-F44mFpWNA*&RsnygPYAv;y+Cpumc2c{jebiyM`}1 z`a*rBzER()AG92e&?t@3I8D$jT7@1+tI}%pAX=T)pfzbN+Jd&Et!M|@krvXf^e}oP zJ)WLG`_rNTI*bmdqv<$0g-)e2=_zzRT|gJpt+a%0quc2Yx|8mrXV5e0S@djr4n3Ei zN6)9b=^nb5UQMr|*V60g_4EdMBfW{Eo z3f>qjne9bM&NwC^N$}SwiH?`HhOEJuGd5ICr3Z7tT*V*7pL#Gi%w7Ch^4!YaVL0Y1 zr?MCufsMpQVWTl`Yz*cj{v!S={wDq|{s9m2Ljg<0(&bdTu{3}X-B<=dSQ6bbGfiwVmLsRqi%r3@uxx;^ z0m1ByHq^}n}aQ6NREUo|0Veqviy}KB)AwJOX0CxPLEL?73QSMoHWG(v6+nI zY6k5ZfDG(MyIzKN8@7W%yB#2E-Plfm3}VoVu)WxUKcPK{9m4Vfq7D!ZfG~Aq#~`8O zzY`j1arxyK2d^SZw%zpI0bshVMdFuuPR+j;Livc^h z53C5gk3Iiifqe;~zJ|wJ8Q38VQv>E?`Y+gz4A@Tq(d!5M6*;f~DM3oGA4rL~MqC3B zefUqayj5T8@Bui^g3NIOAcoyI)IK9AbDYHKKQPDT@c|5TV}O{*nB&TFG_LYH$Atx1gMKedZ9TYkpqlT}P5uSZtD}P(eqoMl;yP01xVDtJ z8Dx$RW|*7*&fL(UzgLF1349ATVwhV(=C~=t+^U~BZiNf~H_UNoIepv}9&R$`)(mbt z=H&E~IX(4u5V!xCH>3LFDf+xVTiNBXU`haiSn#oRT zO9VMX0OHfH=XeSZJsflvIP`JQSvWJyGa2SCzcZItl$tc2#ZZ8bZ>YGr&wso8^xtP- z6JQ72rG5=B#EYO`8w=x24_*lU8sl5>66W=(0Pz$z57{GrD7h@O!2n(^5jd$qzt;}` z9Vqc|Vrf?)2j+{L`$5*?^>Sw2cpX4ScH>ZOMoIjJYPdEdYkTnNcnd&A17xhYwZC*5 z-UadE?RW>?2@r39jDel*#b?Or;j;kZD;5C6PqNd=N3R!O{HKCTV8LYo8OIbHFEMjA zv{-|0lrvj^uf^Bl>+uZ$2>?hSK!N}gya3;XZ$?V+tpEuDNF-x;NfO}fF=!8dfO%yv zz7OAz{{@gxsO$g<2S|kEs&lKwY5aAeoGDOca}iNgfWh;1dG~ zMc6b#0U%Q(gWb#q5UPZ_oLLW{MhpT7)Rr7cjGM2B7))5nnXM#-5PF0@VL%uXMuah8 zLYNX}ggIeBSOO#uAW(w}08$8$B7hVFqy!*PUrGT|1`rWI!~g*+32VZJutiE>TO8ma zB%BCm!i8{Uf_gbX+5vV4S|SCo`^@P9z#akYBOq`d!1dQ#x~C&!{g4C8h_S>tVmvW{ z@FxP$TSO2MOh6e{0Hg{aP>xXBpsF>AZv&(mAW+LBP$AKeay0r0*@Au{BE@pzI{>MS zk*|{M^%^O8>0=-%6Y?a}hN*MnWY4N4YlqDslVv|@W8{OR1!sAwb24Q2>Lt6pqRA<; zAB{2cwS6_2NIHk>aq?vMr@>k#OSbuvlA$BCImNP@(;4 ziMt6;5>p{B`95N(;xEN2WyRVg!lo(_QA4!JnXMpdi8`X5XdoJiCSn@VOiU+Qh*m-Z zkPd)A6Y2uU41ml82sEGB0GR`jxd535kof@VUO}`I9YiP5Ma&>(60?Ze#2jKSF^_=H zdH}K-pdx_Q0kjFAR{;7FV5tBr1XvBgb^{FAzkct?lc?cpVlAURFj463Ce{IDq1g6U z(3@pVULe`z9XMbcv4bhQ9UzOk379%8mUtAXs`Z&Zv6tA-JlzM7CEWzHx}}ona!oVh zFmVDJBXNW{N*p7O0|Z+4a)7J=$jSx8Nf_L<(P02t#dtn9n8WHw4wc7a0tj>vP=lcEYy!v@fNWbv zJR}|wkBKM5Q{ox%oOnUJBwhhz2SBa@#%Ys4qYx0Gc7T9r!DO_c8)& zW8|xUQMEz;QRa)R%=#GlroJ-%T$Y^d=Egn^GUl@g*`3WX@-;uJ^6*rUO!QOYvv{(H zTVv!)e?Ht}rSOZIv1nP5?Y|cJU#!K@*SNwW+XGd=_ZFU2^SV*t4_^6b^y7uu~}?3gYG>*KFH9qNjaL$hqxvL>2cU_c7iPrf0PE9 zsIwK>DpGiCWhuN*|Nddg+6Xm~2HWtzgU2?K!DE}r;IXY3cwhSAed~vZvVOv2Gv>>N z=KHk|9$P4d$988AgKcDcuss3t9Uwn?*k0`60F?tM2GeJKNm{L*HQSr*_a_!(+2bG! z6agsO$07iBKM;1GAEM6=f!Cq%S{PzFD1sd=6#+Yn5dn%r66{zg0+f)HhG+~*VyDVL zr7)mS_AieaP?Ok2|0Ss6K2T+B5nC*$&n}0cP;MV6Q~*#FfDVL)imLugs+u8H15jQ+ zDbIRnsO$!IBXak>tFz~@ z=gT0#U=AS+V0ziZKv0mp4Arn&%wGN{$Sc?@8OTZig<8an$JuKk*mV%BYnVQJBfQ=O zuT#Su25o2Wl2L>K08&)@rGAFuU+h!=CB@TBz<~75_o>@uhN3zXFc@^&Kh@3XUx?Qj zh}QwC(GT$-4+i2b_HE_>s3t(QWM>FbAg|pI@e%te1MxBY2|#rKs@ubU#(oaa!2s1~ zAf~Y2u;0rdzLP>6!gTT@15xjHW2TPvSNn~F{DCb8cX8A3lNuhd6}){&(mauKm>pazg(EJ&Y61Kz9VFun&4P*^>6^E9H7v}jgZ`qwi!^zX@HXkoO*zc z>gGV}9xc(288WDq(+=Ol>NssuVf*~sVVSUJaTfh|w!~T7C+FodTjH!{r0mx(Q?7Cm;NNX%bY8W>_Pw<+9$i~ zP+m8nyza&7vv0xc+wgiooavx@oQE=kFvFk$8vaZDQgeCE`Sf2B{M<+IyNny5DvQg2y$7FAeRl$s6K*RPaY)5CAoa&0BAHoV`OK@P+5LZA4RSrmkHopC9X0+ z;{Y1p!yU*~1!w|5lYa(Bt|nLa52(0sS>pWb#<;2*HsFW>mr4j{>yU)CU+Qj?Ei=F;f|9L zEUK@q3LD;G*_y8+$?UctiL(Z{$?}%$!GeTBWX(1uqxq-{zO#F1q{(VfWicf ziGL5=IJlkMu3sESAwWx*p{Y9@63Z4rxO2En z(B{tN&I4!>K#PCzA7Zn})X6!yzLOK;a?>VHicCz)Nytu{m^MkWI*A;%n7b4@7VZ*& zPL(+pSo~*+xJkMFe_$0Crs&YsaMws>TlNbJC|g(VX6|-5vtI5N?pE$LfQkVMy;o%~ zcL#STcNajb0Sb-3_CJR3-2Ku~BMKzrlcPC@Wj`w54@=3tWJ}Ho*^MfA!$LA9#Vq_Z z_l%qw9D4uk^LZ}J>VLg)nG5I7e!X#xdjpRAxiG-jcXMw7wDCV1a98#x8YHJv#KH&M zhyVHV6YewS<);9h*3E^%efodC{6_Y2v!pgPL-d~e@#h2t_Y*){{%z$~?)RUw58NLB zZTEU_sU{qTQ(9M!pR@RBUkvuqTUJS?niqg@4-^d@1`C;xS z8}|Pt8}P%0BOY9Wscv*~VSu5$CGErT^W0G`U8v}N$6l^C07FyP3Ww0DP~Km>L$CnLk9Sb&K=(1dJi_#Hzhq3d zmeon#*+1bu$2-s9JpfSnniTJ4NbU+G*Ojf$z6P(a!|T7YO$YtMyCb6rCmJBd!@tz; zR;m)od&G1K4xc*uJN1k)k>|V@yqCOJyw|)pytlk}y!X5hypOz3ywAKZ06hj!XoV*L zdJ>?g019)CGXOmc&~pGi56}w$y$H}t0KF_J$*D0V`6LC6n}mPj?`{&d;M#xq2vSMr zBd*5Cm;9VMn~|#$N)9A-;7mWMN~)2ANOe+!)Fib?ZGb{gbpxO`0s0R>Zvpf+K<@zb z?h;a$983-&^+OZs ztj)9cBV9<>|8Q=kJL$`_ClyYKo0OQ8=$)H1(R+A~tcsE3DCilU zN!M?Xm87@CHs53z=}Y?kXN}{@064ji-Xi_s=j!BQ-OWX>=2BeT_OdFvYf9NLD0s4`dw;fJSBJ-fvBqx(o z$Sg9O%pr3D3SI7JfI{(o1?V?`eqTi9lLhEuOq(o54+HcEz&rploY|0o!CAZEk`orY z?8!>94sJ^ztH^4yhO7mc9GrXx7z!|K0a;Hrkd0&$z;J+R0L%zrc0czfaDT3CNjD_? z+@65N$p74&Fjy9s$u^Q%SSQ=b4uBB=WA%_-n|gWv&-1Q;J+0)WBg3VJaKEm)QSkt@kn0F#HqTG?zUrU0-(l5s7%k{WA0 ziG8Jlq@jeA_*V{=yt3Ao0;VOGK`4_+x zyUBw9Q-X=3`QW4ENob(tG4eQh0$|DjQvuk(Uh))q8X7333NSS|u~{x6FUis5<=@@k zpLREpS7F&}znA^PMut8wOx`4KORb!|#aKC}-U9^-XVjrM2S|Ol1ehBV|0DR)ESy=0FJt&}n34XH!k_6EJU;aYYJ8e6&rll-upu(H##fS~ z`O3ee_|xh+Ulo>B`@QV%tLJ+CE4h3PzP6MbUrWkOA9BM7OSzd#pnLS?>qBMW8vx9( zPZ{{eeLnwxY#8Y8uNB``ikNRBMQjWa^X;XGO_->Z#CPI*GK?85?tz@UA>;Y|8Vt1= z3J+q2jJYt}^S$Ks_#+t3{Lu_sxRC*1Rx-{u|1$RB!!dm~-xpwJ-TbitGnX;;=La*4 zpBwaz4!{z9`w;qjbcBB#1}OefKGd?| z02|T6KLG<2HWCJ?PzC-uJ{*B`^UnipR5$-3z(&IerDggz|0Y~m^Vtz0ULi!gLV+TCu!wJQ1{u6+W?dC(bHBKU|a94lDf6FMzYd#F4 z;{i6IhyRZM9$@|eOJVYBU;Y;X29<>WmH&Nj11tt$u>gzf6_^Ul72TPWK>Ubl;NWo}`SuhGWkY3-_o=o;aVCivrVEkrk6AYuMa3mn#9dH*r2VP*i; z1v3Q9!W>p7fD3cbWfc4}&VbK71wDcV5Up&4ELaGzqMxHv!4km=IHDIU6)Y1h2Usz{ zN&q&sSFlpBN&rPu3a~1GRZF(lb=YiVC~N{)nYj79U@O{!PKQn0CfF|6A=nv~la!g1 zz)U;<3;;|FFcH8i;fT9X@}+*jfV~1{ZCfzRlUcm*yMZqP2H3Ccmu&)19i)%8qsDqmtf+gvV zcAQ(X8x7JMUmJ}@vK&otS8yM;LAHB90QZG7{mbC7;3@NHAG38p0L^mRzmHxDKEO1T zI0A?7a07vK@D4}r%=mpdvx`7BcqdK@J~0+A_zDM`$SQy}OU)i&EpXf^k|_}-2k}ub z^PDbj4!1~2%S}s}G&ws->XImec?MI9R=73FLr=OFr+;53#iRH?+fBiE*Y>kv|JR07aQ_3QO%3jAct>BuJDEubS=$W$ZMW=($-j;5^PiLj z1vmF}lhH7}@20E)Hsjxh*oi0y=9x6VryKz`^WSIA6da!XzjAzP80E#}_|!<4<72Y` z1~*U0a{OKJ;@`@~P|UIb;a(b8-6-~kUhQ(A26-&iY@l*npNF@QR2VlJb zgJBw4(jtH@2H29tavSCJsWd8`$#kIOb!QyzQh+U&c(=S#rwXWIseMv#UlX*?Wxu#S zXrCG|>#Shf4ybZ~tpM1{9;%Xpb_X?SjdX}j)l*GOfd<9_z=(3XN8kY^h9!$vGY((~ zC86K|q?c-=+NlnJtp(URfUWPPx~Lh9kJ$jQjZ*JS&6lI8Zs=9kN%YwZ;PpayeM4fZ zzJ!9yeGCm4&Iy2R?x9vtD*?6zU|Xd$oZFlStfMx-z(B1B*tTv8M#t@vsy1!zHbxt^ z18k>cWt-4>_wTx~pMtyLx+!S&d%CHE0NV#e_V1=7>X^)2_e!+ey?jqna8>Y^sXgi} zbrCuQ>Kt{Rx&Sa(2dv<5FLjB!OkDxkQGh|W@Xvo%ctcj<0m-WN(VRQ7ABQC0+w;kX zvL8ocN)j-dP%(k7)%0S0&@ajCjoW}V5b*RZ>YD_ zJL*04f%*urGXOgauyX)|2?1=>#r090J1{SrO~XloMKp)z(ma}^`82fE%K(GT{2O3b z0d@^w*I`X)BlV6}pcSPxi4RCCg4x4OSw$wu`s&%m=tx?dHu&GRi#DN6|D#>E0CpQz zC$?Qbt1AcX-ylU>(=e~^rfmRrx0|*D*geVFZj*T035rZ^fEfL`<&qvsyF-aF_qu5h zfIaN<1hf~jR#viqyCpq}_JL|gkEXq8nEN~i7|d**_R_wzA3YXe&j1E#zLP}vG$_!4 zbTF)x4g%QoZW=oG7m}!6)iHF098E{kQ6X^0d~Vvrq#>S%8lS%}Pk@^LQaTku$I!8G zE9vmK+@u~lMy|cjwM4;>pI``urE7M;V?52N$@ZaNoWA0*@g z50SLFg>;mxnJqeMu$ldDl+e|3D$D4pbSYg%i)b+o=yJM(uB4$Me*)NNfPDejSAcy3 z*mr>a0Jt2$5rCs`x{j`8auT|MZls&&X>>C^9gdQ4jLGo!~Gbc)Es6tXJu4Q9nfi zr5o2vU!||hwaa83*AMk34TJU~*r;3dZF&!Vm%b-$nihb2#c>sY>i}GL5&Zydq#x0b z(MGfpz@6g<06qlZdXknEt4yR4dn=RJyPWN=IFwj);N--(BAvKg9cwFRJ8NlFqd!X0 zR%(b=aW-)F!sW>GoU8DYw%hQdwnv;NoadaEaIMXP>%#Trj^&Q$`f~%h!Q4=8I5(0z zkvkc#AmwoLxCQWQwi0eBSHxY-J;c2T=jT1(*JfqB8PX5Sw(<`0uD}n;uES5sZt?E$ z?!gbr9`T;=zQd2nQ1~Gk3w}h#6OjUvCKX5}IQ{2LjwR#BJhB!}D=i^+!BN$1@-Fmm7=d`dniUy`rMx8!^BBb;&=06+9nhM#(=!H>N(`PzJ4_{o<(-;f{3uZCZB?S|iP z{eWL&4Hu*c=D>XIFifz{!KCUE{GjSKlNSjd3SbHZKc^B6q^w{Bg^`d7hhI-6!EdLE zsA{SK2A0*-4r({Gm->r32tSLuLp_Clj-&+x?hSaZFi=5DVU)rOg^dcw6wWH#RCuiL zRN=Y8ONG}8ZxwzhB8r$Ip~zO`Dw2wW73~zqDV8ZVDb7@!qc~5oM{$ATBE>C=2NVx0 z9#uRcQaq)2M)9KJ9mR)=j}@OPzEXUn_)hVI;zuP!iBcM%q^P8#q^dMX$xz8e$xO*o z$y&))$wSFYX@t^fr7=prN?}ToO3_MjN(o9yN|{PUN>i1}lz>u&Qk7Dxl0>OZsY7X% z(p9C0O0SgOD7{nqp!7)@RmPQB${b~$GGAFuS)`$?rL3zwL|I?iLfJ~$M)`sATNOfu zt-@6yRRk)u$^aEb6=f9z6(bcB6*CnJ6)P1R6+0CN6`@MJO0~*Tl`{hc18oKd4Qv{? zV&JKP?^Fk=8mii;+NnCI3RRs|T~$Y^daL@V`l*gnouC?^nxxvSx>@z0>UGt}s?Stk zsJ<4dzEyp%#!*vNQ&AhJrlw}C=BDPU=A|}LZM51LwGg#XwJ@~^wN$k%wH&oPwF0#w zwQ{vewQ99mwR*KiwHa!&)#j>otM#faR9mICMs1z?TJ>G($J9@#pHe@geop;@`X%+d z>i5+jsy|kLs{UO4rTS|Pfrg=mr$(g4RE;u?1`Sb%MwiA+joBJ=HRfw9*I22sT4Sxo zdX0@5n>9{pe9$!2^wrGPEYYmjY}A~lIbE|=vrV%@vrBWP<_66pns+q6X${n}*7DH` z)QZze)taPLpjE6jRjW*^Q)`*lR;@EyceUlTQEgnCrOnaiY4f!`w7s?cwF9+-wL`VT zwIj9DMB2I9#oAM~%e2MXFd@*MtG!cupY|>7=h`o|Uu(bB{-*syhp$8F$m=NRXzS?e z4AIfo8LJbb6Qh%;Gf^jBr(0){&KaG*bsp)w)cLHdpsTHGq-&yUrfZ>VrR${YqB~UA zUDs3BOLx3(fNqd(sBXA!q;8UKif-DF?|P)3yq-uwPf1TrPhC$_Pg~DPZ>(OPUb$YC zUX5P8UZdVLy=8iv^>*p)(c7o@m)$Y8O-8iTb4>kW1o>@?VIaM<92!DEA`2G0#%8oV}mYw+ISqrqo`uLj=@6%CaQ z2O6pwsvBw=Y8wtV)HgITG&M9g9B)`^INxx&;Tgk6hTn|%MuUw;82K9|8xUXoF}Y{*z~qt16O)f7pH04+d^eRdMNM&2mZ^cMjp0^UbeW47RYeu(J?3SO_hKTDV(yT6kH6SxmQ> zV=>>N$6}$yVvD5~J1ve{T(Y=gan<6w#Z8M_7I!ROTD-P+XYs+}v&A=yAC`!vy5&&I za7(e}Jj+d%`z?=Kp0qq|dDim0<#o%OmbWeMSw6IUY&FnojMYkOW$VG#hStW`rq)*0 zHrDpmj@I7RG1i$P>k{iyYmqguuCT7MuC;Eko@qVDdY*N+^#bcf)=RCITd%Y}YW>_s z-p0-*z$V+K*`~{8md#w7`8G>!*4b>Z*<`cDW}D3pny^y+m^B& zV5?+1&{oS<*H+Kg(AL=2-?q|rq3tT$?Y1Xv&)EKLd&~B|?F(DcS3BBH!A{-I$j;o( z%FfnKXy*gk7Lrh}~Sf<#ucA*4u5g+h(`ZZjaqQyYqIp>|WZvwtH*$-tME_ zXS;9qa`v?S0DC2S6?-*%b$cy)9s9xdruG*0*7mmcuJ$ABN7;MZ``G*0kF$@okFk%p zPqI(7Pq%NfpK0H1zd&Ta*nXM)TKf(5o9(yR@322$f7bq<{X_d#_TL=_I4C*jIv6+@ zJD53GJJ>onI0zj^IgD`#afoz?afowBc1Ux`beQBIc4%^#?J(D2zC(}00*6HoOC44? zYOfMc*@m}9JCf@88{nq!7zt|M@4bL@1S@3_)&kK=yF(~cJ$FFRg! z{KxUO<2}a*j_(~m30Xo?ND1YI%0gA4x=>SSDRdS32>pcPgcF1T!XRO&FhZCv%oI)* zW(jkJ`NAS$iLg{yEfUrV8->$^ox=IT9^nGvV&PKZa^XthHsMa;9^roBLE&NH6X8eU z4=2=#aN;=8P6|%SPO46WoQ61AIe9sabQ4VdEXUv)JOgk$$D>-XA4|n!+79~2TIg6d^oa>z%oo6`D zcAn?l&QF}5JHK=O==|CFoAVDBfy*Ela~DS! z4;Md|K$j4gaF-~TM3)qoG?z@5NiJfSI+q5QE|+eX1ulzSmbt8S+2pd-Wrxcym%T3g zU5>k4bh+j7$(7|wi(D04m0eX`)m`;m4P1>}O$EJ&T<#H z%eyPOtGH{r>$neg*LOE`w{~}SALkzF9^)SGp5&h5p5|WT-sV2TeYX2N_XX~Y+?Tj7 zbKmEF*Zq|T--Gs0@KE+p^-%ZF^3e6r^Dy)<@i6zW^04(7>oLJ2&?Cen%p<}h+9TE@ z!6Vrt%_GxevPYIjj>j~S$8wJ?9ydMNo@$4W^StKy%JZ$~2hY!* z--gKz!-f&VIK#+c)G+yBn!~h*=?>Eywqn@UVcUo89Ja^H&nw(3(kt34&TFgJL9fGJ zN4-uA*B)**++w)ZaNFU#h94V#V)&`yXGg@0$Q&_g#FP;^BfUokj|?3dK2j7l%6gRB zD34LYMvWM?f7Izwk48Nm^rW}_`f+l;m! zEgbDK+HJJw=;5PBjUF@Fcl6xRC%sW`H}3-P9`A=^^vA@E=^S&`N8U%%N5^M~kAaV| zkExH)$Jxi#$K9vFr^{!S&s?8wp9MaPL_Rxwj`$q+IpuTK=Yr2ApC>*aeGy;Wm+i~* z75J+7s`;w>YWeE;4)%5R4f7TGcKG)CF7RFGyV!Sw?-AeQzNdW8`d;w8?0ePshVL!k zyS@*6zxw|0L;VOpjvwhq`3>+>@*C(k$WO!1*w56@+|SBytKUJt!+uBoPK=Ejn?6>Q zId;<6tg+w6k>dp8=y3|;W{+DsZuz*C=FM;~$NGI{wA@ z*W=%f|2Y23`0o>t3HSuH2^tf$Cg@BUJVAeg(FBtTW)mzYSWmE>Fn+@136cpLCtR8E z!+(hXF#kCJGXEO?djBT>X8$h#ZvO@Ti~X1Ruk>Fn@?Yz}-hYq(0sq7P$NW$GpYy-y zf7$ASGZ@KvqC*KtVuR00^iIs0pYGSQl_N zkO-s#)dMX8tpl9{M+JrlMg=AZ<^&c7mIRgsRs>cB)&|xG&Inu-xIJ)J;NHN$0uKcq z4LlKeI#6^j@M7SVz~_Ol0^bC_3;YoHIq+MMTo4*W1hIp-K?*_2L8?K6f`$Z{1z85! z1lb1(gPen0gS>;r22BVG3dn*e2K|cyw@tC^$MeB{(fOKe!~gEEoh=2iFES z1UCiG4qh6(D|m14U%`ihj|QIzJ{^26_+s#t;A_FJg5L(e5B?bZIrv+MTnHM1hpK4AvPg)Ar2wJkP#tcLi|F;hXjR$hD3x!g%pR(30W4hI%HkQ z#*l3xJ3>UeL-vN84mlU{PsoFi$05%`UWR-K`5f{sR4x<^B|}w1O+zh09YcqQhJ=QP zri5mOP7ciuEeI_Nof=ve+7vo7bam*u(2b#6Lbr$R3f&v}SLmV8qoF54Z-?FseGvL6 z^hxOR&{v^vLf?gc4E-GXHH--3gppx_FqJUvFx{{rVfvyl!!YA8hcK5gw=mDJkzw9p zzF}j-vcjf?%?j%YTNt(^Y(I#aV+9& z#D$2<5mzH_N8F2e81W?HS;YHDEK(&>Epl+AW#q8P5s_mf$45qqBI6^IB2yzLMox~* zj?9g$h@2j|Fmg%c^2k+@Ya=&AZjRg*xifN4s^_)*GHs!{4uT2Z=DL!$Jf45RF#oT6N#+@pp^jfxr*v&)Rm}fQ8%ORM?H#q8ucRTRn+HbcJ!cV&1i#Y z+vt(e-qFF)5z*1nanZ@qsnHqH6Qid_*G12d?u}j)y)=3S{J?!(^v38d(c7bUMemKi z9DOzVdi2fcThVu;A4ETneir>A`gQc%==U+YF(xtQF;+1)F)lH~V@Ab{5ykk$jF0h; z35*GjNr*{}$%vU0lM|C4QxsDY(-pHl=5Wl3nA0)mVlK!09dkYAX3X=LS25pXiC9i7 z8B4_~$EwDv$7;pu#2Uvs#Cpg2#s`?vC9XyFd1F?1wm|IEOfqf83O~qPXg~hPY{QEpZ)jGva2)&5c_gwshIF@iZ;Yq^tgfEG5iC7{lkxUdM$|ou$>Lyww zdL@ob^iK3m9GB>y7?c>A7?BvAD2hwWPb^9-Ni0niC6*^vCDtU?B{n8bOProKBXM@( zyu|LrWr^z(HzjUO+>y9Dac|=O!~=7;W>7n1HKJrE_m zP5PAdHR(q(p3F+-CX>m7k`0sHl0B1$Cyz=VlkAr~J~<#cI5{jiGC3wWKe;HmB)K$M zlw6)%m0X)#pWK+-oZONuNuHfNFS#dqLGsGvP03r5w7& z%KVh|DVtJuryNhYnQ}YjMatKdAE{_6k;+LWQ>j$-RIOB<)FG(`sm7_MsphH9sqU%6 zQb(kYN%cz|pX#3)lbV@YlsYw4lvr*$SZb?0qdR~-z zHT6d7t<<}zPg0+!zDj+Y`aTUyBhuJuytK%)w6qMkp?ON$zO++mXVT84T}(Ghw@-IS zcT9ImADKQXeRTSm^pNz>^sw}Z^wji>^hxPi>AC6o=@sc!={4!~8E6KbA)hfILoq`q zV@QTUhH-{jhDC-;#?TD+jA0oIGuCB@HfC(e*q*T~V^7A3jMEwCGA?FZ$+(*FFymdu z$BZu--!qX+Jd>TNlsPbSP^M<4PUetI{Y=A5@66Q9tjuYdEtzeZotZN;=VZ>$?9E)1 zxioV{=IYFKnHw{=WNyp+Fi|+MXyTfQpC&m>s+zQKGBJ7hl)+Pir}R#FmxX4rvv^qo zQI zJL_83&8%BlceCziJ<58L^(^as)~BqmS>LnS+49*6*-F_e*{a!tvJJD%vMsZ1vK_OX zvxjE8XGdm>vKzBovfHvdvu9_|%kIwZ&0d!++K|0B`*8NL?33ANvM*;}&AyR+EBj9N z)9g<<{2V$*HAg>3nB$T&I>#?(d`>`4Xij)eR8CCJq?~;@r*h8cT*~=7=Vs3BoO?MB zaz5pd{at(7$a-DKra@}%0a=mg#p}!o1a^lTbx^(Tc6vU+nU>+J2Q7q?)==I+}*jib6@1X&Ha%3Irm2%l85K9@|5!i z}Im?FC&0OAA&MtS(qru(4oE!S;e(1$ztj7hErRRPeQMV4;4Yuy9Oa zXklDoyr?j-aAM)4!YPH*3uhEADqLK+q;Of`=E7}-I}7&|?k_x0c&YH8!dr!R3!fFf zE|M?OEYd2{F48TsD6%cGFA^3FE%GSxDjHGbUldprTohJxx#(We>XWH2 zr@oo`e(IO0-%F8FtW>Skv^2X^R64D+rL?WIvvg+ZoYMKFy`}3)HEwZW7NHi{^=Y#0$ks#LLC2#B0TS#QVhu z#fQa5#V5q4#An3k#Q%uzi0_LZiJyv}g8@Jb=mR5Q3M_y<5CRwA20VZ_2nJ~&6Xbz% z&<<@|D5RIX4yuzXOt zX1R8`b-7!)U-|g*fb!t-u=2?AnDY4Ytn!@l{PM!`lJe4WQF(cJWqC_^TX|>s%|c`OWhC<&VmrmcJ-}RsOkxT`{Oav%;Xlwqj(3cSUeTL`8H(Tt#w4 zT195Xq>9pt>WaFG#)@ebofV?_6+IOTDi&2NsaRIAxnf7f?uvaChboR%oTxZeajW8a z#kWeiO01Gq$*ts9(v=F8x|KsJ^(zf4O)AYRttxFQ?J7MhM^<`Q`c_V;46F>P46977 z%&N?-ET}B1EU#>+Y^rRoY^`jo?5OOmTvWNVaz*9p%C(i}D{oaktb9`WTvYk0@?+(f z%I{T36;{QsQmZnnvaAwTji?H(il|DgN~ub(%B-4E)l;>wYDv|ys?Ak9s&-fHt2$J5 zwCZ@($*Q~6>eWWorqveJ*41{^4%K6-L#kt|6RMM|)2cJ8^QsH0i>phkMb)5sR`r(Z z8`bY>&>EtKQ$yBJHS#qhYDE4uAvNJOQ8n>3Nj0f8=`~YpT5IOjtg2aCv!P~l&9<7I zHG69I*PN|6UvsJEO3k&J8#T9T?$q3?<<=V1j;_tDZKz#Vd#Ltw?YY{EwU=ve)jqC$ zTKl~AW$o+Qw{=JzUdOKE)d}k8I-5H8x{-C>b-s1u>VoUS>LTl6>f-9^MRhysuGihK zdsEM<=hiFKYuDS<+t&}PA5}l5-ml)jKCnKdKCC{qKBInJ{qFjG^#|$?*B`4tQU7=S zwfYIG zY&hI-wBcOC#fB>lR~zm&JZpH-AbQpCrr}+~heo`S+sJRE88aE6rWZ^vnm%>< z=N5JguSL)z-=f%}+%mXDzs0DeA}g>e=em>f1W5)xR~cHBQu; z*qYLs-a4^$a_iJqQEPc?Rcmc)eQRgyg4V^Y%UV~qu4!H0x~X-4>%rC|t;btWwVrLg z(0Zx$ZtJ7gr>!qq-?n~e{oMLh5+>OqIUqSIIUzYMxgfbLxhlCXdC}(8Hoh&UExs+O zEwwG9ZBkoSTW(uLTUA?4TYXzoTXS1Wn@G~u-nOpoOxw$LrFLO^czZ#6WqVzFV|#OZ zTYE?QjP_aW%i1@$?`YrMzOVgI`_c9j?WfwGcF-Lf9l9NQ9flod9hM!|9kw0AI>vPb zcZ79BcEoffccgV>c1-Tb>X_MayyIEN`%bJ=rBkhQaHnPGu+9;k{+%J6;hj;P@tsMc z&eYEI&Z(ViI(K&N>D=FWu=7agvChk#w>zJ7KJR?h`KD_~msyu(mra*lmuJ`Tu2EfM zy8ODvbwzeXcg1!kbj|Hr-nF7@W!LKeQ={FzL=}Vq7%vMlLo%%B(whtdBM=nR#WE?> z$duT^kc6egsMy*RHa1SI2&SQEigV`Z>^l0+J0Ibkr07#9%09G=$`s2o=rSS-f)J!G zq?_mWPdwxSc}$*?7vv>*M`p+znJ1sgH}akQAivxlZlN1+i`^3Ux_j5X?~b_-X^@6! zm^u;KM58oD9U7;dw43%)LOsgpARVH!-dZo6a1EpkPkC=f-WSnLr&aa`1ku!xA5Xcn!aR}kR|CI-cjxFa5kNil`aI^2la zn1gw^7x!Tq9>7Yh!Vc`iBo3jE>0pLH<;$RH-Ugb*f&SSB`2`?W#j{sU9_;l8zcuLMdg` z6Sbfgb&k%}1-ej|>I!{OSLwq#s6+acKBL1rqA%)8`m%1;EqX-Hn^d#GWST6q&1^S0 zW~a$FyG+3BHYKLaRGS*xU{Bjd+icrxmu1%25qrblw72a&J7y>BEBo5MwSo6`*3Q|F j{tADqU+G8vxZm#g_ Bool { guard let refreshToken = refreshToken else { return false } - struct RefreshRequest: Encodable { - let refreshToken: String - } - - struct RefreshResponse: Decodable { - let accessToken: String - let refreshToken: String - } + // OAuth2 Refresh Token Grant + // OAuth2 Refresh Token Grant + let formData: [String: String] = [ + "grant_type": "refresh_token", + "refresh_token": refreshToken + ] do { - let request = RefreshRequest(refreshToken: refreshToken) - let response: RefreshResponse = try await APIService.shared.post( - endpoint: "/auth/refresh", body: request) + let response: OAuthTokenResponse = try await APIService.shared.postForm( + endpoint: APIConfig.tokenEndpoint, + formData: formData + ) // Save new tokens // Lưu tokens mới KeychainHelper.save(key: StorageKeys.accessToken, value: response.accessToken) - KeychainHelper.save(key: StorageKeys.refreshToken, value: response.refreshToken) + if let newRefreshToken = response.refreshToken { + KeychainHelper.save(key: StorageKeys.refreshToken, value: newRefreshToken) + } return true } catch { diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift index 327fd70e..825b6dbd 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift @@ -144,13 +144,8 @@ final class AuthViewModel: ObservableObject { // MARK: - Actions - // MARK: Mock Credentials (for testing) - // Thông tin mock để test - private let mockEmail = "admin@goodgo.com" - private let mockPassword = "123456" - - /// Perform login - /// Thực hiện đăng nhập + /// Perform login with IAM Service + /// Thực hiện đăng nhập với IAM Service func login() async { guard isLoginValid else { errorMessage = "Vui lòng nhập email và mật khẩu hợp lệ" @@ -160,47 +155,6 @@ final class AuthViewModel: ObservableObject { isLoading = true errorMessage = nil - // Mock login for testing - // Đăng nhập mock để test - if loginEmail.lowercased() == mockEmail && loginPassword == mockPassword { - // Simulate network delay - // Giả lập delay mạng - try? await Task.sleep(nanoseconds: 1_000_000_000) - - // Create mock user and authenticate - // Tạo mock user và xác thực - let mockUser = User( - id: "admin-001", - email: mockEmail, - name: "Admin GoodGo", - avatarUrl: nil, - phoneNumber: "+84901234567", - isEmailVerified: true, - createdAt: Date(), - updatedAt: Date() - ) - - // Save mock tokens and user - // Lưu mock tokens và user - KeychainHelper.save(key: StorageKeys.accessToken, value: "mock_access_token_\(UUID().uuidString)") - KeychainHelper.save(key: StorageKeys.refreshToken, value: "mock_refresh_token_\(UUID().uuidString)") - - if let userData = try? JSONEncoder().encode(mockUser) { - UserDefaults.standard.set(userData, forKey: StorageKeys.userData) - } - - // Update auth state - // Cập nhật trạng thái auth - await MainActor.run { - AuthManager.shared.setAuthenticated(user: mockUser) - } - - isLoading = false - return - } - - // Real API login - // Đăng nhập API thật do { try await AuthManager.shared.login(email: loginEmail, password: loginPassword) } catch { @@ -210,8 +164,8 @@ final class AuthViewModel: ObservableObject { isLoading = false } - /// Perform registration - /// Thực hiện đăng ký + /// Perform registration with IAM Service + /// Thực hiện đăng ký với IAM Service func register() async { guard isRegisterValid else { errorMessage = "Vui lòng kiểm tra lại thông tin đăng ký" @@ -222,8 +176,15 @@ final class AuthViewModel: ObservableObject { errorMessage = nil do { + // Parse name into firstName/lastName + // Tách name thành firstName/lastName + let names = registerName.split(separator: " ", maxSplits: 1) + let firstName = String(names.first ?? "") + let lastName = names.count > 1 ? String(names.last ?? "") : "" + try await AuthManager.shared.register( - name: registerName, + firstName: firstName, + lastName: lastName, email: registerEmail, password: registerPassword ) @@ -234,6 +195,7 @@ final class AuthViewModel: ObservableObject { isLoading = false } + /// Send forgot password email /// Gửi email quên mật khẩu func forgotPassword() async { diff --git a/apps/app-client-base-swift/README.md b/apps/app-client-base-swift/README.md index 1277757f..1493d92c 100644 --- a/apps/app-client-base-swift/README.md +++ b/apps/app-client-base-swift/README.md @@ -1,291 +1,56 @@ -# App Client Base Swift / Ứng Dụng Client iOS +# App Client Base Swift -> **EN**: Native iOS client application for GoodGo platform, built with Swift and SwiftUI following MVVM architecture. -> **VI**: Ứng dụng iOS native cho nền tảng GoodGo, xây dựng bằng Swift và SwiftUI theo kiến trúc MVVM. +> **EN**: Native iOS client for GoodGo platform | **VI**: Ứng dụng iOS native cho nền tảng GoodGo -## 📱 Features / Tính Năng +## 📱 Overview / Tổng Quan -| Feature / Tính năng | Description / Mô tả | -|---------------------|---------------------| -| 🔐 Authentication | Login, Register, Forgot Password với form validation | -| 🏠 Home Dashboard | Greeting động, Featured items, Activity feed | -| 🔍 Explore | Khám phá địa điểm và dịch vụ | -| 👤 Profile | Quản lý thông tin cá nhân và cài đặt | -| 🌓 Dark Mode | Hỗ trợ chế độ tối tự động | -| 🌐 i18n | Đa ngôn ngữ (Tiếng Việt & English) | +iOS application built with Swift 5.9+ and SwiftUI following MVVM architecture. -## 🛠️ Tech Stack / Công Nghệ - -| Technology | Version | Purpose / Mục đích | -|------------|---------|-------------------| -| Swift | 5.9+ | Primary language / Ngôn ngữ chính | -| SwiftUI | iOS 15+ | Declarative UI framework | -| Xcode | 15.0+ | IDE development | -| URLSession | Native | HTTP networking | -| Keychain | Native | Secure token storage / Lưu trữ token bảo mật | -| Combine | Native | Reactive programming | - -## � Prerequisites / Yêu Cầu - -- **macOS**: 14.0+ (Sonoma) -- **Xcode**: 15.0+ -- **iOS Target**: 15.0+ -- **Apple Developer Account**: Required for device deployment / Cần thiết cho deploy lên thiết bị +Ứng dụng iOS xây dựng bằng Swift 5.9+ và SwiftUI theo kiến trúc MVVM. ## 🚀 Quick Start / Bắt Đầu Nhanh -### 1. Clone và mở project ```bash -cd apps/app-client-base-swift open AppClientBaseSwift/AppClientBaseSwift.xcodeproj +# Press ⌘R to build and run ``` -### 2. Chọn Simulator -- Xcode menu: **Product > Destination > iPhone 15 Pro** (hoặc simulator khác) +**Mock Login:** `admin@goodgo.com` / `123456` -### 3. Build và Run -```bash -# Sử dụng shortcut -⌘R (Command + R) +## 📚 Documentation / Tài Liệu -# Hoặc từ terminal -xcodebuild -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ - -scheme AppClientBaseSwift \ - -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ - build -``` +| Language | Links | +|----------|-------| +| 🇬🇧 English | [README](./docs/en/README.md) • [Architecture](./docs/en/architecture.md) | +| 🇻🇳 Tiếng Việt | [README](./docs/vi/README.md) • [Kiến trúc](./docs/vi/architecture.md) | -### 4. Mock Login (để test) -``` -Email: admin@goodgo.com -Password: 123456 -``` +## 🛠️ Tech Stack -## 📂 Project Structure / Cấu Trúc Project +| Technology | Purpose | +|------------|---------| +| Swift 5.9+ | Primary language | +| SwiftUI | Declarative UI | +| URLSession | HTTP networking | +| Keychain | Secure storage | + +## 📂 Structure / Cấu Trúc ``` AppClientBaseSwift/ -├── App/ -│ └── AppClientBaseSwiftApp.swift # @main entry point -│ -├── Core/ -│ ├── Constants/ -│ │ └── Constants.swift # API, App, Storage, DesignSystem -│ └── Extensions/ -│ ├── View+Extensions.swift # SwiftUI modifiers -│ └── String+Extensions.swift # Validation, formatting -│ -├── Models/ -│ └── User.swift # User entity + extensions -│ -├── ViewModels/ # MVVM ViewModels -│ ├── AuthViewModel.swift # Login/Register/ForgotPassword -│ ├── HomeViewModel.swift # Home screen logic -│ └── ProfileViewModel.swift # Profile management -│ -├── Views/ -│ ├── Auth/ # Authentication screens -│ │ ├── AuthContainerView.swift # Auth navigation container -│ │ ├── LoginView.swift # Login UI -│ │ ├── RegisterView.swift # Registration UI -│ │ └── ForgotPasswordView.swift # Password reset UI -│ │ -│ ├── Home/ # Home components -│ │ ├── WalletCard.swift # Wallet balance card -│ │ ├── PromoCarousel.swift # Promotions carousel -│ │ ├── ServiceGrid.swift # Services grid -│ │ └── ActivityFeed.swift # Recent activities -│ │ -│ └── Screens/ # Main screens -│ ├── ContentView.swift # Root container + TabBar -│ ├── SplashView.swift # Splash animation -│ ├── WelcomeView.swift # Onboarding -│ ├── HomeView.swift # Home tab -│ ├── ExploreView.swift # Explore tab -│ └── ProfileView.swift # Profile tab -│ -├── Services/ -│ ├── APIService.swift # HTTP client với URLSession -│ └── AuthManager.swift # Auth state + Keychain -│ -└── Resources/ - ├── Assets.xcassets/ # Images & Colors - ├── en.lproj/ # English localization - └── vi.lproj/ # Vietnamese localization +├── App/ # Entry point +├── Core/ # Constants, Extensions +├── Models/ # Data models +├── ViewModels/ # MVVM ViewModels +├── Views/ # SwiftUI Views +├── Services/ # API, Auth +└── Resources/ # Assets, Localization ``` -## 🎨 Architecture / Kiến Trúc +## 🔗 Related / Liên Quan -### MVVM Pattern +- [app-client-base-net](../app-client-base-net) - .NET MAUI client +- [iam-service-net](../../services/iam-service-net) - Auth backend -``` -┌─────────────────────────────────────────────────────────────┐ -│ VIEW (SwiftUI) │ -│ HomeView, ProfileView, AuthContainerView, LoginView... │ -├─────────────────────────────────────────────────────────────┤ -│ @StateObject / @EnvironmentObject │ -│ │ │ -├────────────────────────────▼────────────────────────────────┤ -│ VIEWMODEL (ObservableObject) │ -│ HomeViewModel, AuthViewModel, ProfileViewModel │ -│ • @Published properties for reactive UI │ -│ • async/await methods for data loading │ -│ • Business logic and validation │ -├─────────────────────────────────────────────────────────────┤ -│ Protocol-based Dependency Injection │ -│ │ │ -├────────────────────────────▼────────────────────────────────┤ -│ SERVICES │ -│ APIService (HTTP) • AuthManager (Auth State + Keychain) │ -└─────────────────────────────────────────────────────────────┘ -``` +--- -### Authentication Flow - -```mermaid -stateDiagram-v2 - [*] --> SplashScreen - SplashScreen --> CheckAuth: App Launch - CheckAuth --> Authenticated: Token Valid - CheckAuth --> Unauthenticated: No Token - Unauthenticated --> Login - Login --> Authenticated: Success - Login --> Register: Sign Up - Register --> Authenticated: Success - Authenticated --> HomeScreen - HomeScreen --> Unauthenticated: Logout -``` - -### Data Flow - -``` -User Action → View → ViewModel.method() → Service.request() → API - ↓ - @Published update - ↓ - View rerender -``` - -## 📋 Coding Conventions / Quy Ước Code - -### File Structure -```swift -// MARK: - Imports -import SwiftUI - -// MARK: - Type Definition -/// Description in English -/// Mô tả bằng tiếng Việt -struct/class/enum TypeName { - - // MARK: - Properties - - // MARK: - Init - - // MARK: - Public Methods - - // MARK: - Private Methods -} - -// MARK: - Extensions - -// MARK: - Preview Provider (DEBUG only) -``` - -### ViewModel Pattern -```swift -@MainActor -final class FeatureViewModel: ObservableObject { - // Published properties for UI binding - @Published var isLoading = false - @Published var errorMessage: String? - @Published var data: [Model] = [] - - // Dependencies via init - private let apiService: APIServiceProtocol - - init(apiService: APIServiceProtocol = APIService.shared) { - self.apiService = apiService - } - - // Async methods - func loadData() async { - isLoading = true - defer { isLoading = false } - - do { - data = try await apiService.get(endpoint: "/data") - } catch { - errorMessage = error.localizedDescription - } - } -} -``` - -### Bilingual Comments -```swift -/// Load user profile data -/// Tải dữ liệu hồ sơ người dùng -func loadProfile() async { } -``` - -## ⚙️ Configuration / Cấu Hình - -### API Configuration -```swift -// Core/Constants/Constants.swift -enum APIConfig { - static let baseURL = "https://api.goodgo.vn" - static let apiVersion = "/api/v1" - static let timeout: TimeInterval = 30.0 -} -``` - -### Environment Variables -| Key | Description / Mô tả | Default | -|-----|---------------------|---------| -| `API_BASE_URL` | Backend API URL | `https://api.goodgo.vn` | -| `API_VERSION` | API version prefix | `/api/v1` | - -## 🧪 Testing / Kiểm Thử - -### Run Unit Tests -```bash -xcodebuild test \ - -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ - -scheme AppClientBaseSwift \ - -destination 'platform=iOS Simulator,name=iPhone 15 Pro' -``` - -### Test Plan -Located at: `AppClientBaseSwift.xctestplan` - -## 🔐 Security / Bảo Mật - -| Feature | Implementation / Triển khai | -|---------|----------------------------| -| Token Storage | Keychain Services (not UserDefaults) | -| Secure Requests | HTTPS only, Bearer token auth | -| Session Management | Auto token refresh, secure logout | -| Data Protection | Sensitive data encrypted at rest | - -## 📱 Supported Devices / Thiết Bị Hỗ Trợ - -- **iPhone**: 8 and later (iOS 15+) -- **iPad**: All iPads with iOS 15+ -- **Orientations**: Portrait (primary), Landscape (supported) - -## 🔗 Related Projects / Dự Án Liên Quan - -- [app-client-base-net](../app-client-base-net) - .NET MAUI cross-platform client -- [iam-service-net](../../services/iam-service-net) - Authentication backend -- [web-client](../web-client) - Web application - -## 📚 Additional Documentation / Tài Liệu Bổ Sung - -- [ARCHITECTURE.md](./ARCHITECTURE.md) - Chi tiết kiến trúc và design decisions -- [Swift Enterprise Skills](../../.agent/skills/swift-enterprise-architect/SKILL.md) - Swift development guidelines - -## 📄 License - -Copyright © 2026 GoodGo. All rights reserved. +**Copyright © 2026 GoodGo. All rights reserved.** diff --git a/apps/app-client-base-swift/docs/README.md b/apps/app-client-base-swift/docs/README.md new file mode 100644 index 00000000..357457da --- /dev/null +++ b/apps/app-client-base-swift/docs/README.md @@ -0,0 +1,33 @@ +# Documentation / Tài Liệu + +## Languages / Ngôn Ngữ + +| Language | Documentation | +|----------|---------------| +| 🇬🇧 English | [docs/en/](./en/README.md) | +| 🇻🇳 Tiếng Việt | [docs/vi/](./vi/README.md) | + +## Structure / Cấu Trúc + +``` +docs/ +├── en/ # English documentation +│ ├── README.md # Quick start guide +│ └── architecture.md # Architecture details +│ +├── vi/ # Vietnamese documentation +│ ├── README.md # Hướng dẫn bắt đầu nhanh +│ └── architecture.md # Chi tiết kiến trúc +│ +└── README.md # This index file +``` + +## Quick Links / Liên Kết Nhanh + +### English +- [Getting Started](./en/README.md) +- [Architecture Guide](./en/architecture.md) + +### Tiếng Việt +- [Bắt Đầu Nhanh](./vi/README.md) +- [Hướng Dẫn Kiến Trúc](./vi/architecture.md) diff --git a/apps/app-client-base-swift/docs/en/README.md b/apps/app-client-base-swift/docs/en/README.md new file mode 100644 index 00000000..0537cf79 --- /dev/null +++ b/apps/app-client-base-swift/docs/en/README.md @@ -0,0 +1,207 @@ +# App Client Base Swift + +> Native iOS client application for GoodGo platform, built with Swift and SwiftUI following MVVM architecture. + +## 📱 Features + +| Feature | Description | +|---------|-------------| +| 🔐 Authentication | Login, Register, Forgot Password with form validation | +| 🏠 Home Dashboard | Dynamic greeting, Featured items, Activity feed | +| 🔍 Explore | Discover locations and services | +| 👤 Profile | User profile management and settings | +| 🌓 Dark Mode | Automatic dark mode support | +| 🌐 i18n | Multi-language support (Vietnamese & English) | + +## 🛠️ Tech Stack + +| Technology | Version | Purpose | +|------------|---------|---------| +| Swift | 5.9+ | Primary language | +| SwiftUI | iOS 15+ | Declarative UI framework | +| Xcode | 15.0+ | IDE development | +| URLSession | Native | HTTP networking | +| Keychain | Native | Secure token storage | +| Combine | Native | Reactive programming | + +## 📋 Prerequisites + +- **macOS**: 14.0+ (Sonoma) +- **Xcode**: 15.0+ +- **iOS Target**: 15.0+ +- **Apple Developer Account**: Required for device deployment + +## 🚀 Quick Start + +### 1. Clone and open project +```bash +cd apps/app-client-base-swift +open AppClientBaseSwift/AppClientBaseSwift.xcodeproj +``` + +### 2. Select Simulator +- Xcode menu: **Product > Destination > iPhone 15 Pro** (or another simulator) + +### 3. Build and Run +```bash +# Using shortcut +⌘R (Command + R) + +# Or from terminal +xcodebuild -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ + -scheme AppClientBaseSwift \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ + build +``` + +### 4. Mock Login (for testing) +``` +Email: admin@goodgo.com +Password: 123456 +``` + +## 📂 Project Structure + +``` +AppClientBaseSwift/ +├── App/ +│ └── AppClientBaseSwiftApp.swift # @main entry point +│ +├── Core/ +│ ├── Constants/ +│ │ └── Constants.swift # API, App, Storage, DesignSystem +│ └── Extensions/ +│ ├── View+Extensions.swift # SwiftUI modifiers +│ └── String+Extensions.swift # Validation, formatting +│ +├── Models/ +│ └── User.swift # User entity + extensions +│ +├── ViewModels/ # MVVM ViewModels +│ ├── AuthViewModel.swift # Login/Register/ForgotPassword +│ ├── HomeViewModel.swift # Home screen logic +│ └── ProfileViewModel.swift # Profile management +│ +├── Views/ +│ ├── Auth/ # Authentication screens +│ ├── Home/ # Home components +│ └── Screens/ # Main screens +│ +├── Services/ +│ ├── APIService.swift # HTTP client with URLSession +│ └── AuthManager.swift # Auth state + Keychain +│ +└── Resources/ + ├── Assets.xcassets/ # Images & Colors + ├── en.lproj/ # English localization + └── vi.lproj/ # Vietnamese localization +``` + +## 🎨 Architecture + +### MVVM Pattern + +``` +┌─────────────────────────────────────────────────────────────┐ +│ VIEW (SwiftUI) │ +│ HomeView, ProfileView, AuthContainerView, LoginView... │ +├─────────────────────────────────────────────────────────────┤ +│ @StateObject / @EnvironmentObject │ +├─────────────────────────────────────────────────────────────┤ +│ VIEWMODEL (ObservableObject) │ +│ HomeViewModel, AuthViewModel, ProfileViewModel │ +│ • @Published properties for reactive UI │ +│ • async/await methods for data loading │ +├─────────────────────────────────────────────────────────────┤ +│ Protocol-based Dependency Injection │ +├─────────────────────────────────────────────────────────────┤ +│ SERVICES │ +│ APIService (HTTP) • AuthManager (Auth State + Keychain) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 📋 Coding Conventions + +### File Structure +```swift +// MARK: - Imports +import SwiftUI + +// MARK: - Type Definition +/// Description in English +struct/class/enum TypeName { + // MARK: - Properties + // MARK: - Init + // MARK: - Public Methods + // MARK: - Private Methods +} +``` + +### ViewModel Pattern +```swift +@MainActor +final class FeatureViewModel: ObservableObject { + @Published var isLoading = false + @Published var errorMessage: String? + + private let apiService: APIServiceProtocol + + init(apiService: APIServiceProtocol = APIService.shared) { + self.apiService = apiService + } + + func loadData() async { + isLoading = true + defer { isLoading = false } + // ... + } +} +``` + +## ⚙️ Configuration + +### API Configuration +```swift +enum APIConfig { + static let baseURL = "https://api.goodgo.vn" + static let apiVersion = "/api/v1" + static let timeout: TimeInterval = 30.0 +} +``` + +## 🧪 Testing + +```bash +xcodebuild test \ + -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ + -scheme AppClientBaseSwift \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' +``` + +## 🔐 Security + +| Feature | Implementation | +|---------|----------------| +| Token Storage | Keychain Services (not UserDefaults) | +| Secure Requests | HTTPS only, Bearer token auth | +| Session Management | Auto token refresh, secure logout | +| Data Protection | Sensitive data encrypted at rest | + +## 📱 Supported Devices + +- **iPhone**: 8 and later (iOS 15+) +- **iPad**: All iPads with iOS 15+ +- **Orientations**: Portrait (primary), Landscape (supported) + +## 🔗 Related Projects + +- [app-client-base-net](../app-client-base-net) - .NET MAUI cross-platform client +- [iam-service-net](../../services/iam-service-net) - Authentication backend + +## 📚 Additional Documentation + +- [Architecture Guide](./architecture.md) - Detailed architecture and design decisions + +## 📄 License + +Copyright © 2026 GoodGo. All rights reserved. diff --git a/apps/app-client-base-swift/ARCHITECTURE.md b/apps/app-client-base-swift/docs/en/architecture.md similarity index 66% rename from apps/app-client-base-swift/ARCHITECTURE.md rename to apps/app-client-base-swift/docs/en/architecture.md index aebfdf68..6d24d574 100644 --- a/apps/app-client-base-swift/ARCHITECTURE.md +++ b/apps/app-client-base-swift/docs/en/architecture.md @@ -1,9 +1,8 @@ -# Architecture / Kiến Trúc +# Architecture Guide -> **EN**: Detailed architecture documentation for AppClientBaseSwift iOS application. -> **VI**: Tài liệu kiến trúc chi tiết cho ứng dụng iOS AppClientBaseSwift. +> Detailed architecture documentation for AppClientBaseSwift iOS application. -## Overview / Tổng Quan +## Overview AppClientBaseSwift is a native iOS application built using **MVVM (Model-View-ViewModel)** architecture pattern with **SwiftUI** for declarative UI. The app follows Apple's modern development best practices including: @@ -12,7 +11,7 @@ AppClientBaseSwift is a native iOS application built using **MVVM (Model-View-Vi - **Protocol-oriented programming** for testability - **Keychain Services** for secure storage -## Architecture Diagram / Sơ Đồ Kiến Trúc +## Architecture Diagram ``` ┌─────────────────────────────────────────────────────────────────────────┐ @@ -26,57 +25,42 @@ AppClientBaseSwift is a native iOS application built using **MVVM (Model-View-Vi │ │ │ LoginView │ │RegisterView │ │ForgotPasswd │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ @StateObject / @EnvironmentObject │ -│ ▼ │ +│ @StateObject / @EnvironmentObject │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ ViewModels (@MainActor) │ │ │ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ │ │ │ AuthViewModel │ │ HomeViewModel │ │ProfileViewModel│ │ │ -│ │ │ @Published │ │ @Published │ │ @Published │ │ │ -│ │ │ - isLoading │ │ - items │ │ - user │ │ │ -│ │ │ - errorMessage │ │ - greeting │ │ - isEditing │ │ │ │ │ └────────────────┘ └────────────────┘ └────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ - │ Dependency Injection (Protocol-based) - ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ SERVICE LAYER │ │ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ │ │ APIService │ │ AuthManager │ │ -│ │ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ │ -│ │ │ APIServiceProtocol │ │ │ │ @Published authState │ │ │ -│ │ │ - request() │ │ │ │ - login() │ │ │ -│ │ │ - get(), post() │ │ │ │ - register() │ │ │ -│ │ │ - put(), delete() │ │ │ │ - logout() │ │ │ -│ │ └───────────────────────┘ │ │ │ - refreshToken() │ │ │ -│ │ URLSession │ │ │ Keychain │ │ │ +│ │ • request() │ │ • @Published authState │ │ +│ │ • get(), post() │ │ • login(), register() │ │ +│ │ • URLSession │ │ • Keychain storage │ │ │ └─────────────────────────────┘ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ DATA LAYER │ │ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ │ │ Models │ │ Constants │ │ -│ │ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ │ -│ │ │ User (Codable) │ │ │ │ APIConfig │ │ │ -│ │ │ HomeItem │ │ │ │ AppConstants │ │ │ -│ │ │ AuthState │ │ │ │ StorageKeys │ │ │ -│ │ └───────────────────────┘ │ │ │ DesignSystem │ │ │ +│ │ • User (Codable) │ │ • APIConfig │ │ +│ │ • HomeItem │ │ • StorageKeys │ │ +│ │ • AuthState │ │ • DesignSystem │ │ │ └─────────────────────────────┘ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` -## Component Details / Chi Tiết Component +## Component Details ### 1. Presentation Layer #### Views -| Component | Responsibility / Trách nhiệm | -|-----------|------------------------------| +| Component | Responsibility | +|-----------|----------------| | `SplashView` | Animated splash screen, delayed navigation | | `ContentView` | Root TabView container, auth state routing | | `AuthContainerView` | Auth flow navigation (Login/Register/Forgot) | @@ -88,15 +72,12 @@ AppClientBaseSwift is a native iOS application built using **MVVM (Model-View-Vi ```swift @MainActor final class HomeViewModel: ObservableObject { - // Reactive properties @Published var isLoading: Bool = false @Published var items: [HomeItem] = [] @Published var errorMessage: String? - // Dependencies injected via init private let apiService: APIServiceProtocol - // Async methods using Swift Concurrency func loadData() async { ... } } ``` @@ -104,7 +85,7 @@ final class HomeViewModel: ObservableObject { ### 2. Service Layer #### APIService -HTTP client following **Single Responsibility Principle**: +HTTP client following Single Responsibility Principle: ```swift protocol APIServiceProtocol { @@ -122,7 +103,6 @@ protocol APIServiceProtocol { - Automatic JSON encoding/decoding (snake_case ↔ camelCase) - Bearer token injection - HTTP status code handling -- Error categorization #### AuthManager Singleton for authentication state: @@ -130,48 +110,20 @@ Singleton for authentication state: ```swift final class AuthManager: ObservableObject { @MainActor static let shared = AuthManager() - @Published var authState: AuthState = .unknown - - // Keychain-backed tokens - var accessToken: String? { get } - var refreshToken: String? { get } } ``` **AuthState Enum:** ```swift enum AuthState { - case unknown // Initial state / Trạng thái khởi tạo - case unauthenticated // Logged out / Chưa đăng nhập - case authenticated(User) // Logged in / Đã đăng nhập + case unknown // Initial state + case unauthenticated // Logged out + case authenticated(User) // Logged in } ``` -### 3. Data Layer - -#### Models -```swift -struct User: Codable, Identifiable, Equatable { - let id: String - let email: String - let name: String - let avatarUrl: String? - let phoneNumber: String? - let isEmailVerified: Bool - let createdAt: Date? - let updatedAt: Date? -} -``` - -#### Constants -Organized into semantic enums: -- `APIConfig`: Base URL, version, timeout -- `AppConstants`: App name, bundle ID, keychain service -- `StorageKeys`: UserDefaults/Keychain keys -- `DesignSystem`: Spacing, corner radius, font sizes - -## Data Flow / Luồng Dữ Liệu +## Data Flow ```mermaid sequenceDiagram @@ -190,47 +142,41 @@ sequenceDiagram VM-->>V: SwiftUI re-render ``` -## Authentication Flow / Luồng Xác Thực +## Authentication Flow ```mermaid stateDiagram-v2 [*] --> Unknown: App Launch - Unknown --> Authenticated: Token Found + Valid + Unknown --> Authenticated: Token Valid Unknown --> Unauthenticated: No Token - Unauthenticated --> Login: Show Login - Login --> Authenticated: Login Success + Unauthenticated --> Login + Login --> Authenticated: Success Login --> Register: Navigate - Register --> Authenticated: Register Success + Register --> Authenticated: Success - Authenticated --> HomeScreen: Show Main App + Authenticated --> HomeScreen HomeScreen --> Unauthenticated: Logout - - Authenticated --> TokenRefresh: Token Expired - TokenRefresh --> Authenticated: Refresh Success - TokenRefresh --> Unauthenticated: Refresh Failed ``` -## Design Decisions / Quyết Định Thiết Kế +## Design Decisions -### 1. Why MVVM? / Tại Sao MVVM? +### 1. Why MVVM? -| Benefit / Lợi ích | Description / Mô tả | -|-------------------|---------------------| -| Testability | ViewModel có thể test độc lập không cần UI | -| Separation of Concerns | View chỉ hiển thị, logic nằm ở ViewModel | -| SwiftUI Compatibility | `@ObservableObject` + `@Published` native | +| Benefit | Description | +|---------|-------------| +| Testability | ViewModel can be tested independently without UI | +| Separation of Concerns | View only displays, logic in ViewModel | +| SwiftUI Compatibility | `@ObservableObject` + `@Published` are native | | Reactive Updates | Combine-based automatic UI refresh | ### 2. Why Protocol-based DI? ```swift // Protocol enables mocking for tests -protocol APIServiceProtocol { - func get(endpoint: String) async throws -> T -} +protocol APIServiceProtocol { ... } -// Production implementation +// Production final class APIService: APIServiceProtocol { ... } // Test mock @@ -247,16 +193,11 @@ final class MockAPIService: APIServiceProtocol { ... } ### 4. Why @MainActor on ViewModels? -```swift -@MainActor -final class HomeViewModel: ObservableObject { ... } -``` - - Ensures all `@Published` updates happen on main thread - Prevents concurrency issues with SwiftUI - Explicit thread safety contract -## Security Architecture / Kiến Trúc Bảo Mật +## Security Architecture ``` ┌────────────────────────────────────────────────────────────┐ @@ -264,26 +205,22 @@ final class HomeViewModel: ObservableObject { ... } ├────────────────────────────────────────────────────────────┤ │ Layer 1: Transport Security (HTTPS/TLS) │ │ • All API calls use HTTPS │ -│ • Certificate pinning (TODO) │ ├────────────────────────────────────────────────────────────┤ │ Layer 2: Token Security (Keychain) │ │ • Access token stored in Keychain │ │ • Refresh token stored in Keychain │ -│ • kSecClassGenericPassword protection │ ├────────────────────────────────────────────────────────────┤ │ Layer 3: Session Security │ │ • Token expiry validation │ │ • Automatic token refresh │ -│ • Secure logout (clear all tokens) │ ├────────────────────────────────────────────────────────────┤ │ Layer 4: Input Validation │ │ • Email format validation │ │ • Password strength checking │ -│ • Form field sanitization │ └────────────────────────────────────────────────────────────┘ ``` -## Future Considerations / Hướng Phát Triển +## Future Considerations | Feature | Priority | Description | |---------|----------|-------------| @@ -291,11 +228,7 @@ final class HomeViewModel: ObservableObject { ... } | Biometric Auth | High | Face ID / Touch ID login | | Offline Mode | Medium | Local caching with SwiftData | | Push Notifications | Medium | APNs integration | -| Analytics | Low | Event tracking system | -## Related Documentation / Tài Liệu Liên Quan +## Related Documentation - [README.md](./README.md) - Quick start guide -- [Swift Enterprise Architect Skill](../../.agent/skills/swift-enterprise-architect/SKILL.md) -- [Swift Security Skill](../../.agent/skills/swift-security/SKILL.md) -- [Swift Networking Skill](../../.agent/skills/swift-networking/SKILL.md) diff --git a/apps/app-client-base-swift/docs/vi/README.md b/apps/app-client-base-swift/docs/vi/README.md new file mode 100644 index 00000000..3775a937 --- /dev/null +++ b/apps/app-client-base-swift/docs/vi/README.md @@ -0,0 +1,207 @@ +# App Client Base Swift + +> Ứng dụng iOS native cho nền tảng GoodGo, xây dựng bằng Swift và SwiftUI theo kiến trúc MVVM. + +## 📱 Tính Năng + +| Tính năng | Mô tả | +|-----------|-------| +| 🔐 Xác thực | Đăng nhập, Đăng ký, Quên mật khẩu với form validation | +| 🏠 Trang chủ | Lời chào động, Items nổi bật, Feed hoạt động | +| 🔍 Khám phá | Tìm kiếm địa điểm và dịch vụ | +| 👤 Hồ sơ | Quản lý thông tin cá nhân và cài đặt | +| 🌓 Chế độ tối | Hỗ trợ dark mode tự động | +| 🌐 Đa ngôn ngữ | Hỗ trợ Tiếng Việt & Tiếng Anh | + +## 🛠️ Công Nghệ + +| Công nghệ | Phiên bản | Mục đích | +|-----------|-----------|----------| +| Swift | 5.9+ | Ngôn ngữ chính | +| SwiftUI | iOS 15+ | UI Framework declarative | +| Xcode | 15.0+ | IDE phát triển | +| URLSession | Native | HTTP networking | +| Keychain | Native | Lưu trữ token bảo mật | +| Combine | Native | Reactive programming | + +## 📋 Yêu Cầu + +- **macOS**: 14.0+ (Sonoma) +- **Xcode**: 15.0+ +- **iOS Target**: 15.0+ +- **Tài khoản Apple Developer**: Cần thiết để deploy lên thiết bị + +## 🚀 Bắt Đầu Nhanh + +### 1. Clone và mở project +```bash +cd apps/app-client-base-swift +open AppClientBaseSwift/AppClientBaseSwift.xcodeproj +``` + +### 2. Chọn Simulator +- Menu Xcode: **Product > Destination > iPhone 15 Pro** (hoặc simulator khác) + +### 3. Build và Run +```bash +# Sử dụng shortcut +⌘R (Command + R) + +# Hoặc từ terminal +xcodebuild -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ + -scheme AppClientBaseSwift \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ + build +``` + +### 4. Mock Login (để test) +``` +Email: admin@goodgo.com +Password: 123456 +``` + +## 📂 Cấu Trúc Project + +``` +AppClientBaseSwift/ +├── App/ +│ └── AppClientBaseSwiftApp.swift # Entry point @main +│ +├── Core/ +│ ├── Constants/ +│ │ └── Constants.swift # API, App, Storage, DesignSystem +│ └── Extensions/ +│ ├── View+Extensions.swift # SwiftUI modifiers +│ └── String+Extensions.swift # Validation, formatting +│ +├── Models/ +│ └── User.swift # Entity User + extensions +│ +├── ViewModels/ # MVVM ViewModels +│ ├── AuthViewModel.swift # Login/Đăng ký/Quên mật khẩu +│ ├── HomeViewModel.swift # Logic màn hình Home +│ └── ProfileViewModel.swift # Quản lý hồ sơ +│ +├── Views/ +│ ├── Auth/ # Màn hình xác thực +│ ├── Home/ # Components trang chủ +│ └── Screens/ # Màn hình chính +│ +├── Services/ +│ ├── APIService.swift # HTTP client với URLSession +│ └── AuthManager.swift # Auth state + Keychain +│ +└── Resources/ + ├── Assets.xcassets/ # Hình ảnh & Màu sắc + ├── en.lproj/ # Bản địa hóa Tiếng Anh + └── vi.lproj/ # Bản địa hóa Tiếng Việt +``` + +## 🎨 Kiến Trúc + +### Pattern MVVM + +``` +┌─────────────────────────────────────────────────────────────┐ +│ VIEW (SwiftUI) │ +│ HomeView, ProfileView, AuthContainerView, LoginView... │ +├─────────────────────────────────────────────────────────────┤ +│ @StateObject / @EnvironmentObject │ +├─────────────────────────────────────────────────────────────┤ +│ VIEWMODEL (ObservableObject) │ +│ HomeViewModel, AuthViewModel, ProfileViewModel │ +│ • Thuộc tính @Published cho reactive UI │ +│ • Phương thức async/await để tải dữ liệu │ +├─────────────────────────────────────────────────────────────┤ +│ Dependency Injection dựa trên Protocol │ +├─────────────────────────────────────────────────────────────┤ +│ SERVICES │ +│ APIService (HTTP) • AuthManager (Auth State + Keychain) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 📋 Quy Ước Code + +### Cấu trúc File +```swift +// MARK: - Imports +import SwiftUI + +// MARK: - Định nghĩa Type +/// Mô tả bằng tiếng Việt +struct/class/enum TypeName { + // MARK: - Properties + // MARK: - Init + // MARK: - Public Methods + // MARK: - Private Methods +} +``` + +### Pattern ViewModel +```swift +@MainActor +final class FeatureViewModel: ObservableObject { + @Published var isLoading = false + @Published var errorMessage: String? + + private let apiService: APIServiceProtocol + + init(apiService: APIServiceProtocol = APIService.shared) { + self.apiService = apiService + } + + func loadData() async { + isLoading = true + defer { isLoading = false } + // ... + } +} +``` + +## ⚙️ Cấu Hình + +### Cấu hình API +```swift +enum APIConfig { + static let baseURL = "https://api.goodgo.vn" + static let apiVersion = "/api/v1" + static let timeout: TimeInterval = 30.0 +} +``` + +## 🧪 Kiểm Thử + +```bash +xcodebuild test \ + -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ + -scheme AppClientBaseSwift \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' +``` + +## 🔐 Bảo Mật + +| Tính năng | Triển khai | +|-----------|------------| +| Lưu trữ Token | Keychain Services (không dùng UserDefaults) | +| Request bảo mật | Chỉ HTTPS, xác thực Bearer token | +| Quản lý Session | Tự động refresh token, logout an toàn | +| Bảo vệ dữ liệu | Mã hóa dữ liệu nhạy cảm khi lưu trữ | + +## 📱 Thiết Bị Hỗ Trợ + +- **iPhone**: 8 trở lên (iOS 15+) +- **iPad**: Tất cả iPad với iOS 15+ +- **Hướng màn hình**: Portrait (chính), Landscape (hỗ trợ) + +## 🔗 Dự Án Liên Quan + +- [app-client-base-net](../app-client-base-net) - Client đa nền tảng .NET MAUI +- [iam-service-net](../../services/iam-service-net) - Backend xác thực + +## 📚 Tài Liệu Bổ Sung + +- [Hướng dẫn Kiến trúc](./architecture.md) - Chi tiết kiến trúc và quyết định thiết kế + +## 📄 Giấy Phép + +Bản quyền © 2026 GoodGo. Bảo lưu mọi quyền. diff --git a/apps/app-client-base-swift/docs/vi/architecture.md b/apps/app-client-base-swift/docs/vi/architecture.md new file mode 100644 index 00000000..1c287dd6 --- /dev/null +++ b/apps/app-client-base-swift/docs/vi/architecture.md @@ -0,0 +1,234 @@ +# Hướng Dẫn Kiến Trúc + +> Tài liệu kiến trúc chi tiết cho ứng dụng iOS AppClientBaseSwift. + +## Tổng Quan + +AppClientBaseSwift là ứng dụng iOS native được xây dựng theo mẫu kiến trúc **MVVM (Model-View-ViewModel)** với **SwiftUI** cho UI declarative. Ứng dụng tuân theo các best practices phát triển hiện đại của Apple bao gồm: + +- **Swift Concurrency** (async/await) +- **Combine** cho reactive data binding +- **Protocol-oriented programming** để tăng khả năng test +- **Keychain Services** cho lưu trữ bảo mật + +## Sơ Đồ Kiến Trúc + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ LỚP PRESENTATION │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ SwiftUI Views │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ SplashView │ │ HomeView │ │ ExploreView │ │ ProfileView │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ LoginView │ │RegisterView │ │ForgotPasswd │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ @StateObject / @EnvironmentObject │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ViewModels (@MainActor) │ │ +│ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ +│ │ │ AuthViewModel │ │ HomeViewModel │ │ProfileViewModel│ │ │ +│ │ └────────────────┘ └────────────────┘ └────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + Dependency Injection (dựa trên Protocol) +┌─────────────────────────────────────────────────────────────────────────┐ +│ LỚP SERVICE │ +│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ APIService │ │ AuthManager │ │ +│ │ • request() │ │ • @Published authState │ │ +│ │ • get(), post() │ │ • login(), register() │ │ +│ │ • URLSession │ │ • Lưu trữ Keychain │ │ +│ └─────────────────────────────┘ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────────────┐ +│ LỚP DATA │ +│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ Models │ │ Constants │ │ +│ │ • User (Codable) │ │ • APIConfig │ │ +│ │ • HomeItem │ │ • StorageKeys │ │ +│ │ • AuthState │ │ • DesignSystem │ │ +│ └─────────────────────────────┘ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Chi Tiết Component + +### 1. Lớp Presentation + +#### Views +| Component | Trách nhiệm | +|-----------|-------------| +| `SplashView` | Màn hình splash động, điều hướng trễ | +| `ContentView` | Container TabView gốc, routing theo auth state | +| `AuthContainerView` | Điều hướng luồng Auth (Login/Đăng ký/Quên MK) | +| `HomeView` | Tab Home với lời chào, promo, dịch vụ | +| `ExploreView` | Tính năng khám phá và tìm kiếm | +| `ProfileView` | Hồ sơ người dùng và cài đặt | + +#### ViewModels +```swift +@MainActor +final class HomeViewModel: ObservableObject { + @Published var isLoading: Bool = false + @Published var items: [HomeItem] = [] + @Published var errorMessage: String? + + private let apiService: APIServiceProtocol + + func loadData() async { ... } +} +``` + +### 2. Lớp Service + +#### APIService +HTTP client tuân theo nguyên tắc Single Responsibility: + +```swift +protocol APIServiceProtocol { + func request( + endpoint: String, + method: HTTPMethod, + body: Encodable?, + headers: [String: String]? + ) async throws -> T +} +``` + +**Tính năng:** +- Xử lý request/response generic +- Tự động mã hóa/giải mã JSON (snake_case ↔ camelCase) +- Tự động thêm Bearer token +- Xử lý mã trạng thái HTTP + +#### AuthManager +Singleton quản lý trạng thái xác thực: + +```swift +final class AuthManager: ObservableObject { + @MainActor static let shared = AuthManager() + @Published var authState: AuthState = .unknown +} +``` + +**Enum AuthState:** +```swift +enum AuthState { + case unknown // Trạng thái khởi tạo + case unauthenticated // Chưa đăng nhập + case authenticated(User) // Đã đăng nhập +} +``` + +## Luồng Dữ Liệu + +```mermaid +sequenceDiagram + participant V as View + participant VM as ViewModel + participant S as Service + participant API as Backend API + + V->>VM: Hành động User (tap button) + VM->>VM: Đặt isLoading = true + VM->>S: await service.request() + S->>API: HTTP Request + API-->>S: JSON Response + S-->>VM: Model đã giải mã + VM->>VM: Cập nhật @Published + VM-->>V: SwiftUI render lại +``` + +## Luồng Xác Thực + +```mermaid +stateDiagram-v2 + [*] --> Unknown: Khởi động App + Unknown --> Authenticated: Token hợp lệ + Unknown --> Unauthenticated: Không có Token + + Unauthenticated --> Login + Login --> Authenticated: Thành công + Login --> Register: Điều hướng + Register --> Authenticated: Thành công + + Authenticated --> HomeScreen + HomeScreen --> Unauthenticated: Đăng xuất +``` + +## Quyết Định Thiết Kế + +### 1. Tại sao MVVM? + +| Lợi ích | Mô tả | +|---------|-------| +| Khả năng test | ViewModel có thể test độc lập không cần UI | +| Phân tách trách nhiệm | View chỉ hiển thị, logic nằm ở ViewModel | +| Tương thích SwiftUI | `@ObservableObject` + `@Published` native | +| Cập nhật reactive | UI tự động làm mới dựa trên Combine | + +### 2. Tại sao DI dựa trên Protocol? + +```swift +// Protocol cho phép mock khi test +protocol APIServiceProtocol { ... } + +// Production +final class APIService: APIServiceProtocol { ... } + +// Test mock +final class MockAPIService: APIServiceProtocol { ... } +``` + +### 3. Tại sao Keychain thay vì UserDefaults? + +| Keychain | UserDefaults | +|----------|--------------| +| ✅ Mã hóa khi lưu trữ | ❌ Text thuần | +| ✅ Secure enclave | ❌ Có thể truy cập | +| ✅ Riêng cho app | ❌ Shared prefs | + +### 4. Tại sao @MainActor trên ViewModels? + +- Đảm bảo tất cả cập nhật `@Published` xảy ra trên main thread +- Ngăn chặn vấn đề concurrency với SwiftUI +- Contract an toàn thread rõ ràng + +## Kiến Trúc Bảo Mật + +``` +┌────────────────────────────────────────────────────────────┐ +│ CÁC LỚP BẢO MẬT │ +├────────────────────────────────────────────────────────────┤ +│ Lớp 1: Bảo mật Transport (HTTPS/TLS) │ +│ • Tất cả API calls sử dụng HTTPS │ +├────────────────────────────────────────────────────────────┤ +│ Lớp 2: Bảo mật Token (Keychain) │ +│ • Access token lưu trong Keychain │ +│ • Refresh token lưu trong Keychain │ +├────────────────────────────────────────────────────────────┤ +│ Lớp 3: Bảo mật Session │ +│ • Kiểm tra hết hạn token │ +│ • Tự động refresh token │ +├────────────────────────────────────────────────────────────┤ +│ Lớp 4: Validation Input │ +│ • Kiểm tra định dạng email │ +│ • Kiểm tra độ mạnh mật khẩu │ +└────────────────────────────────────────────────────────────┘ +``` + +## Hướng Phát Triển + +| Tính năng | Ưu tiên | Mô tả | +|-----------|---------|-------| +| Certificate Pinning | Cao | Xác thực chứng chỉ TLS | +| Xác thực sinh trắc | Cao | Đăng nhập Face ID / Touch ID | +| Chế độ Offline | Trung bình | Cache local với SwiftData | +| Push Notifications | Trung bình | Tích hợp APNs | + +## Tài Liệu Liên Quan + +- [README.md](./README.md) - Hướng dẫn bắt đầu nhanh