i+rQbybmy(y>l(H)CqccU7l*ys5{3Qc-Y3N^D89X+O<5oksojM+Ltb Ydjow8!TEFB>w?36_Ge}A1N zKbA2G6#sD)?Vu!l>6!9t)gg1h!y}~xK+>#}crBQ(6qm<})k(MbO@EhxGEG?vLFq%5 zL8 H P@hMJ}AsE&hi z0MAZR=(%(c3nmFp^21t;l30wc?VQe`?duB%@qQDxKxqrA!f<){Gl&2S5q #paN<5YNDle!#G*=ok7IYXbp zZ<5=w*kp2 JQla zAY;7uXh`n+&rD_QcSSNvNW~kJTN`O){~p(UFql9p7RUN9@%Wy+la%(JQp(Qjm(V|7 z{bQ=&T=orAa=WtxkzAp b#>@JT;-nX@UeXtwCkgFPjWwdfbtg$MMMPkydsv%a&rLSR=d-TY2IN; zyRTy`5JW7JoY=G#xDbZ!w;1G#rc^rNE+)IWq^@N7H-rG!$l=1OBoDm(XJg>+ic0w? zerApaU5G1kmNeW rY*)AuC;Hacj*acq*TLAArQ@cDI+$^N4wcT@r>kJFUZ1>-P@*D1JEGi~yus z!SNQpLEKi6M!~zM(!+%aSd+M^eMd} dBbZL~H5Q7XLov!lZC*;$hS%K{!MDs)zbD5IJz* zVoTB8saWA1t7@Ulr{Ym4%eDC6c{OK_aQ>_ *}T3Yr4(ssm9Ou z6Rj7T6Ta)Xe$U5>C%bN#LxVm(U-SEMVX@oaKmS|W*WnwzY#y-|ODqm}ThHI^mo_-n z z`UJhuJ(xED wzlY)tXwmf?8LZc9F1pxz +_`;UQuW>2bYDGUON5&7%rTY?e%^BaLTn4w_AB>;)OxWU03wmS5+kEM z@bQ*o2T?$nIs_!)1@QP@IU+;MpzM`kAu$dStFb%d*K*ag5S6p7RqpK}FP!KDA8s*A zJ6ja*+`Mm;R28O8=3;X)#3Uow+NYd)JANQy>C!a>8blDSSL^z~i*ML`oOsS4?*xFL)*JUl#n6$h4jeDanYp};vq1FrLW*I7)Sr)%pq3b^g1z;s<+{7W2k zSr#(4Yok?O x=chdT%j0}2H9CgU!H(ZO95mg (Q=HS>hS?ex~lQ9ag_ zi&pwR8_%KFOjh)( +5wcCfO~>iWE;`*jn5{WG z&{4$Y6PQ{~xjDiwkYYx0>L5DdczPd&xmR2{bj^BxpQD&(ka(9Z7=n%O)clgz=#DN4 zHbokLhAgj$gkOEau)wTK%O{*?_P+5Qx}37YtN?o~o;Sz|GR76`)O{E7L2%iR8!Wbg z6XY|?7QN4VNd?VgNS1PDp%E~&Ea{e @3#(vX9YHb(_06&`uoXe zk;on>0O9-?nCp##9O_0Zko(BY?-4p`Br6F+T=@*AxK5#Z-p2^}ZjYPg9w?88t$V*S z5;ye7w?A9{ygNw4EMM} #5 z;e(atXbn2Zk?N2sWx!@~0o~2<8P5%?7s|vQ2p$5p4%Lm$$!M=7h%pOY{cR8$1Reb~ z64slXi6$1Ml` a$`7MXubg@VhXXe;l#)~MJ6K5NLW}HDrNE|9?YaWd-JmPW%yPS zs#E%(?wI^%$Qx-j24Re&{a~7}+3tHQ$e!v}6Lls!aEzT7la0k*-5 X&&}*z{NddX^?D&GV#^L~ zw2P6Mriad|ljqS^Gc~It>>7-}t~xEhg~)4sd3!8vsiZliUzwG2sn%6Alcun#mzBW3 zGvo8IL4)`EV9dk6*aTaWy3)~UW|3!mdi44T+RD5l>%hKRAFTo8o3Dn08N&0#Pve9D zw12tqfi~*o(!*IVWbe|iSROXnM3~8HMb^6liQ^~Z+KH_}$x{sMAc~7b&kjI9l?@s* z)hfOitQ$)Z85jH(uA&Mp !X^){i;h^7&S@5N5PYB;gH0_VKONyj4(PQ~PieE( zaGq&jVpEN#Z<~%ecM22$woyuhgi5UI3WcG(09RUj6n&`8V+by*&NlPQ>=J$YAaQ!i z#?Bhw_>E2?T{wF`D^C}Qr>a=v!nielsVUz^@j4_M=EmJ_n4r1o+2W&iUdMKC$M(=n z21eN8PO4Ozb#<*7>D(+)As>f)L6g<0xSk>iIQ*6RcYKj#AKA$o;jSQuF#RRRMG aPnS&Sc@l*neUwGk!W#SIhOQrE#7{VBqNuMGz|6pgHi$H8K>5 z% 4XjGYgYj6^Lli3_FDHeUuBdYc<~`HWR!Z z^dGSvHj^8DADC4v{R;Td91E 7xYbu9SVtdC=6T4r>l$RJ_5Lg5EyGpXabp2<`qT6btTut3| zN~@_cZk`Noh2)py;MgV$@ouvWv@!dg;tcdt6j4(1{ArUiRV=`j?n^y>n_^)OUU(^- zZK5c=`q2{2_OtXdtROTx%%1eBSIpx&PP00CV1zOxULyAIWi=(h@b_OQwnbS5^X+7D zV$>6Q^7Vpwc!h5UcWB9kOtrjYjHixbLmaV}z|$YPtJr$Fp)jt{ox}vC)ymcm#Hd?M zt+TkNrV1km{#*W626mhT`3@6v#XtODLyD60LDSkRo%%BhyRb5wPBc!>0KTq=Vs6nl zwtghAd|CKGa{N$$a1j`OaB<=CI60|8JUI657i*A?y zVKQLe5Uo|`fxYzVZNxB2-;GfUR7CKt(UgptR!~^$gzse20zGG+O__1YRH-J>1teFF zU$ {!I(Q*Su@bRqIJ^W}F)`<~ftG&ro=8g0r>RJvF^w_aD0K{WZ1(uO+3dE;t zAcZjeR<}4e!-Vp$J_kBeKABjRY2H6EXYAb@SYZz$!CqJh0}h6_tp!K)%~YwQt#4gm z>GP*D5kPOyUsc<)tNPVtvJ$)Ct`fH%Ar<;!NP!?BE+3QEMAQ%G{TDbmRKMeb=k>&V ztUZ*&xJa>q9;ZlaW1DGA)i}3|?L9D71pRT7RvhUwQ7l4EJiPQ_t>{bY(IRTcaE_eq ze&9oFY-j}@oXt1?MB;!jEM27UoPG7Jo^N{n2P_jFjb5?zFM7^nVJF)nn+a7Z7cLGe zXKZH1tIt2GW;6_A&7SNhM3{08j|zP6o%mJXzANcoqn)mju3zHl&EO0~xFmpFgKATQ zK><1QI4C~uIR>;S9@(+A(*fyvHJobj;j*sJUB^qXF>z=9TlpY;pS>J;$bnj7q$C(? zeR{7(zppc=AkM6est#a(vMBy+v} Z3`CAU`BuoZn z*9MmeFR5jKfiaRWQGatxvrRS;61^I4$89nb?4Q=&B&dxOy*Oei%}!1Li=_-ue U%Yzo5dps_ul8QfAYb>#AKiV5h?}%rG62y=AU=zNf|pI?H? zMUgskubKVXaY$*!owL=%u*>ZF+q1H)S#5(<#)aCgP6C>lwHH&Cqs9M51{UdhRiTCi z+N#BcYEmp30-7@JFSChYWPKI_u7EWeP8M3Ox=zh-&SWW_M+j4V5NRi^#w-#tk^Fbw zFdxg(k1C7&78!BbSPg3KBGSuQc0JBI8C9!{l_izDqB19|+o2>pO)qnJ`VWRTD2m?q z94g@+fd7W ?HB-mecwV#vz*F>PB{5>yS3@-yRPd0} zJ9SO|sD-++28mHFVmzaxJq*XLoY(w9kAfWKj`{f|Gg~q-Y{=Xz&vF9}2DLP5Atca- zWwh!;$#L=CIC#(}vqikW*5e;z+5(gF8DG%)DtSltPa5e!DIU)^Fb(=7ARn`j*398G z->g^?g^CxiXQQ= y+E@F|tAo#MhkPFB+bsyBQmKlvoiPTVNo1>O zPOSTECXO?a-1do9%jVPEs;maS^L*i&fm^NxpSZr_>pYudbk%O$-!!q^)OPI5VM9nW z{WR+Wd&G4;30=tKnz9f}TWMr+JM5^4GF*5qRnbjwcCE>UX@+i `XmmK(${F%xjT{&Qor8Y%atcF>Yj8n1NXlSxN=j8|A5(QV} z osavzXsn@q-zO`S@1ZAuxiqRwc1VT zH2vS@f9_Na*J|k_oRfNgqlOy0r+KTg@Fm3R{xVo_NVg7X3uD@O=IRBmp#R#|{A61w zn=QS!)7HRi|G)=mv?k_lRkc5K4MU@7NT7JjQ!a+rHWGCyg3D$utQtB)^D=e;d6f7u ziKf}o-Z`b(w$td4xe>R?*u_yOhOsJ2aD{S(0f%~d{j`5^dS2&(?)UdWLH@|BzWccn zvh1@U%ii&IhEs+Y?4^{seTPXV1UW8q4P@qn4@#hbp06^39H8okkI{7a&0uq$6WXk~ zR?~FaHtS`yvYcDHPt{cB5ws{@wq1+2U31&4Yk%ne9U?#uki~v8MUv~PapT?QQ)8-o z@`>f?lkI7S&^($!xKRed_fBr8OJ1P|74&7W#|$CZ 9F0{#e1h%b%6rb(IS9+-scTpK+4CK?MZcwpz?WAgdhN%gw#ycy= zR=p?eb 9*wmpq8j&&{uHz+byiQWYWBhcW&-FW5WiTB>md#hs$yaxp>|v;H zLavj0wfc~1v$ed7I;4mA-2=S}wv%m+Z27`!ABD#vcPvZoO-)s`a=5|Nj6U%6VW`90 zIjh2Sj&4XJg-WV-e+iZU{w!zjrOmu~FlMEz5L`q-^%C;#%ZgDxel#$o2k$GNFH(?6 zP8Eu&o2-O`m?BM!wo;GT>i<4LXjT7JRRYO(853of{1{L0$451_8ev %oo<*TUNHg1&{Ljm0v>4|b8-K+o6b+#HXuXDFB~SfA)z)=Wq-v%>_# zz-K2ZH{T E;q zCQtIIVP4P_)b&^#%Q@lO!-P9-Iqi5Iywa{t8fT5|S12RAlN%6cs#NZO5Uxi@izI#N ziqHAO6}J^=uUHR7`=W61+syHnlU-fs!&ZV40V_N{(0v4V98*66dmcbd;qj`|plj*n z3yBYka~YkT9IFyecHLisPpmwl+nJ3-r`3+^OS8ZMINVd}*2~1?PAA%>N`DZc!<~=g zdsCGisr7IQQb@;>1n|N$!e3+J#Vi(eX>=Hwl@oGYa%hXPoj$f&LB(}ke0!BRT!H3i zD>Is#iBvsn@CAG!Y89ctz7nR^IsR7X?3a_#+S PrPNX+1d@}DM)+4)hn>uci zF9UDY)^wCSC1Td@x(zOgnq?<>@FlTCbB_}_EKpVbs^6p$vg&02UOz5+H7JSUpd)B= z0~ry_Fj+)28kwMn>(`GTJLCa9DW@{Yf2rv_8%p5IZ#f7 8A~G){{(A{Ug0d;R3r_!|?~W|<~A1c~g;)lqC+_K^r5aC`rd zSUVvaXIDWbb(@f@dy_UkO%Wno$@4XJ&&x(1hWBRNM|~P#&uh1%#F%;Oalk$hz_>HD zg9xpl`&{xy2CurPvc#eP%L +sXW))&d#V;=>} z%R!U2?R8r*1`MZwWZ2wI#rJ!1>ulc8W*#XiG+$ws 7TP9MCCb2j!8y`6uOEBse_2)?s~&QoL 6tV zLr44eJK=T2(0WcK0DxY#Cd2pemg_=MGRsgy@q{r|ZZk<~`E#2@bHgU@cw0DhL4dFn zwI$W3sh?Ls_*7t6nijtrw|CriI|#mjImx|?4!ej`zSP7eE5&go5Kxdxa-IP1(>VF$ zQBCVCS(DN<^tG&|ckBsFki7ZDN#<&U_{SgNrMV}_+k~@>al9b@j&~U<9m2I2_v@|w z?v=Z=TjD+IG~^-9Lh!;Z2f+~?`Z8WR4cQtI@||ng3BE=O47l>Mu9 eh3 sPpz$0%<{Bd<+zKdGPc6F 7vaLqxA)I}GDVzEl%@jI={Sxrum5hq pzI)!LG zFMds^WlVM+qYd#yfT+RJ_X#((YiWn+%B00g&RY9@a@zOzSn7-_gK;V8baF}%VW@mL z_SD(DYCx*&O(yhJ?#gYoI&Awx6n530nC8nZ>q^wY>kup^xZ=H|7)JxN*p-LeWYHsE zy~e%;vx)T#%i)0qG3UQ8mJU^W<(rkbt1ET&g9Ye_3oaHJ5n7S{i%Mr0rb;P=<}#e~ zV$Pc>@L1e`#+RE5*4#DOnfijy$|K`QOc!YyNAxT9mPT%OZ=}fnete9lT2WB*z?p58 z{qDirNt%^f` `dX$>PUb-mX$x=6$y5IGbdu8lgV(=EOZXZO2tXBh) z-!4N8FIs}}0E%skve)<}E+?`W!OZ$sz;$RT{rd_we@b6~UfR}KCbM4`XVNCgI;^hE z=p(%>tw-3^85u&jMGnA_i&h=}ORJ~^OpWnE=ooZ@nLkroH5_BfV?DF!lKlXrnh_;T zU_=)SO%`$RHbFvkg1khY%AGL1pwiQRjdp=qPcp1^2ZEk2Zujiu0#93NfD|qh06Cry zWK-#-uN(ea`vverPluBBThe8C2>!%@Ag7kwj$!dqOI@0<5G68y* *^@mt&^9NwZjxquch*;Pf7} zXzYa0q0l=8dJyz9_F-w-=N)5jOI2F1(^vWmRq!>(DB}9}ErNbFVRRGqqhnVAj`CQR zaZ@?$)jenAWkc4$CJF0?vs<#Q${naHmaXvs`|e;W{_+_{Y=K?JV}p8^tr13(-R-sg z`4&%!*j`G`VOFyznb){Y$^oNboe7*X-l+j>H@2k>R6T^8VfaIGU<_Fg7((`aaQjZy z_5lbtFGH}V{iwf{b0DEk!zeozT=WHs*CWR2eL$-46=j>jI#x`9=p8t}vV$2McC!rG zSK-mO;U4(YS1DG8V`n-H9BmUHvux2#Rup`Nf^tDn^zf9yj{t1Uv{Oh7J=MQ;t9yN| zF_Gk&Fwt5f!r7OreN+#J-Knp8gW7$S-EKQ#JDEd^{dNYECP1uHd5-;c6FqjL^hc(( zoy3E_*J9!iF{p&gZ9jdH;wV1TVzK8@4Zd9jPcfl`7pG3mQwuiPax_`_QD`;3R-43+ z^p(|?o{Tou(!bD_#$Pnf*pj|HBZlA(x6>jHsfW&AN#DHGU5#~`70>Sj$<*`Y1$zUc z2-S+&s=!uSp$>)Sl3#v%6$PC5*;c^QEtsg|Q~8J~7pq_(|4#_K<`jCmGwYLPo;pAL z`&~1&1f*-+HiN-O^^jKjjuv){-Y)zV`zbp}Zw`Z_?dsxb{2650*y2(kFw7Sw&dRJv zSd+PqHy-+vQw6DM2BO{9?VC798_SIIqoj$b$h@!FEbTn@Ox7~E<(pnMy(&)T1s!sc zg^sfo+#2QEvwjBp9CjkbiJ$1yHp0*2`Jr}26~pVTo1QuY!-k*N2STGuvwc(JcU;yl zYe!*6QDZ*bor?SSFqjZ&atpQ*@(zLKl2tZ68J>3SOA4|f^?YanqPB@oeD=op^}v!{ z(+2C52=A$5jKBZA#DU|qOs 83R7Ad}eqTxjQ~KtbBQ*dXScSt{cGQC- zb2#K1E+IcMan4)7)(Er&!8R1Ms#)4x5^5K!RH>#i;w +&lRg3MjcziTU97+XSwb(-)# zm`@kXi0VeA@)Dpid~bMsQ_el-bJd_5gC7`C^&8pnk}1jo_CGlT0Y2pO%VteR4R&|> zJa?>nGUwJ@pPk2yR6-z+We<`(nmccQ_wO7yuv12GXoK8Pq6LyXnvct%y#pMmXn})J z_pti(aQ?(ikY0XCdzjmq9T4*Aa~tOQyM-`Hb~XNGDns;*Q<{1#HMCGSyFDQ!OEg xNM0tiY|vJ}yl|GmaF4 zNDFGDcW#gIrB2?WJ*WZW<3)*njRZ^BO3^NVGt9RTsANMB{)3dWy5ND?MPbJkrk)a8 zkV9eU!0N8Kz@8o*Yqhh%^uid9WKm^p0kZq5pd+IcSiol5^;UzfU)d6N_&PSRxID=G zy{3ul;ex9(JNTC?&EKXl9C!IyQrEOgZc`;lhiP-PTp#BaN!#!y-=;uDDP(1=w0d>z z=aA ;{ zmX2Q?ostVoN7Xzp$#>tfUW0PUrog@lWh!%)`GuS+gO=yqi_U~AHp~-!3b!JRl#%KQ zk|cc=?gKQWCj=F22bft1i!eDKS$|YL`8j@2w^%~joUrV$=x%u~4W!MamsyPxws5I- zbTR8^J@%3P_;E%|g~MS{g;T#DC2wFbE_vE@mula8n;02dru_sk63aHv)7wXlax$d< zE&RW8$``Io+G$`~+(!susLamUDNBUnEU?7JpHpP`!IXsd0o<%vlmd+26%L`l6?lz3 z1zh{?t|ug6nU G3=v;6XjU5gk9+Yfg)8#^1RhnL${QA6 zqf!d>u}x7+%sX;=HuoRZO!P?-;Po{9@I+$Wy+r=t0DA#y0sKPc{y_fB3X-3BSG2 x7ujQZrexg>pdnkgwovUlY#wgybMvcPaD**|mtJe^PLn2B z`Zln5X^|&e(9bog`FS) e(hvtUbH_Ygd*fi^KAwK#;_peq(R)cuaZeYGfbD-c}1 zEy`EGu0-$_L+tw=0Br0q0%!S9M6+!ElUOU`T{K2B62$%w{AgiLw0Q8+2hL=-=BP{y zqQ9cfa<-U*ZWITssGxfUA>!**NC;Df-k0Ty**nQvL-6lMbHYelH~#4Up*Q-Tf 7_d=HjU z>P5%jRW)pnwE4#S=KPB!)|{1(!CqqC?KN1tn|cNuU;dY>_*^@aVN^e9vDJjqMF$>3 zHmJ3*FIZ6pab5!Te|QL?$1y{T?Y;HE0 re>OdJF7jEAlr~S*V%!^B2 zyZ-%+yoikBqGT0@l>IIRS?4bVsI|N5{BK|^bc$JHPEe(kP)=G$!05A(wEjc+JoFdi zH}iXpGhUcCr|BXbslB-tqgh0?l^b3~kH#(1gJnADM483G(@1Dh|3^$Frzk5*#TWiN zeme1c$>b24gMI4tB#Hbq-JfMcuhvGOpFS5YzFis9-y%eLrssv0qp6M{eyZciFMBGc z8X`x9M|o}h 5 zv2yc7J!p~aCYWT`Nl%S$x$xVUUlR(;Q7K+&G$fd+8#@>I-^~ko5KoFMj&}9mS2BK) z84DfU967*1X_qp)<>AmlQ$R3O?h4ze0k%E#*m-U#-A8m3Tkk1}l?8JvLtBZN`t!)v zwF%GlDV7FMkCm}xx00&szuMll?q_y7wm^%wTdSw5zW6tKFbE9%s%eDu><+zxx~{rN z6(U=v?IUV|GKL@%Z6BV!^~?d8GZCP%Jcd8s9_8p9)hqchC5@usD9nB8p-ixJm)8XL z0I}3L+;mp8dc7c0=?e**>ptm7qv)=0dFaKI9wD#m>hwJVy$UYCLEcX$|pMJV2> zSF69|0QhmCX763?@T43QL%K?nVZ7cJ(37IO)r<_Gl*qQ?{V}W;pOCL(XaP@M^5+0Z zzqB7GeAbK5u6g`^2|&mOjsl JV`Rr0H`n`x(+#A9KMQE z$Uc=%v@h8i_ee#%_Ju|&288{oEV|Z1K?F<|$f>mgy}B&6U8%xzhNo~^>#CbgOnj;< zLPAHIr5dT)MQ-G|Tfjc|>oXQ)1Pyb |4OoHD&H7U>uH4sk*wwv>j93 zRmi8AnZpBLj-4q?pNvqAazv=0ftUQ4$5PK2i^HMFnbht&N*{Fm)vPE9YwB{-)Fze5 zyZ 9l6Z_g9dMEgX+e(T& zKZAOI;=kvfllQb_*4)Z* (hkwD4eVSu(aWLR3X)}FeBQOPNL1`G0x_I3|GCi*HIZkm z>dnb=dPNFY@8o}$h#w+l0w~d2cNqp2uNmRE$2FEHfjHnkmM9Y~i+hG2NpR{g tX8T;59A8D ni}$0*OmnklzYTNN$8)amsK zHEZjrkc`d$Kp8rSLfgoulz*1r$~# +LVi`lX3UmG7lJvu+pIoPfAM3)-zmtzjyIYAdyXy&{-YyE?pSf%ezE7D;3V&jXpf z6Uzm!1+fCge)vQmjGPLYO8?PH>}2@$dpx2NjU^*%`PGTjqH6n1Cud(yTKe*2Z>^zC zCn#Iq)TR ORAO N)ybqo9RQ?#FdFKSvNOEp zBB5o*wDk?PusTq0iLjtj3H#|IQ`LKd_=XVM6rkon(wP7T8m)NWc ~!CE0!kgWyr*7+oT{V}U!hH`t(MGOoD z&g&K8UHD$nTHCRMUx1T1LzDA{&8p<1e F {o0(&*C2QDaNGkr_ZdMZui5`F^*9NP6T8))UyT+Db<#@eS)~M zV4;N=l3Ul{28-)!ek9!FTRmVLf21$fKwl;0A5Ks{zkg6oD=xb|e;<^B=3628O^2>O z4N_NqTa4LCo`eNQA5qe%Yuh=mgobt|xIRmC7JHyQ|0m-^EAXC~t+9iKW`#ZAY^@7x z%@k~~D#)8C+ jK(#5(VSDgL zucN(5jhqf0sHW3+mYn0)ZxHV62#W35F R!U sC601eRrjM&wB#8aa@@jh zAag|`F+^;R_ bT%3FSdNlV-0yJhF3iBz{+x22!4P=~z#~rvj&Mmn z9bofs|91r=@WzM7Q{P9S5#{Sbw0!qkbE_?fFjY`4TDD9@(aVUiXa!iyG<2@gUGWH+ zjxc#xi4J)ASBVb70U?C1UZ5Vp+V4rvhn*>QVnA9>uUFD?2&L%vgD-1xPH JZcunI(br!2;9m?qHixe;I+*gwGV8J; zKjlI4qCR0${(i3^sKTAb<8e}q5%RD;1srOP(*>!JVJ0MvN#&%19ISvNUJ=z6D2>*& zC&OMb0 aYH|QG zlw6;VwNw>l6G~bZLUAaDJC-za3+Y-W=BXBae3rG3B(WbE)!Lx|ng!D+PyvdDp?VGj zQcIWhls1jL5Sc*WfYZ*Ex@&ncDE{BWMfL|m@?B{^DZAB_joKxWSg?}QOs`WYCquQJ zjQ|dtEZo_UhMGM#y@VD5I{FV8)ZWc67+%v*fp*BP5|H|!E5*icIM4`^F%+z3d_3xn z=xZ3DPY?jY7Ufy=2hDh6%-#?OXJnQY=xy;~%py2R(WFv}@QFtVWewXY>sw|e*yOmi zM{DJlN}-pD@Wf?YlCU1@oNQLtwe+S=HL5(To(}y46_*q`s5Da^hH2(e$V{rR)W3Tz zYRE3(6IKxPc^s_4tTHPY3VPuj91u>j@D$Xm(B{vx_7*XvSR9&tDkx%+md(vGdx^Vi z?5h_;y$ZKk6eONG--t! W7puKrC9$`~>v7MeDFRs1?RueNR zjJO(AIB23nenp$ar-CIpvtjhtELBso^64bJ3P7i!F*W^KYLiQW-0D)4(Ogvl9U%%f z(|v^P;KUD7PzfQh%%M8{FoVNW1ob+l(x7BJ+wK=>Q06Ss U=rGo2b~5sHfNSjB LQ0?C0eiH$5rR@#PSOBtI972RDkjO| zHj$m%OiX>OB`E@0Xp#8p{Y2;Co|Q3A(o=p#>ZU}u(?Z41HMMj3W6PW9A;RW5Y3-Qk z-uUs4 7PWUVT)#DI__KR zu|cvdc~e>!wg=~vw6?u=_g%1$SJ3McJqov>^2UL3W|RIQ+TNwLlo5lR9G9akpW8Dj zLE9464lEOyqyX$%oSGSmYD^EO77+14k*i>6qYX*+STd7>!Y}orQMoEeqY9t>I~HdN zTdh+w)^-#ovh(S6EcDtTgXWWgI{VI1 YX}aw^DncV?WfCWX};*xZMPAZeDwgmHmeBOD>Q^jd6T?AlO|8MX^r@#Xvw3 z>P)c?mOI2bFy;szK8~kb7g)yO@|
n{ObI^sl8X-(AK&S%*CD zd+Rs Qms$iDWT%?udCXE6Pu(q_Q-7BFSRK6v zLrhG}rkx^jl-J3bGM0p41TN#W#4_Ey*YFygeDifZD^6gg7$SSvx`t^`LCX<(b~&cR zjKPkOmS2wVEyR1!&A6wt=@Cb l5XR`>2(F-uLErXJ(u$zMSxoVke>sWbdy-W1 z6bxc~xQSj%*!l_9Y^OzYf#mU^X|Ju@r~9wy@`enfC%4Por%U5Ue}zko(HK7<{OC^+ zF DOQpIYeKVlj^d$4!&n(czon&*HgTg& zzbj%!^HoRZJ0LAxSj>r=l^v!Mgwg%d>NozcHO!t{Av}RS0|N{M14HO Fu@Q`e%P_Y$*3-Xm9B8KGJ`n6E(i%iug z(debGs*a9piVv^SV=xF>gJsu;d(5wF%8}&V6-fa`jFpmM29_*D+9*Fpa0N;6U6MzN z? u}IhH%5XYi3E3fQ*B2!N*exA-8-6Gz zP6lym?V$D`5H_lig^mQxmBqw&{@rKKl_AsA-FBW*F{?Cok*yuajUWiF`(YKAe^+*X zvhkEb&Eo_g(I1v}HNCB@S*4Dsr|PPFb;Qj{=TF&-bB|AzcLs9qNH$Ra6k`q8?oG z{3B|9;;Y^R%RcX>!c+#J@!|ApTyLT}yrO Q^aKapS%A!@)kmz4b%@*XC*V`P>ff+YqIY_Qn0^tFKlHjH-*Rn4t|MfIY%eo- zP_Y9m{94rYzdzsj*-^>IDE1i%K5$1pt7wU{RIgHVY$+^$@4Tugg|Ut#$gxTl6=QuO z$@%NOHD;@49lq&Z%yBJ=GLLV_8UyIvdVB $tyfGex! zfSMm8;x|EjZlP6bC{!wxFt tK4ri84abai5l2{@SvXGAz=>oqdOh&%ZIw zv@bOEv0t7{C}JuwTU3ThbyMfaFi)_0myC+xwgoQx3 rA!;$MsF;0P&G`%?5ZaIO)ZA{ ^ILL6Z$oai(8NaqR!ySAhZ2_pn;1r-L9oID=mzIY0YE(GBrh zA@}j?J8aA4;;CKE9c>L?P%*~z>;Pu08qiYK Ie|l?*DlA`Cfz}_UlTui<>Y`UB zZ^&QO5#H3s+|-Sv8PxOrfdCVD$(i|#yftp-I*H5M1s`(q2RFA+pWKQYG2Pp=0p)S& zptLbWFKbLS%?Mn*e`CiYOh@DgpbjBn3di3E_b0ehni((#?Q|*=$6h?Z)W*~&Xdkfz zir4j2Tx0}NX2m{GJk*nzFMajPar T!%mPy-X!aYe>p z$x!E(YnCimOJldyWv9l-6E}^V`P{P)uPEHd=RpU!3vE3TsVO6&h~HS9%%U!b7TvAX zsT#hRM5DZ3rH8k*P7729`wSwOjkS-dEPqwh=`I$5)CgJ;>;V)u%ER6}f-QPwz2PrH zp7gSm?(j*R(+*n_I++vo&*ZmW*A|IzAu2Ts%4*kztMJ>dd|mz>ivEylxu(~6|E1O7 zL^gBcGD6I6ZjPw0izv@2iU_O@FM^y~KhjHAqkN>?`w_F+h_?5HfFl`u5>$$md;p3A zkSVdo@8E-wl^N=1=&W^0XE|*4&cH+NM#lfa72?EEJy#c`iJ@11iZ(9ex;%HESXf@Z zeLOzaqL1c<`aMkhlUuS+NDS|LJOgXQFCxSaCx^yHxBpl$f*8Hx-6i7D6(e<$a4cO! z?6LAODM7{o6)Tx)1q-SWKhyE9#Nzn(R#3&4B#8mXN}ju>ed_UK65dm5$>=6vyzp7z zGMVAj-PP2V!>vo`0Gt5=gf<|=+Li9yM12FbigbLqVDe#Zb+o|s(PK5uh@NWl+X(GV z_HWht)I|^&$cU8y#9${(bYgOLQdl#$VHN 63(^8jmeBBuTl7=6fgWa?=9fZE`!(^rZd3xW?F`9$0 zC+fscGY>Yr%T%#F-14U!ey7CTV-`IUZ;NZuAcEW Oo`0)tr3!pr!@wjoM1(VH;3>~=@Ag7OQJSs4nb;Eo^P$x3aKOv& zFib!-i*IXSr@CL-5yTit#E@^I1*MLFPBc^EE!htYME3B4UKiL~YpQx?SEI(g>jepD zSoT;sRjyLALY02B$oQ1w9Iw6!Zg!8$$5|SE9d2m-OF}uXRW|BJeb(<{QHD-CT-P>b zk6dUr1WWnelWhdP8j zVj&+MrzEwo>O4<@-C=2%|JH_nOu9V$ zpGv5L#`TI9u&9+TKQE#^3!gH*SFRxS#pT0A6s9nSn;taizsIiuU1r$*mJ*uLmRc0p zr(dVN^IMEZcCD{tL#uCOZD#na?f+_neD@V_o-@lOYCv$DKDV!AN{zAAva>8A(#9Rv zeV~KbAt|%t)ya$D%RqyT`Xx`4AT#|LHZ|RNJ~D$#TfDF312^xU3x}<5#vJ_M@XMcA zc+8zCfNPBKZQFxBnwU*(i%mT4e`2kjwl()+)X<{YS6v@4t5SocY)I!RPVsXqs4MQW zFrj5uEym!v1+v)h@GT&R4GwmXINi4)jYvuR1!h&9F@gfk=~If}p=Erx; VN>GOCBNzyKs!#R82lVS`}EEU|NbDBEm!o^&dgBa=?#A#%#!rW@u5~->a%e&?aos zxW0v~?$K+?Fy}72LJ1Wdi7Nkzvf?iK bJG3%8Ri@zh z{raU%4kiv+IpgmP0$E&td4Q{zoSBrVo?VfaqnDVJfu5I@r}*zhc6@w1*bt3`sWldS zG#->kZfAbA$4~T&h)o=;nYRW#?sC`plc<8geRsTJVL-#MEUePv<0q>0b8GI}^}_ku z^cce4^cV>5G{`6>8N(f#G6+Flbf6YCb0nJ!*qH?7(8giEzoR3RtxX`s#zQ@+vG<0E zLeuPE3j7^rfxqF9h_+~cb`uaY?574bLtSw~Ucy?E+T64-A3A`+i7T6;EF?cWx{nd{ zfHoiALrZ(%rhiWWgh5akL&j#rTs8l0v47owPWD|YsPp0+Ixe9!SB3Nf5{YxrJo!5! zSnVQ#=>+~Z_mhP}u!mWbL^_EGJDQIrxPf&SZ#^>4nQ0+B9Ptzl3z >Qi%OaFpR|D7kXH3BQS>sWw}?XzmR;BCWL zW%H;)t*{*uLG{=Eh&3>zF= }==_{u>HHZ|GuSZA@?GXkct@LT_N;NNZ!_ zY+!8QY(Qu4XlHNY=xkx)BqdGbI7vStLjx&AOEoh!+sL}ayeBD5^B_YjAuT~8A&fRN zJ>@hrOFK3t0V_M+!YX{+shV7T5|9#z`sbts<=CvW4V|>~s2J=hxzy~05EGOI=HFTY zflz?|-<2=_E%?7$alrq6{8t^!|1|ke(8+%r000yR2>&ag`G3Kl{HKHeB!B#m184gG og9GxPhX1*>|6?fa`2Vrd@>2hTiGK~`Uq=B10C@Z73;^JN0h;^1k^lez literal 0 HcmV?d00001 From 089ca021fddb35ca185879b6ae3133c378f30a7c Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Fri, 20 Oct 2023 16:33:07 +0200 Subject: [PATCH 132/272] Implement TwintDelegate COAND-806 --- .../components/core/PaymentMethodTypes.kt | 2 + .../paymentmethod/PaymentMethodDetails.kt | 1 + .../core/paymentmethod/TwintPaymentMethod.kt | 54 ++++ twint/build.gradle | 1 + .../adyen/checkout/twint/TwintComponent.kt | 36 ++- .../checkout/twint/TwintComponentState.kt | 6 +- .../checkout/twint/TwintConfiguration.kt | 125 ++++++++ .../provider/TwintComponentProvider.kt | 279 ++++++++++++++++++ .../twint/internal/ui/DefaultTwintDelegate.kt | 134 +++++++++ .../twint/internal/ui/TwintDelegate.kt | 12 +- 10 files changed, 644 insertions(+), 6 deletions(-) create mode 100644 components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/TwintPaymentMethod.kt create mode 100644 twint/src/main/java/com/adyen/checkout/twint/TwintConfiguration.kt create mode 100644 twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintComponentProvider.kt create mode 100644 twint/src/main/java/com/adyen/checkout/twint/internal/ui/DefaultTwintDelegate.kt diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt b/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt index d8c8e4b29b..4a97ba42a1 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt @@ -46,6 +46,7 @@ object PaymentMethodTypes { const val PAY_BY_BANK = "paybybank" const val SCHEME = "scheme" const val SEPA = "sepadirectdebit" + const val TWINT = "twint" const val UPI = "upi" const val UPI_COLLECT = "upi_collect" const val UPI_QR = "upi_qr" @@ -129,6 +130,7 @@ object PaymentMethodTypes { PROMPT_PAY, SCHEME, SEPA, + TWINT, UPI, UPI_COLLECT, UPI_QR, diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt index 9ff95241e0..fe12f6c6da 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt @@ -88,6 +88,7 @@ abstract class PaymentMethodDetails : ModelObject() { SepaPaymentMethod.PAYMENT_METHOD_TYPE -> SepaPaymentMethod.SERIALIZER SevenElevenPaymentMethod.PAYMENT_METHOD_TYPE -> SevenElevenPaymentMethod.SERIALIZER + TwintPaymentMethod.PAYMENT_METHOD_TYPE -> TwintPaymentMethod.SERIALIZER else -> GenericPaymentMethod.SERIALIZER } @Suppress("UNCHECKED_CAST") diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/TwintPaymentMethod.kt b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/TwintPaymentMethod.kt new file mode 100644 index 0000000000..d27b9f43bf --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/TwintPaymentMethod.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 20/10/2023. + */ + +package com.adyen.checkout.components.core.paymentmethod + +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.core.exception.ModelSerializationException +import com.adyen.checkout.core.internal.data.model.getStringOrNull +import kotlinx.parcelize.Parcelize +import org.json.JSONException +import org.json.JSONObject + +@Parcelize +data class TwintPaymentMethod( + override var type: String?, + var subtype: String?, + override var checkoutAttemptId: String?, +) : PaymentMethodDetails() { + + companion object { + + const val PAYMENT_METHOD_TYPE = PaymentMethodTypes.TWINT + + private const val SUBTYPE = "subtype" + + @JvmField + val SERIALIZER: Serializer = object : Serializer { + override fun serialize(modelObject: TwintPaymentMethod): JSONObject { + return try { + JSONObject().apply { + putOpt(TYPE, modelObject.type) + putOpt(SUBTYPE, modelObject.subtype) + putOpt(CHECKOUT_ATTEMPT_ID, modelObject.checkoutAttemptId) + } + } catch (e: JSONException) { + throw ModelSerializationException(TwintPaymentMethod::class.java, e) + } + } + + override fun deserialize(jsonObject: JSONObject): TwintPaymentMethod { + return TwintPaymentMethod( + type = jsonObject.getStringOrNull(TYPE), + subtype = jsonObject.getStringOrNull(SUBTYPE), + checkoutAttemptId = jsonObject.getStringOrNull(CHECKOUT_ATTEMPT_ID), + ) + } + } + } +} diff --git a/twint/build.gradle b/twint/build.gradle index 43cda7d359..8f9f5dd588 100644 --- a/twint/build.gradle +++ b/twint/build.gradle @@ -39,6 +39,7 @@ android { dependencies { // Checkout + api project(':action-core') api project(':ui-core') api project(':sessions-core') diff --git a/twint/src/main/java/com/adyen/checkout/twint/TwintComponent.kt b/twint/src/main/java/com/adyen/checkout/twint/TwintComponent.kt index 421560674f..b6b554aefb 100644 --- a/twint/src/main/java/com/adyen/checkout/twint/TwintComponent.kt +++ b/twint/src/main/java/com/adyen/checkout/twint/TwintComponent.kt @@ -8,25 +8,43 @@ package com.adyen.checkout.twint +import androidx.activity.ComponentActivity import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.adyen.checkout.action.core.internal.ActionHandlingComponent +import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent +import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.internal.util.adyenLog +import com.adyen.checkout.twint.internal.provider.TwintComponentProvider import com.adyen.checkout.twint.internal.ui.TwintDelegate +import com.adyen.checkout.ui.core.internal.ui.ComponentViewType +import com.adyen.checkout.ui.core.internal.ui.ViewableComponent +import kotlinx.coroutines.flow.Flow class TwintComponent internal constructor( private val twintDelegate: TwintDelegate, + private val genericActionDelegate: GenericActionDelegate, + private val actionHandlingComponent: DefaultActionHandlingComponent, + internal val componentEventHandler: ComponentEventHandler , ) : ViewModel(), - PaymentComponent { + PaymentComponent, + ViewableComponent, + ActionHandlingComponent by actionHandlingComponent { - override val delegate: ComponentDelegate get() = twintDelegate + override val delegate: ComponentDelegate get() = actionHandlingComponent.activeDelegate + + override val viewFlow: Flow = genericActionDelegate.viewFlow init { twintDelegate.initialize(viewModelScope) + componentEventHandler.initialize(viewModelScope) } internal fun observe( @@ -40,6 +58,10 @@ class TwintComponent internal constructor( twintDelegate.removeObserver() } + fun startTwintScreen(activity: ComponentActivity) { + twintDelegate.startTwintScreen(activity) + } + override fun setInteractionBlocked(isInteractionBlocked: Boolean) { adyenLog(AdyenLogLevel.WARN) { "Interaction with TwintComponent can't be blocked" } } @@ -48,5 +70,15 @@ class TwintComponent internal constructor( adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } super.onCleared() twintDelegate.onCleared() + componentEventHandler.onCleared() + } + + companion object { + + @JvmField + val PROVIDER = TwintComponentProvider() + + @JvmField + val PAYMENT_METHOD_TYPES = listOf(PaymentMethodTypes.TWINT) } } diff --git a/twint/src/main/java/com/adyen/checkout/twint/TwintComponentState.kt b/twint/src/main/java/com/adyen/checkout/twint/TwintComponentState.kt index ffea7e688b..c0111e8477 100644 --- a/twint/src/main/java/com/adyen/checkout/twint/TwintComponentState.kt +++ b/twint/src/main/java/com/adyen/checkout/twint/TwintComponentState.kt @@ -10,10 +10,10 @@ package com.adyen.checkout.twint import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.PaymentComponentState -import com.adyen.checkout.components.core.paymentmethod.GenericPaymentMethod +import com.adyen.checkout.components.core.paymentmethod.TwintPaymentMethod data class TwintComponentState( - override val data: PaymentComponentData , + override val data: PaymentComponentData , override val isInputValid: Boolean, override val isReady: Boolean, -) : PaymentComponentState +) : PaymentComponentState diff --git a/twint/src/main/java/com/adyen/checkout/twint/TwintConfiguration.kt b/twint/src/main/java/com/adyen/checkout/twint/TwintConfiguration.kt new file mode 100644 index 0000000000..1b1bb7b156 --- /dev/null +++ b/twint/src/main/java/com/adyen/checkout/twint/TwintConfiguration.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 20/10/2023. + */ + +package com.adyen.checkout.twint + +import android.content.Context +import com.adyen.checkout.action.core.GenericActionConfiguration +import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker +import com.adyen.checkout.core.Environment +import kotlinx.parcelize.Parcelize +import java.util.Locale + +@Parcelize +class TwintConfiguration private constructor( + override val shopperLocale: Locale?, + override val environment: Environment, + override val clientKey: String, + override val analyticsConfiguration: AnalyticsConfiguration?, + override val amount: Amount?, + internal val genericActionConfiguration: GenericActionConfiguration, +) : Configuration { + + class Builder : ActionHandlingPaymentMethodConfigurationBuilder { + + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + + /** + * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. + * + * @param context A context + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + @Suppress("DEPRECATION") + @Deprecated("You can omit the context parameter") + constructor(context: Context, environment: Environment, clientKey: String) : super( + context, + environment, + clientKey, + ) + + /** + * Initialize a configuration builder with the required fields. + * + * @param shopperLocale The [Locale] of the shopper. + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor( + shopperLocale: Locale, + environment: Environment, + clientKey: String + ) : super(shopperLocale, environment, clientKey) + + override fun buildInternal(): TwintConfiguration { + return TwintConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsConfiguration = analyticsConfiguration, + amount = amount, + genericActionConfiguration = genericActionConfigurationBuilder.build(), + ) + } + } +} + +fun CheckoutConfiguration.twint( + configuration: @CheckoutConfigurationMarker TwintConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = TwintConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addActionConfiguration(config) + return this +} + +internal fun CheckoutConfiguration.getTwintConfiguration(): TwintConfiguration? { + return getActionConfiguration(TwintConfiguration::class.java) +} + +internal fun TwintConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addActionConfiguration(this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintComponentProvider.kt b/twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintComponentProvider.kt new file mode 100644 index 0000000000..f22391ce69 --- /dev/null +++ b/twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintComponentProvider.kt @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 20/10/2023. + */ + +package com.adyen.checkout.twint.internal.provider + +import android.app.Application +import androidx.annotation.RestrictTo +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import androidx.savedstate.SavedStateRegistryOwner +import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent +import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.ComponentCallback +import com.adyen.checkout.components.core.Order +import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.components.core.internal.DefaultComponentEventHandler +import com.adyen.checkout.components.core.internal.PaymentObserverRepository +import com.adyen.checkout.components.core.internal.data.api.AnalyticsMapper +import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository +import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepositoryData +import com.adyen.checkout.components.core.internal.data.api.AnalyticsService +import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository +import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper +import com.adyen.checkout.components.core.internal.util.get +import com.adyen.checkout.components.core.internal.util.viewModelFactory +import com.adyen.checkout.core.exception.ComponentException +import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider +import com.adyen.checkout.sessions.core.CheckoutSession +import com.adyen.checkout.sessions.core.SessionComponentCallback +import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler +import com.adyen.checkout.sessions.core.internal.SessionInteractor +import com.adyen.checkout.sessions.core.internal.SessionSavedStateHandleContainer +import com.adyen.checkout.sessions.core.internal.data.api.SessionRepository +import com.adyen.checkout.sessions.core.internal.data.api.SessionService +import com.adyen.checkout.sessions.core.internal.provider.SessionPaymentComponentProvider +import com.adyen.checkout.twint.TwintComponent +import com.adyen.checkout.twint.TwintComponentState +import com.adyen.checkout.twint.TwintConfiguration +import com.adyen.checkout.twint.internal.ui.DefaultTwintDelegate +import com.adyen.checkout.twint.toCheckoutConfiguration + +class TwintComponentProvider +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +constructor( + private val dropInOverrideParams: DropInOverrideParams? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), + private val analyticsRepository: AnalyticsRepository? = null, +) : + PaymentComponentProvider< + TwintComponent, + TwintConfiguration, + TwintComponentState, + ComponentCallback , + >, + SessionPaymentComponentProvider< + TwintComponent, + TwintConfiguration, + TwintComponentState, + SessionComponentCallback , + > { + + @Suppress("LongMethod") + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + application: Application, + componentCallback: ComponentCallback , + order: Order?, + key: String? + ): TwintComponent { + assertSupported(paymentMethod) + + val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) + val viewModelFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> + + val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( + analyticsRepositoryData = AnalyticsRepositoryData( + application = application, + componentParams = componentParams, + paymentMethod = paymentMethod, + ), + analyticsService = AnalyticsService( + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), + ), + analyticsMapper = AnalyticsMapper(), + ) + + val twintDelegate = DefaultTwintDelegate( + observerRepository = PaymentObserverRepository(), + paymentMethod = paymentMethod, + order = order, + componentParams = componentParams, + analyticsRepository = analyticsRepository, + ) + + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, + savedStateHandle = savedStateHandle, + application = application, + ) + + TwintComponent( + twintDelegate = twintDelegate, + genericActionDelegate = genericActionDelegate, + actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, twintDelegate), + componentEventHandler = DefaultComponentEventHandler(), + ) + } + + return ViewModelProvider(viewModelStoreOwner, viewModelFactory)[key, TwintComponent::class.java] + .also { component -> + component.observe(lifecycleOwner) { + component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) + } + } + } + + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: TwintConfiguration, + application: Application, + componentCallback: ComponentCallback , + order: Order?, + key: String?, + ): TwintComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + + @Suppress("LongMethod") + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + application: Application, + componentCallback: SessionComponentCallback , + key: String? + ): TwintComponent { + assertSupported(paymentMethod) + + val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) + val viewModelFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) + + val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( + analyticsRepositoryData = AnalyticsRepositoryData( + application = application, + componentParams = componentParams, + paymentMethod = paymentMethod, + sessionId = checkoutSession.sessionSetupResponse.id, + ), + analyticsService = AnalyticsService( + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), + ), + analyticsMapper = AnalyticsMapper(), + ) + + val twintDelegate = DefaultTwintDelegate( + observerRepository = PaymentObserverRepository(), + paymentMethod = paymentMethod, + order = checkoutSession.order, + componentParams = componentParams, + analyticsRepository = analyticsRepository, + ) + + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, + savedStateHandle = savedStateHandle, + application = application, + ) + + val sessionSavedStateHandleContainer = SessionSavedStateHandleContainer( + savedStateHandle = savedStateHandle, + checkoutSession = checkoutSession, + ) + + val sessionInteractor = SessionInteractor( + sessionRepository = SessionRepository( + sessionService = SessionService(httpClient), + clientKey = componentParams.clientKey, + ), + sessionModel = sessionSavedStateHandleContainer.getSessionModel(), + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, + ) + + val sessionComponentEventHandler = SessionComponentEventHandler ( + sessionInteractor = sessionInteractor, + sessionSavedStateHandleContainer = sessionSavedStateHandleContainer, + ) + + TwintComponent( + twintDelegate = twintDelegate, + genericActionDelegate = genericActionDelegate, + actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, twintDelegate), + componentEventHandler = sessionComponentEventHandler, + ) + } + + return ViewModelProvider(viewModelStoreOwner, viewModelFactory)[key, TwintComponent::class.java] + .also { component -> + component.observe(lifecycleOwner) { + component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) + } + } + } + + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: TwintConfiguration, + application: Application, + componentCallback: SessionComponentCallback , + key: String? + ): TwintComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + + private fun assertSupported(paymentMethod: PaymentMethod) { + if (!isPaymentMethodSupported(paymentMethod)) { + throw ComponentException("Unsupported payment method ${paymentMethod.type}") + } + } + + override fun isPaymentMethodSupported(paymentMethod: PaymentMethod): Boolean { + return TwintComponent.PAYMENT_METHOD_TYPES.contains(paymentMethod.type) + } +} diff --git a/twint/src/main/java/com/adyen/checkout/twint/internal/ui/DefaultTwintDelegate.kt b/twint/src/main/java/com/adyen/checkout/twint/internal/ui/DefaultTwintDelegate.kt new file mode 100644 index 0000000000..5cc2458b94 --- /dev/null +++ b/twint/src/main/java/com/adyen/checkout/twint/internal/ui/DefaultTwintDelegate.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 20/10/2023. + */ + +package com.adyen.checkout.twint.internal.ui + +import androidx.activity.ComponentActivity +import androidx.lifecycle.LifecycleOwner +import ch.twint.payment.sdk.Twint +import ch.twint.payment.sdk.TwintPayResult +import com.adyen.checkout.components.core.OrderRequest +import com.adyen.checkout.components.core.PaymentComponentData +import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.PaymentComponentEvent +import com.adyen.checkout.components.core.internal.PaymentObserverRepository +import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository +import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.internal.util.bufferedChannel +import com.adyen.checkout.components.core.paymentmethod.TwintPaymentMethod +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.core.internal.util.adyenLog +import com.adyen.checkout.twint.TwintComponentState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +internal class DefaultTwintDelegate( + private val observerRepository: PaymentObserverRepository, + private val paymentMethod: PaymentMethod, + private val order: OrderRequest?, + override val componentParams: ComponentParams, + private val analyticsRepository: AnalyticsRepository, +) : TwintDelegate { + + private val _componentStateFlow = MutableStateFlow(createComponentState()) + override val componentStateFlow: Flow = _componentStateFlow + + private val exceptionChannel: Channel = bufferedChannel() + override val exceptionFlow: Flow = exceptionChannel.receiveAsFlow() + + private val submitChannel: Channel = bufferedChannel() + override val submitFlow: Flow = submitChannel.receiveAsFlow() + + override fun initialize(coroutineScope: CoroutineScope) { + setupAnalytics(coroutineScope) + + componentStateFlow.onEach { + onState(it) + }.launchIn(coroutineScope) + } + + private fun setupAnalytics(coroutineScope: CoroutineScope) { + adyenLog(AdyenLogLevel.DEBUG) { "setupAnalytics" } + coroutineScope.launch { + analyticsRepository.setupAnalytics() + } + } + + private fun onState(state: TwintComponentState) { + if (state.isValid) { + submitChannel.trySend(state) + } + } + + override fun observe( + lifecycleOwner: LifecycleOwner, + coroutineScope: CoroutineScope, + callback: (PaymentComponentEvent ) -> Unit + ) { + observerRepository.addObservers( + stateFlow = componentStateFlow, + exceptionFlow = exceptionFlow, + submitFlow = submitFlow, + lifecycleOwner = lifecycleOwner, + coroutineScope = coroutineScope, + callback = callback, + ) + } + + override fun removeObserver() { + observerRepository.removeObservers() + } + + private fun createComponentState(): TwintComponentState { + val paymentMethod = TwintPaymentMethod( + type = paymentMethod.type, + subtype = "sdk", + checkoutAttemptId = analyticsRepository.getCheckoutAttemptId(), + ) + + val paymentComponentData = PaymentComponentData( + paymentMethod = paymentMethod, + order = order, + amount = componentParams.amount, + ) + + return TwintComponentState( + data = paymentComponentData, + isInputValid = true, + isReady = true, + ) + } + + override fun startTwintScreen(activity: ComponentActivity) { + val twint = Twint(activity) { result -> + when (result) { + TwintPayResult.TW_B_SUCCESS -> TODO() + TwintPayResult.TW_B_ERROR -> TODO() + TwintPayResult.TW_B_APP_NOT_INSTALLED -> TODO() + } + } + + twint.payWithCode("test") + } + + override fun getPaymentMethodType(): String { + return paymentMethod.type ?: PaymentMethodTypes.UNKNOWN + } + + override fun onCleared() { + removeObserver() + } +} diff --git a/twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintDelegate.kt b/twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintDelegate.kt index 1c48d871a7..3783532610 100644 --- a/twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintDelegate.kt +++ b/twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintDelegate.kt @@ -8,7 +8,17 @@ package com.adyen.checkout.twint.internal.ui +import androidx.activity.ComponentActivity import com.adyen.checkout.components.core.internal.ui.PaymentComponentDelegate +import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.twint.TwintComponentState +import kotlinx.coroutines.flow.Flow -internal interface TwintDelegate : PaymentComponentDelegate +internal interface TwintDelegate : PaymentComponentDelegate { + + val componentStateFlow: Flow + + val exceptionFlow: Flow + + fun startTwintScreen(activity: ComponentActivity) +} From bacb2f3be870653b523a18ba7f9fc0e4580cd4ae Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Fri, 20 Oct 2023 16:33:43 +0200 Subject: [PATCH 133/272] Add support for Twint in drop-in COAND-806 --- action-core/build.gradle | 3 +- .../action/core/GenericActionConfiguration.kt | 9 + .../ActionHandlingConfigurationBuilder.kt | 6 + ...ndlingPaymentMethodConfigurationBuilder.kt | 9 + .../provider/ActionComponentExtensions.kt | 2 + .../internal/ui/ActionDelegateProvider.kt | 17 +- .../internal/ui/DefaultBoletoDelegate.kt | 1 + .../components/core/PaymentMethodTypes.kt | 3 +- .../components/core/action/SdkAction.kt | 1 + .../components/core/action/TwintSdkData.kt | 48 +++ .../paymentmethod/GenericPaymentMethod.kt | 5 + .../paymentmethod/PaymentMethodDetails.kt | 1 - .../core/paymentmethod/TwintPaymentMethod.kt | 54 ---- drop-in/build.gradle | 1 + .../checkout/dropin/DropInConfiguration.kt | 1 + .../dropin/internal/ui/DropInActivity.kt | 6 + .../ui/DefaultInstantPaymentDelegate.kt | 10 + twint/build.gradle | 3 - .../java/com/adyen/checkout/twint/Twint.kt | 52 ++++ .../checkout/twint/TwintActionComponent.kt | 67 +++++ ...uration.kt => TwintActionConfiguration.kt} | 32 +- .../adyen/checkout/twint/TwintComponent.kt | 84 ------ .../checkout/twint/TwintComponentState.kt | 19 -- .../provider/TwintActionComponentProvider.kt | 121 ++++++++ .../provider/TwintComponentProvider.kt | 279 ------------------ .../twint/internal/ui/DefaultTwintDelegate.kt | 142 +++++---- .../twint/internal/ui/TwintDelegate.kt | 22 +- .../twint/internal/ui/TwintViewProvider.kt | 30 ++ .../WeChatPayActionComponentProvider.kt | 3 +- 29 files changed, 483 insertions(+), 548 deletions(-) create mode 100644 components-core/src/main/java/com/adyen/checkout/components/core/action/TwintSdkData.kt delete mode 100644 components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/TwintPaymentMethod.kt create mode 100644 twint/src/main/java/com/adyen/checkout/twint/Twint.kt create mode 100644 twint/src/main/java/com/adyen/checkout/twint/TwintActionComponent.kt rename twint/src/main/java/com/adyen/checkout/twint/{TwintConfiguration.kt => TwintActionConfiguration.kt} (77%) delete mode 100644 twint/src/main/java/com/adyen/checkout/twint/TwintComponent.kt delete mode 100644 twint/src/main/java/com/adyen/checkout/twint/TwintComponentState.kt create mode 100644 twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintActionComponentProvider.kt delete mode 100644 twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintComponentProvider.kt create mode 100644 twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintViewProvider.kt diff --git a/action-core/build.gradle b/action-core/build.gradle index b2c6de067c..61c0203b73 100644 --- a/action-core/build.gradle +++ b/action-core/build.gradle @@ -41,8 +41,9 @@ dependencies { api project(':await') api project(':qr-code') api project(':redirect') - compileOnly project(':wechatpay') + api project(':twint') api project(':voucher') + compileOnly project(':wechatpay') //Tests testImplementation project(':3ds2') diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionConfiguration.kt b/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionConfiguration.kt index a8b6792ba3..e9dad0c222 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionConfiguration.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionConfiguration.kt @@ -23,6 +23,7 @@ import com.adyen.checkout.components.core.internal.Configuration import com.adyen.checkout.core.Environment import com.adyen.checkout.qrcode.QRCodeConfiguration import com.adyen.checkout.redirect.RedirectConfiguration +import com.adyen.checkout.twint.TwintActionConfiguration import com.adyen.checkout.voucher.VoucherConfiguration import com.adyen.checkout.wechatpay.WeChatPayActionConfiguration import kotlinx.parcelize.Parcelize @@ -136,6 +137,14 @@ class GenericActionConfiguration private constructor( return this } + /** + * Add configuration for Twint action. + */ + override fun addTwintActionConfiguration(configuration: TwintActionConfiguration): Builder { + availableActionConfigs[configuration::class.java] = configuration + return this + } + /** * Add configuration for WeChat Pay action. */ diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingConfigurationBuilder.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingConfigurationBuilder.kt index 2f841fc99b..91da96d609 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingConfigurationBuilder.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingConfigurationBuilder.kt @@ -12,6 +12,7 @@ import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration import com.adyen.checkout.await.AwaitConfiguration import com.adyen.checkout.qrcode.QRCodeConfiguration import com.adyen.checkout.redirect.RedirectConfiguration +import com.adyen.checkout.twint.TwintActionConfiguration import com.adyen.checkout.voucher.VoucherConfiguration import com.adyen.checkout.wechatpay.WeChatPayActionConfiguration @@ -37,6 +38,11 @@ internal interface ActionHandlingConfigurationBuilder { */ fun addRedirectActionConfiguration(configuration: RedirectConfiguration): BuilderT + /** + * Add configuration for Twint action. + */ + fun addTwintActionConfiguration(configuration: TwintActionConfiguration): BuilderT + /** * Add configuration for Voucher action. */ diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt index 411c6f740f..75d3fa8837 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt @@ -17,6 +17,7 @@ import com.adyen.checkout.components.core.internal.Configuration import com.adyen.checkout.core.Environment import com.adyen.checkout.qrcode.QRCodeConfiguration import com.adyen.checkout.redirect.RedirectConfiguration +import com.adyen.checkout.twint.TwintActionConfiguration import com.adyen.checkout.voucher.VoucherConfiguration import com.adyen.checkout.wechatpay.WeChatPayActionConfiguration import java.util.Locale @@ -124,6 +125,14 @@ constructor( return this as BuilderT } + /** + * Add configuration for Twint action. + */ + override fun addTwintActionConfiguration(configuration: TwintActionConfiguration): BuilderT { + genericActionConfigurationBuilder.addTwintActionConfiguration(configuration) + return this as BuilderT + } + /** * Add configuration for Voucher action. */ diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/ActionComponentExtensions.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/ActionComponentExtensions.kt index 70adbf359d..b57cacbad2 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/ActionComponentExtensions.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/ActionComponentExtensions.kt @@ -15,6 +15,7 @@ import com.adyen.checkout.components.core.internal.provider.ActionComponentProvi import com.adyen.checkout.core.internal.util.runCompileOnly import com.adyen.checkout.qrcode.QRCodeComponent import com.adyen.checkout.redirect.RedirectComponent +import com.adyen.checkout.twint.TwintActionComponent import com.adyen.checkout.voucher.VoucherComponent import com.adyen.checkout.wechatpay.WeChatPayActionComponent @@ -23,6 +24,7 @@ private val allActionProviders = listOfNotNull( runCompileOnly { AwaitComponent.PROVIDER }, runCompileOnly { QRCodeComponent.PROVIDER }, runCompileOnly { RedirectComponent.PROVIDER }, + runCompileOnly { TwintActionComponent.PROVIDER }, runCompileOnly { VoucherComponent.PROVIDER }, runCompileOnly { WeChatPayActionComponent.PROVIDER }, ) diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProvider.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProvider.kt index 951b17dccf..8efbd75626 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProvider.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProvider.kt @@ -13,6 +13,7 @@ import androidx.lifecycle.SavedStateHandle import com.adyen.checkout.adyen3ds2.internal.provider.Adyen3DS2ComponentProvider import com.adyen.checkout.await.internal.provider.AwaitComponentProvider import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.action.AwaitAction import com.adyen.checkout.components.core.action.BaseThreeds2Action @@ -26,6 +27,7 @@ import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.qrcode.internal.provider.QRCodeComponentProvider import com.adyen.checkout.redirect.internal.provider.RedirectComponentProvider +import com.adyen.checkout.twint.internal.provider.TwintActionComponentProvider import com.adyen.checkout.voucher.internal.provider.VoucherComponentProvider import com.adyen.checkout.wechatpay.internal.provider.WeChatPayActionComponentProvider @@ -46,7 +48,20 @@ internal class ActionDelegateProvider( is RedirectAction -> RedirectComponentProvider(dropInOverrideParams, localeProvider) is BaseThreeds2Action -> Adyen3DS2ComponentProvider(dropInOverrideParams, localeProvider) is VoucherAction -> VoucherComponentProvider(dropInOverrideParams, localeProvider) - is SdkAction<*> -> WeChatPayActionComponentProvider(dropInOverrideParams, localeProvider) + is SdkAction<*> -> { + when (action.paymentMethodType) { + PaymentMethodTypes.TWINT -> TwintActionComponentProvider(dropInOverrideParams, localeProvider) + PaymentMethodTypes.WECHAT_PAY_SDK -> WeChatPayActionComponentProvider( + dropInOverrideParams, + localeProvider, + ) + + else -> throw CheckoutException( + "Can't find delegate for action: ${action.type} and type: ${action.paymentMethodType}", + ) + } + } + else -> throw CheckoutException("Can't find delegate for action: ${action.type}") } diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegate.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegate.kt index 97d0b10dca..a4c1164446 100644 --- a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegate.kt +++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegate.kt @@ -235,6 +235,7 @@ internal class DefaultBoletoDelegate( paymentMethod = GenericPaymentMethod( type = paymentMethod.type, checkoutAttemptId = analyticsRepository.getCheckoutAttemptId(), + subtype = null, ), order = order, amount = componentParams.amount, diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt b/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt index 4a97ba42a1..ecbdf4e74e 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt @@ -46,7 +46,6 @@ object PaymentMethodTypes { const val PAY_BY_BANK = "paybybank" const val SCHEME = "scheme" const val SEPA = "sepadirectdebit" - const val TWINT = "twint" const val UPI = "upi" const val UPI_COLLECT = "upi_collect" const val UPI_QR = "upi_qr" @@ -56,6 +55,7 @@ object PaymentMethodTypes { const val PAY_NOW = "paynow" const val PIX = "pix" const val PROMPT_PAY = "promptpay" + const val TWINT = "twint" const val WECHAT_PAY_SDK = "wechatpaySDK" const val MULTIBANCO = "multibanco" @@ -143,6 +143,7 @@ object PaymentMethodTypes { PAY_NOW, PIX, PROMPT_PAY, + TWINT, WECHAT_PAY_SDK, MULTIBANCO, ) diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/action/SdkAction.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/SdkAction.kt index 68b9bd4186..b8d77ba4fc 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/action/SdkAction.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/SdkAction.kt @@ -64,6 +64,7 @@ data class SdkAction ( } @Suppress("UNCHECKED_CAST") return when (paymentMethodType) { + PaymentMethodTypes.TWINT -> TwintSdkData.SERIALIZER as Serializer PaymentMethodTypes.WECHAT_PAY_SDK -> WeChatPaySdkData.SERIALIZER as Serializer else -> throw CheckoutException("sdkData not found for type paymentMethodType - $paymentMethodType") } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/action/TwintSdkData.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/TwintSdkData.kt new file mode 100644 index 0000000000..88ce7ad4a8 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/TwintSdkData.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 27/10/2023. + */ + +package com.adyen.checkout.components.core.action + +import com.adyen.checkout.core.exception.ModelSerializationException +import kotlinx.parcelize.Parcelize +import org.json.JSONException +import org.json.JSONObject + +@Parcelize +data class TwintSdkData( + val token: String, +) : SdkData() { + + companion object { + + private const val TOKEN = "token" + + @JvmField + val SERIALIZER: Serializer = object : Serializer { + override fun serialize(modelObject: TwintSdkData): JSONObject { + return try { + JSONObject().apply { + putOpt(TOKEN, modelObject.token) + } + } catch (e: JSONException) { + throw ModelSerializationException(TwintSdkData::class.java, e) + } + } + + override fun deserialize(jsonObject: JSONObject): TwintSdkData { + return try { + TwintSdkData( + token = jsonObject.getString(TOKEN), + ) + } catch (e: JSONException) { + throw ModelSerializationException(TwintSdkData::class.java, e) + } + } + } + } +} diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/GenericPaymentMethod.kt b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/GenericPaymentMethod.kt index 944e8ea62f..de62ba78b7 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/GenericPaymentMethod.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/GenericPaymentMethod.kt @@ -17,10 +17,13 @@ import org.json.JSONObject data class GenericPaymentMethod( override var type: String?, override var checkoutAttemptId: String?, + var subtype: String?, ) : PaymentMethodDetails() { companion object { + private const val SUBTYPE = "subtype" + @JvmField val SERIALIZER: Serializer = object : Serializer { override fun serialize(modelObject: GenericPaymentMethod): JSONObject { @@ -28,6 +31,7 @@ data class GenericPaymentMethod( JSONObject().apply { putOpt(TYPE, modelObject.type) putOpt(CHECKOUT_ATTEMPT_ID, modelObject.checkoutAttemptId) + putOpt(SUBTYPE, modelObject.subtype) } } catch (e: JSONException) { throw ModelSerializationException(GenericPaymentMethod::class.java, e) @@ -38,6 +42,7 @@ data class GenericPaymentMethod( return GenericPaymentMethod( type = jsonObject.getStringOrNull(TYPE), checkoutAttemptId = jsonObject.getStringOrNull(CHECKOUT_ATTEMPT_ID), + subtype = jsonObject.getStringOrNull(SUBTYPE) ) } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt index fe12f6c6da..9ff95241e0 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt @@ -88,7 +88,6 @@ abstract class PaymentMethodDetails : ModelObject() { SepaPaymentMethod.PAYMENT_METHOD_TYPE -> SepaPaymentMethod.SERIALIZER SevenElevenPaymentMethod.PAYMENT_METHOD_TYPE -> SevenElevenPaymentMethod.SERIALIZER - TwintPaymentMethod.PAYMENT_METHOD_TYPE -> TwintPaymentMethod.SERIALIZER else -> GenericPaymentMethod.SERIALIZER } @Suppress("UNCHECKED_CAST") diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/TwintPaymentMethod.kt b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/TwintPaymentMethod.kt deleted file mode 100644 index d27b9f43bf..0000000000 --- a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/TwintPaymentMethod.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2023 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by oscars on 20/10/2023. - */ - -package com.adyen.checkout.components.core.paymentmethod - -import com.adyen.checkout.components.core.PaymentMethodTypes -import com.adyen.checkout.core.exception.ModelSerializationException -import com.adyen.checkout.core.internal.data.model.getStringOrNull -import kotlinx.parcelize.Parcelize -import org.json.JSONException -import org.json.JSONObject - -@Parcelize -data class TwintPaymentMethod( - override var type: String?, - var subtype: String?, - override var checkoutAttemptId: String?, -) : PaymentMethodDetails() { - - companion object { - - const val PAYMENT_METHOD_TYPE = PaymentMethodTypes.TWINT - - private const val SUBTYPE = "subtype" - - @JvmField - val SERIALIZER: Serializer = object : Serializer { - override fun serialize(modelObject: TwintPaymentMethod): JSONObject { - return try { - JSONObject().apply { - putOpt(TYPE, modelObject.type) - putOpt(SUBTYPE, modelObject.subtype) - putOpt(CHECKOUT_ATTEMPT_ID, modelObject.checkoutAttemptId) - } - } catch (e: JSONException) { - throw ModelSerializationException(TwintPaymentMethod::class.java, e) - } - } - - override fun deserialize(jsonObject: JSONObject): TwintPaymentMethod { - return TwintPaymentMethod( - type = jsonObject.getStringOrNull(TYPE), - subtype = jsonObject.getStringOrNull(SUBTYPE), - checkoutAttemptId = jsonObject.getStringOrNull(CHECKOUT_ATTEMPT_ID), - ) - } - } - } -} diff --git a/drop-in/build.gradle b/drop-in/build.gradle index 085efd47b2..3456563921 100644 --- a/drop-in/build.gradle +++ b/drop-in/build.gradle @@ -74,6 +74,7 @@ dependencies { api project(':sepa') api project(':seven-eleven') api project(':sessions-core') + api project(':twint') api project(':upi') api project(':wechatpay') diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt index e6df140c20..9e906071c4 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt @@ -45,6 +45,7 @@ import com.adyen.checkout.openbanking.OpenBankingConfiguration import com.adyen.checkout.payeasy.PayEasyConfiguration import com.adyen.checkout.sepa.SepaConfiguration import com.adyen.checkout.seveneleven.SevenElevenConfiguration +import com.adyen.checkout.twint.TwintActionConfiguration import com.adyen.checkout.upi.UPIConfiguration import kotlinx.parcelize.Parcelize import java.util.Locale diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInActivity.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInActivity.kt index 0cfd2e0709..ae0f8534c1 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInActivity.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInActivity.kt @@ -37,6 +37,7 @@ import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.internal.util.createLocalizedContext import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.internal.util.adyenLog +import com.adyen.checkout.core.internal.util.runCompileOnly import com.adyen.checkout.dropin.AddressLookupDropInServiceResult import com.adyen.checkout.dropin.BalanceDropInServiceResult import com.adyen.checkout.dropin.BaseDropInServiceResult @@ -62,6 +63,7 @@ import com.adyen.checkout.giftcard.GiftCardComponentState import com.adyen.checkout.redirect.RedirectComponent import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionPaymentResult +import com.adyen.checkout.twint.Twint import com.adyen.checkout.wechatpay.WeChatPayUtils import kotlinx.coroutines.launch import com.adyen.checkout.ui.core.R as UICoreR @@ -164,6 +166,10 @@ internal class DropInActivity : initObservers() startDropInService() + + runCompileOnly { + Twint.initialize(this) + } } private fun noDialogPresent(): Boolean { diff --git a/instant/src/main/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegate.kt b/instant/src/main/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegate.kt index c623dafb3d..aae741547a 100644 --- a/instant/src/main/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegate.kt +++ b/instant/src/main/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegate.kt @@ -55,6 +55,7 @@ internal class DefaultInstantPaymentDelegate( paymentMethod = GenericPaymentMethod( type = paymentMethod.type, checkoutAttemptId = analyticsRepository.getCheckoutAttemptId(), + subtype = getSubtype(paymentMethod), ), order = order, amount = componentParams.amount, @@ -62,6 +63,11 @@ internal class DefaultInstantPaymentDelegate( return InstantComponentState(paymentComponentData, isInputValid = true, isReady = true) } + private fun getSubtype(paymentMethod: PaymentMethod): String? = when (paymentMethod.type) { + PaymentMethodTypes.TWINT -> SDK_SUBTYPE + else -> null + } + override fun initialize(coroutineScope: CoroutineScope) { setupAnalytics(coroutineScope) } @@ -95,4 +101,8 @@ internal class DefaultInstantPaymentDelegate( override fun onCleared() { removeObserver() } + + companion object { + private const val SDK_SUBTYPE = "sdk" + } } diff --git a/twint/build.gradle b/twint/build.gradle index 8f9f5dd588..5d8e5f0213 100644 --- a/twint/build.gradle +++ b/twint/build.gradle @@ -39,12 +39,9 @@ android { dependencies { // Checkout - api project(':action-core') api project(':ui-core') - api project(':sessions-core') // Dependencies - implementation libraries.material implementation files('./libs/TwintSdk-android-7.0.0.aar') //Tests diff --git a/twint/src/main/java/com/adyen/checkout/twint/Twint.kt b/twint/src/main/java/com/adyen/checkout/twint/Twint.kt new file mode 100644 index 0000000000..d46d7ba9b4 --- /dev/null +++ b/twint/src/main/java/com/adyen/checkout/twint/Twint.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 17/11/2023. + */ + +package com.adyen.checkout.twint + +import androidx.activity.ComponentActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import ch.twint.payment.sdk.Twint +import ch.twint.payment.sdk.TwintPayResult +import com.adyen.checkout.core.exception.CheckoutException + +object Twint { + + private var twint: Twint? = null + + private var onResultListener: ((TwintPayResult) -> Unit)? = null + + fun initialize(activity: ComponentActivity) { + twint = Twint(activity, ::onTwintResult) + + val observer = object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + onDestroy() + super.onDestroy(owner) + } + } + activity.lifecycle.addObserver(observer) + } + + private fun onTwintResult(result: TwintPayResult) { + onResultListener?.invoke(result) + } + + internal fun setResultListener(listener: (TwintPayResult) -> Unit) { + onResultListener = listener + } + + internal fun payWithCode(code: String) { + twint?.payWithCode(code) ?: throw CheckoutException("Twint not initialised before payment.") + } + + private fun onDestroy() { + onResultListener = null + twint = null + } +} diff --git a/twint/src/main/java/com/adyen/checkout/twint/TwintActionComponent.kt b/twint/src/main/java/com/adyen/checkout/twint/TwintActionComponent.kt new file mode 100644 index 0000000000..5c0895adfe --- /dev/null +++ b/twint/src/main/java/com/adyen/checkout/twint/TwintActionComponent.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 18/10/2023. + */ + +package com.adyen.checkout.twint + +import android.app.Activity +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.components.core.internal.ActionComponent +import com.adyen.checkout.components.core.internal.ActionComponentEvent +import com.adyen.checkout.components.core.internal.ActionComponentEventHandler +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog +import com.adyen.checkout.twint.internal.provider.TwintActionComponentProvider +import com.adyen.checkout.twint.internal.ui.TwintDelegate +import com.adyen.checkout.ui.core.internal.ui.ComponentViewType +import com.adyen.checkout.ui.core.internal.ui.ViewableComponent +import kotlinx.coroutines.flow.Flow + +class TwintActionComponent internal constructor( + override val delegate: TwintDelegate, + internal val actionComponentEventHandler: ActionComponentEventHandler, +) : ViewModel(), + ActionComponent, + ViewableComponent { + + override val viewFlow: Flow = delegate.viewFlow + + init { + delegate.initialize(viewModelScope) + } + + internal fun observe(lifecycleOwner: LifecycleOwner, callback: (ActionComponentEvent) -> Unit) { + delegate.observe(lifecycleOwner, viewModelScope, callback) + } + + internal fun removeObserver() { + delegate.removeObserver() + } + + override fun canHandleAction(action: Action): Boolean { + return PROVIDER.canHandleAction(action) + } + + override fun handleAction(action: Action, activity: Activity) { + delegate.handleAction(action, activity) + } + + override fun onCleared() { + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } + super.onCleared() + delegate.onCleared() + } + + companion object { + + @JvmField + val PROVIDER = TwintActionComponentProvider() + } +} diff --git a/twint/src/main/java/com/adyen/checkout/twint/TwintConfiguration.kt b/twint/src/main/java/com/adyen/checkout/twint/TwintActionConfiguration.kt similarity index 77% rename from twint/src/main/java/com/adyen/checkout/twint/TwintConfiguration.kt rename to twint/src/main/java/com/adyen/checkout/twint/TwintActionConfiguration.kt index 1b1bb7b156..b01898a6f7 100644 --- a/twint/src/main/java/com/adyen/checkout/twint/TwintConfiguration.kt +++ b/twint/src/main/java/com/adyen/checkout/twint/TwintActionConfiguration.kt @@ -9,28 +9,29 @@ package com.adyen.checkout.twint import android.content.Context -import com.adyen.checkout.action.core.GenericActionConfiguration -import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale +/** + * Configuration class for the [TwintActionComponent]. + */ @Parcelize -class TwintConfiguration private constructor( +class TwintActionConfiguration private constructor( override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, override val amount: Amount?, - internal val genericActionConfiguration: GenericActionConfiguration, ) : Configuration { - class Builder : ActionHandlingPaymentMethodConfigurationBuilder { + class Builder : BaseConfigurationBuilder { /** * Initialize a configuration builder with the required fields. @@ -76,23 +77,22 @@ class TwintConfiguration private constructor( clientKey: String ) : super(shopperLocale, environment, clientKey) - override fun buildInternal(): TwintConfiguration { - return TwintConfiguration( + override fun buildInternal(): TwintActionConfiguration { + return TwintActionConfiguration( shopperLocale = shopperLocale, environment = environment, clientKey = clientKey, analyticsConfiguration = analyticsConfiguration, amount = amount, - genericActionConfiguration = genericActionConfigurationBuilder.build(), ) } } } -fun CheckoutConfiguration.twint( - configuration: @CheckoutConfigurationMarker TwintConfiguration.Builder.() -> Unit = {} +fun CheckoutConfiguration.twintAction( + configuration: @CheckoutConfigurationMarker TwintActionConfiguration.Builder.() -> Unit = {} ): CheckoutConfiguration { - val config = TwintConfiguration.Builder(environment, clientKey) + val config = TwintActionConfiguration.Builder(environment, clientKey) .apply { shopperLocale?.let { setShopperLocale(it) } amount?.let { setAmount(it) } @@ -104,11 +104,11 @@ fun CheckoutConfiguration.twint( return this } -internal fun CheckoutConfiguration.getTwintConfiguration(): TwintConfiguration? { - return getActionConfiguration(TwintConfiguration::class.java) +internal fun CheckoutConfiguration.getTwintActionConfiguration(): TwintActionConfiguration? { + return getActionConfiguration(TwintActionConfiguration::class.java) } -internal fun TwintConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { +internal fun TwintActionConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { return CheckoutConfiguration( shopperLocale = shopperLocale, environment = environment, @@ -117,9 +117,5 @@ internal fun TwintConfiguration.toCheckoutConfiguration(): CheckoutConfiguration analyticsConfiguration = analyticsConfiguration, ) { addActionConfiguration(this@toCheckoutConfiguration) - - genericActionConfiguration.getAllConfigurations().forEach { - addActionConfiguration(it) - } } } diff --git a/twint/src/main/java/com/adyen/checkout/twint/TwintComponent.kt b/twint/src/main/java/com/adyen/checkout/twint/TwintComponent.kt deleted file mode 100644 index b6b554aefb..0000000000 --- a/twint/src/main/java/com/adyen/checkout/twint/TwintComponent.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2023 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by oscars on 18/10/2023. - */ - -package com.adyen.checkout.twint - -import androidx.activity.ComponentActivity -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.adyen.checkout.action.core.internal.ActionHandlingComponent -import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent -import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate -import com.adyen.checkout.components.core.PaymentMethodTypes -import com.adyen.checkout.components.core.internal.ComponentEventHandler -import com.adyen.checkout.components.core.internal.PaymentComponent -import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.AdyenLogLevel -import com.adyen.checkout.core.internal.util.adyenLog -import com.adyen.checkout.twint.internal.provider.TwintComponentProvider -import com.adyen.checkout.twint.internal.ui.TwintDelegate -import com.adyen.checkout.ui.core.internal.ui.ComponentViewType -import com.adyen.checkout.ui.core.internal.ui.ViewableComponent -import kotlinx.coroutines.flow.Flow - -class TwintComponent internal constructor( - private val twintDelegate: TwintDelegate, - private val genericActionDelegate: GenericActionDelegate, - private val actionHandlingComponent: DefaultActionHandlingComponent, - internal val componentEventHandler: ComponentEventHandler , -) : ViewModel(), - PaymentComponent, - ViewableComponent, - ActionHandlingComponent by actionHandlingComponent { - - override val delegate: ComponentDelegate get() = actionHandlingComponent.activeDelegate - - override val viewFlow: Flow = genericActionDelegate.viewFlow - - init { - twintDelegate.initialize(viewModelScope) - componentEventHandler.initialize(viewModelScope) - } - - internal fun observe( - lifecycleOwner: LifecycleOwner, - callback: (PaymentComponentEvent ) -> Unit - ) { - twintDelegate.observe(lifecycleOwner, viewModelScope, callback) - } - - internal fun removeObserver() { - twintDelegate.removeObserver() - } - - fun startTwintScreen(activity: ComponentActivity) { - twintDelegate.startTwintScreen(activity) - } - - override fun setInteractionBlocked(isInteractionBlocked: Boolean) { - adyenLog(AdyenLogLevel.WARN) { "Interaction with TwintComponent can't be blocked" } - } - - override fun onCleared() { - adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } - super.onCleared() - twintDelegate.onCleared() - componentEventHandler.onCleared() - } - - companion object { - - @JvmField - val PROVIDER = TwintComponentProvider() - - @JvmField - val PAYMENT_METHOD_TYPES = listOf(PaymentMethodTypes.TWINT) - } -} diff --git a/twint/src/main/java/com/adyen/checkout/twint/TwintComponentState.kt b/twint/src/main/java/com/adyen/checkout/twint/TwintComponentState.kt deleted file mode 100644 index c0111e8477..0000000000 --- a/twint/src/main/java/com/adyen/checkout/twint/TwintComponentState.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) 2023 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by oscars on 18/10/2023. - */ - -package com.adyen.checkout.twint - -import com.adyen.checkout.components.core.PaymentComponentData -import com.adyen.checkout.components.core.PaymentComponentState -import com.adyen.checkout.components.core.paymentmethod.TwintPaymentMethod - -data class TwintComponentState( - override val data: PaymentComponentData , - override val isInputValid: Boolean, - override val isReady: Boolean, -) : PaymentComponentState diff --git a/twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintActionComponentProvider.kt b/twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintActionComponentProvider.kt new file mode 100644 index 0000000000..5376ffe4c3 --- /dev/null +++ b/twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintActionComponentProvider.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 20/10/2023. + */ + +package com.adyen.checkout.twint.internal.provider + +import android.app.Application +import androidx.annotation.RestrictTo +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import androidx.savedstate.SavedStateRegistryOwner +import com.adyen.checkout.components.core.ActionComponentCallback +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.components.core.action.SdkAction +import com.adyen.checkout.components.core.internal.ActionObserverRepository +import com.adyen.checkout.components.core.internal.DefaultActionComponentEventHandler +import com.adyen.checkout.components.core.internal.PaymentDataRepository +import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper +import com.adyen.checkout.components.core.internal.util.get +import com.adyen.checkout.components.core.internal.util.viewModelFactory +import com.adyen.checkout.core.internal.util.LocaleProvider +import com.adyen.checkout.twint.TwintActionComponent +import com.adyen.checkout.twint.TwintActionConfiguration +import com.adyen.checkout.twint.internal.ui.DefaultTwintDelegate +import com.adyen.checkout.twint.internal.ui.TwintDelegate +import com.adyen.checkout.twint.toCheckoutConfiguration + +class TwintActionComponentProvider +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +constructor( + private val dropInOverrideParams: DropInOverrideParams? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), +) : ActionComponentProvider { + + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + application: Application, + checkoutConfiguration: CheckoutConfiguration, + callback: ActionComponentCallback, + key: String? + ): TwintActionComponent { + val twintFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> + val twintDelegate = getDelegate(checkoutConfiguration, savedStateHandle, application) + TwintActionComponent( + delegate = twintDelegate, + actionComponentEventHandler = DefaultActionComponentEventHandler(callback), + ) + } + + return ViewModelProvider(viewModelStoreOwner, twintFactory)[key, TwintActionComponent::class.java] + .also { component -> + component.observe(lifecycleOwner, component.actionComponentEventHandler::onActionComponentEvent) + } + } + + override fun getDelegate( + checkoutConfiguration: CheckoutConfiguration, + savedStateHandle: SavedStateHandle, + application: Application, + ): TwintDelegate { + val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) + + return DefaultTwintDelegate( + observerRepository = ActionObserverRepository(), + componentParams = componentParams, + paymentDataRepository = PaymentDataRepository(savedStateHandle), + ) + } + + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + application: Application, + configuration: TwintActionConfiguration, + callback: ActionComponentCallback, + key: String?, + ): TwintActionComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + application = application, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + callback = callback, + key = key, + ) + } + + override val supportedActionTypes: List = listOf(SdkAction.ACTION_TYPE) + + override fun canHandleAction(action: Action): Boolean { + return supportedActionTypes.contains(action.type) && PAYMENT_METHODS.contains(action.paymentMethodType) + } + + override fun providesDetails(action: Action): Boolean { + return true + } + + companion object { + private val PAYMENT_METHODS = listOf(PaymentMethodTypes.TWINT) + } +} diff --git a/twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintComponentProvider.kt b/twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintComponentProvider.kt deleted file mode 100644 index f22391ce69..0000000000 --- a/twint/src/main/java/com/adyen/checkout/twint/internal/provider/TwintComponentProvider.kt +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright (c) 2023 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by oscars on 20/10/2023. - */ - -package com.adyen.checkout.twint.internal.provider - -import android.app.Application -import androidx.annotation.RestrictTo -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelStoreOwner -import androidx.savedstate.SavedStateRegistryOwner -import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent -import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider -import com.adyen.checkout.components.core.CheckoutConfiguration -import com.adyen.checkout.components.core.ComponentCallback -import com.adyen.checkout.components.core.Order -import com.adyen.checkout.components.core.PaymentMethod -import com.adyen.checkout.components.core.internal.DefaultComponentEventHandler -import com.adyen.checkout.components.core.internal.PaymentObserverRepository -import com.adyen.checkout.components.core.internal.data.api.AnalyticsMapper -import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepositoryData -import com.adyen.checkout.components.core.internal.data.api.AnalyticsService -import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository -import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams -import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper -import com.adyen.checkout.components.core.internal.util.get -import com.adyen.checkout.components.core.internal.util.viewModelFactory -import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.data.api.HttpClientFactory -import com.adyen.checkout.core.internal.util.LocaleProvider -import com.adyen.checkout.sessions.core.CheckoutSession -import com.adyen.checkout.sessions.core.SessionComponentCallback -import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler -import com.adyen.checkout.sessions.core.internal.SessionInteractor -import com.adyen.checkout.sessions.core.internal.SessionSavedStateHandleContainer -import com.adyen.checkout.sessions.core.internal.data.api.SessionRepository -import com.adyen.checkout.sessions.core.internal.data.api.SessionService -import com.adyen.checkout.sessions.core.internal.provider.SessionPaymentComponentProvider -import com.adyen.checkout.twint.TwintComponent -import com.adyen.checkout.twint.TwintComponentState -import com.adyen.checkout.twint.TwintConfiguration -import com.adyen.checkout.twint.internal.ui.DefaultTwintDelegate -import com.adyen.checkout.twint.toCheckoutConfiguration - -class TwintComponentProvider -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -constructor( - private val dropInOverrideParams: DropInOverrideParams? = null, - private val localeProvider: LocaleProvider = LocaleProvider(), - private val analyticsRepository: AnalyticsRepository? = null, -) : - PaymentComponentProvider< - TwintComponent, - TwintConfiguration, - TwintComponentState, - ComponentCallback , - >, - SessionPaymentComponentProvider< - TwintComponent, - TwintConfiguration, - TwintComponentState, - SessionComponentCallback , - > { - - @Suppress("LongMethod") - override fun get( - savedStateRegistryOwner: SavedStateRegistryOwner, - viewModelStoreOwner: ViewModelStoreOwner, - lifecycleOwner: LifecycleOwner, - paymentMethod: PaymentMethod, - checkoutConfiguration: CheckoutConfiguration, - application: Application, - componentCallback: ComponentCallback , - order: Order?, - key: String? - ): TwintComponent { - assertSupported(paymentMethod) - - val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( - checkoutConfiguration = checkoutConfiguration, - deviceLocale = localeProvider.getLocale(application), - dropInOverrideParams = dropInOverrideParams, - componentSessionParams = null, - ) - val viewModelFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - - val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( - analyticsRepositoryData = AnalyticsRepositoryData( - application = application, - componentParams = componentParams, - paymentMethod = paymentMethod, - ), - analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), - ), - analyticsMapper = AnalyticsMapper(), - ) - - val twintDelegate = DefaultTwintDelegate( - observerRepository = PaymentObserverRepository(), - paymentMethod = paymentMethod, - order = order, - componentParams = componentParams, - analyticsRepository = analyticsRepository, - ) - - val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( - checkoutConfiguration = checkoutConfiguration, - savedStateHandle = savedStateHandle, - application = application, - ) - - TwintComponent( - twintDelegate = twintDelegate, - genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, twintDelegate), - componentEventHandler = DefaultComponentEventHandler(), - ) - } - - return ViewModelProvider(viewModelStoreOwner, viewModelFactory)[key, TwintComponent::class.java] - .also { component -> - component.observe(lifecycleOwner) { - component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) - } - } - } - - override fun get( - savedStateRegistryOwner: SavedStateRegistryOwner, - viewModelStoreOwner: ViewModelStoreOwner, - lifecycleOwner: LifecycleOwner, - paymentMethod: PaymentMethod, - configuration: TwintConfiguration, - application: Application, - componentCallback: ComponentCallback , - order: Order?, - key: String?, - ): TwintComponent { - return get( - savedStateRegistryOwner = savedStateRegistryOwner, - viewModelStoreOwner = viewModelStoreOwner, - lifecycleOwner = lifecycleOwner, - paymentMethod = paymentMethod, - checkoutConfiguration = configuration.toCheckoutConfiguration(), - application = application, - componentCallback = componentCallback, - order = order, - key = key, - ) - } - - @Suppress("LongMethod") - override fun get( - savedStateRegistryOwner: SavedStateRegistryOwner, - viewModelStoreOwner: ViewModelStoreOwner, - lifecycleOwner: LifecycleOwner, - checkoutSession: CheckoutSession, - paymentMethod: PaymentMethod, - checkoutConfiguration: CheckoutConfiguration, - application: Application, - componentCallback: SessionComponentCallback , - key: String? - ): TwintComponent { - assertSupported(paymentMethod) - - val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( - checkoutConfiguration = checkoutConfiguration, - deviceLocale = localeProvider.getLocale(application), - dropInOverrideParams = dropInOverrideParams, - componentSessionParams = null, - ) - val viewModelFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) - - val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( - analyticsRepositoryData = AnalyticsRepositoryData( - application = application, - componentParams = componentParams, - paymentMethod = paymentMethod, - sessionId = checkoutSession.sessionSetupResponse.id, - ), - analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), - ), - analyticsMapper = AnalyticsMapper(), - ) - - val twintDelegate = DefaultTwintDelegate( - observerRepository = PaymentObserverRepository(), - paymentMethod = paymentMethod, - order = checkoutSession.order, - componentParams = componentParams, - analyticsRepository = analyticsRepository, - ) - - val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( - checkoutConfiguration = checkoutConfiguration, - savedStateHandle = savedStateHandle, - application = application, - ) - - val sessionSavedStateHandleContainer = SessionSavedStateHandleContainer( - savedStateHandle = savedStateHandle, - checkoutSession = checkoutSession, - ) - - val sessionInteractor = SessionInteractor( - sessionRepository = SessionRepository( - sessionService = SessionService(httpClient), - clientKey = componentParams.clientKey, - ), - sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, - ) - - val sessionComponentEventHandler = SessionComponentEventHandler ( - sessionInteractor = sessionInteractor, - sessionSavedStateHandleContainer = sessionSavedStateHandleContainer, - ) - - TwintComponent( - twintDelegate = twintDelegate, - genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, twintDelegate), - componentEventHandler = sessionComponentEventHandler, - ) - } - - return ViewModelProvider(viewModelStoreOwner, viewModelFactory)[key, TwintComponent::class.java] - .also { component -> - component.observe(lifecycleOwner) { - component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) - } - } - } - - override fun get( - savedStateRegistryOwner: SavedStateRegistryOwner, - viewModelStoreOwner: ViewModelStoreOwner, - lifecycleOwner: LifecycleOwner, - checkoutSession: CheckoutSession, - paymentMethod: PaymentMethod, - configuration: TwintConfiguration, - application: Application, - componentCallback: SessionComponentCallback , - key: String? - ): TwintComponent { - return get( - savedStateRegistryOwner = savedStateRegistryOwner, - viewModelStoreOwner = viewModelStoreOwner, - lifecycleOwner = lifecycleOwner, - checkoutSession = checkoutSession, - paymentMethod = paymentMethod, - checkoutConfiguration = configuration.toCheckoutConfiguration(), - application = application, - componentCallback = componentCallback, - key = key, - ) - } - - private fun assertSupported(paymentMethod: PaymentMethod) { - if (!isPaymentMethodSupported(paymentMethod)) { - throw ComponentException("Unsupported payment method ${paymentMethod.type}") - } - } - - override fun isPaymentMethodSupported(paymentMethod: PaymentMethod): Boolean { - return TwintComponent.PAYMENT_METHOD_TYPES.contains(paymentMethod.type) - } -} diff --git a/twint/src/main/java/com/adyen/checkout/twint/internal/ui/DefaultTwintDelegate.kt b/twint/src/main/java/com/adyen/checkout/twint/internal/ui/DefaultTwintDelegate.kt index 5cc2458b94..3b73214f71 100644 --- a/twint/src/main/java/com/adyen/checkout/twint/internal/ui/DefaultTwintDelegate.kt +++ b/twint/src/main/java/com/adyen/checkout/twint/internal/ui/DefaultTwintDelegate.kt @@ -8,80 +8,56 @@ package com.adyen.checkout.twint.internal.ui -import androidx.activity.ComponentActivity +import android.app.Activity import androidx.lifecycle.LifecycleOwner -import ch.twint.payment.sdk.Twint import ch.twint.payment.sdk.TwintPayResult -import com.adyen.checkout.components.core.OrderRequest -import com.adyen.checkout.components.core.PaymentComponentData -import com.adyen.checkout.components.core.PaymentMethod -import com.adyen.checkout.components.core.PaymentMethodTypes -import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.components.core.internal.PaymentObserverRepository -import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.ActionComponentData +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.components.core.action.SdkAction +import com.adyen.checkout.components.core.action.TwintSdkData +import com.adyen.checkout.components.core.internal.ActionComponentEvent +import com.adyen.checkout.components.core.internal.ActionObserverRepository +import com.adyen.checkout.components.core.internal.PaymentDataRepository +import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams import com.adyen.checkout.components.core.internal.util.bufferedChannel -import com.adyen.checkout.components.core.paymentmethod.TwintPaymentMethod import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.util.adyenLog -import com.adyen.checkout.twint.TwintComponentState +import com.adyen.checkout.twint.Twint +import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch +import org.json.JSONObject internal class DefaultTwintDelegate( - private val observerRepository: PaymentObserverRepository, - private val paymentMethod: PaymentMethod, - private val order: OrderRequest?, - override val componentParams: ComponentParams, - private val analyticsRepository: AnalyticsRepository, + private val observerRepository: ActionObserverRepository, + override val componentParams: GenericComponentParams, + private val paymentDataRepository: PaymentDataRepository, ) : TwintDelegate { - private val _componentStateFlow = MutableStateFlow(createComponentState()) - override val componentStateFlow: Flow = _componentStateFlow + private val detailsChannel: Channel = bufferedChannel() + override val detailsFlow: Flow = detailsChannel.receiveAsFlow() private val exceptionChannel: Channel = bufferedChannel() override val exceptionFlow: Flow = exceptionChannel.receiveAsFlow() - private val submitChannel: Channel = bufferedChannel() - override val submitFlow: Flow = submitChannel.receiveAsFlow() + override val viewFlow: Flow = MutableStateFlow(TwintComponentViewType) - override fun initialize(coroutineScope: CoroutineScope) { - setupAnalytics(coroutineScope) - - componentStateFlow.onEach { - onState(it) - }.launchIn(coroutineScope) - } - - private fun setupAnalytics(coroutineScope: CoroutineScope) { - adyenLog(AdyenLogLevel.DEBUG) { "setupAnalytics" } - coroutineScope.launch { - analyticsRepository.setupAnalytics() - } - } - - private fun onState(state: TwintComponentState) { - if (state.isValid) { - submitChannel.trySend(state) - } - } + override fun initialize(coroutineScope: CoroutineScope) = Unit override fun observe( lifecycleOwner: LifecycleOwner, coroutineScope: CoroutineScope, - callback: (PaymentComponentEvent ) -> Unit + callback: (ActionComponentEvent) -> Unit ) { observerRepository.addObservers( - stateFlow = componentStateFlow, + detailsFlow = detailsFlow, exceptionFlow = exceptionFlow, - submitFlow = submitFlow, + permissionFlow = null, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, callback = callback, @@ -92,40 +68,62 @@ internal class DefaultTwintDelegate( observerRepository.removeObservers() } - private fun createComponentState(): TwintComponentState { - val paymentMethod = TwintPaymentMethod( - type = paymentMethod.type, - subtype = "sdk", - checkoutAttemptId = analyticsRepository.getCheckoutAttemptId(), - ) + override fun handleAction(action: Action, activity: Activity) { + @Suppress("UNCHECKED_CAST") + val sdkAction = (action as? SdkAction ) + if (sdkAction == null) { + exceptionChannel.trySend(ComponentException("Unsupported action")) + return + } - val paymentComponentData = PaymentComponentData( - paymentMethod = paymentMethod, - order = order, - amount = componentParams.amount, - ) + val paymentData = action.paymentData + paymentDataRepository.paymentData = paymentData + if (paymentData == null) { + adyenLog(AdyenLogLevel.ERROR) { "Payment data is null" } + exceptionChannel.trySend(ComponentException("Payment data is null")) + return + } - return TwintComponentState( - data = paymentComponentData, - isInputValid = true, - isReady = true, - ) + val sdkData = action.sdkData + if (sdkData == null) { + exceptionChannel.trySend(ComponentException("SDK Data is null")) + return + } + + Twint.setResultListener(::handleTwintResult) + try { + Twint.payWithCode(sdkData.token) + } catch (e: CheckoutException) { + exceptionChannel.trySend(e) + } } - override fun startTwintScreen(activity: ComponentActivity) { - val twint = Twint(activity) { result -> - when (result) { - TwintPayResult.TW_B_SUCCESS -> TODO() - TwintPayResult.TW_B_ERROR -> TODO() - TwintPayResult.TW_B_APP_NOT_INSTALLED -> TODO() + private fun handleTwintResult(result: TwintPayResult) { + when (result) { + TwintPayResult.TW_B_SUCCESS -> { + detailsChannel.trySend(createActionComponentData()) + } + + TwintPayResult.TW_B_ERROR -> { + onError(ComponentException("Twint encountered an error.")) + } + + TwintPayResult.TW_B_APP_NOT_INSTALLED -> { + onError(ComponentException("Twint app not installed.")) } } + } - twint.payWithCode("test") + private fun createActionComponentData(): ActionComponentData { + return ActionComponentData( + // The backend doesn't accept null, so we have to send an empty json object. + details = JSONObject(), + paymentData = paymentDataRepository.paymentData, + ) } - override fun getPaymentMethodType(): String { - return paymentMethod.type ?: PaymentMethodTypes.UNKNOWN + override fun onError(e: CheckoutException) { + exceptionChannel.trySend(e) } override fun onCleared() { diff --git a/twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintDelegate.kt b/twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintDelegate.kt index 3783532610..98e0c1adbd 100644 --- a/twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintDelegate.kt +++ b/twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintDelegate.kt @@ -8,17 +8,13 @@ package com.adyen.checkout.twint.internal.ui -import androidx.activity.ComponentActivity -import com.adyen.checkout.components.core.internal.ui.PaymentComponentDelegate -import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.twint.TwintComponentState -import kotlinx.coroutines.flow.Flow +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.internal.ui.ActionDelegate +import com.adyen.checkout.components.core.internal.ui.DetailsEmittingDelegate +import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate -internal interface TwintDelegate : PaymentComponentDelegate { - - val componentStateFlow: Flow - - val exceptionFlow: Flow - - fun startTwintScreen(activity: ComponentActivity) -} +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface TwintDelegate : + ActionDelegate, + DetailsEmittingDelegate, + ViewProvidingDelegate diff --git a/twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintViewProvider.kt b/twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintViewProvider.kt new file mode 100644 index 0000000000..bb3e31659e --- /dev/null +++ b/twint/src/main/java/com/adyen/checkout/twint/internal/ui/TwintViewProvider.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 31/10/2023. + */ + +package com.adyen.checkout.twint.internal.ui + +import android.content.Context +import com.adyen.checkout.ui.core.internal.ui.ComponentView +import com.adyen.checkout.ui.core.internal.ui.ComponentViewType +import com.adyen.checkout.ui.core.internal.ui.ViewProvider +import com.adyen.checkout.ui.core.internal.ui.view.PaymentInProgressView + +internal object TwintViewProvider : ViewProvider { + + override fun getView( + viewType: ComponentViewType, + context: Context, + ): ComponentView = when (viewType) { + TwintComponentViewType -> PaymentInProgressView(context) + else -> throw IllegalArgumentException("Unsupported view type") + } +} + +internal object TwintComponentViewType : ComponentViewType { + override val viewProvider: ViewProvider = TwintViewProvider +} diff --git a/wechatpay/src/main/java/com/adyen/checkout/wechatpay/internal/provider/WeChatPayActionComponentProvider.kt b/wechatpay/src/main/java/com/adyen/checkout/wechatpay/internal/provider/WeChatPayActionComponentProvider.kt index 8b199b49ef..301ea6af84 100644 --- a/wechatpay/src/main/java/com/adyen/checkout/wechatpay/internal/provider/WeChatPayActionComponentProvider.kt +++ b/wechatpay/src/main/java/com/adyen/checkout/wechatpay/internal/provider/WeChatPayActionComponentProvider.kt @@ -115,8 +115,7 @@ constructor( ) } - override val supportedActionTypes: List - get() = listOf(SdkAction.ACTION_TYPE) + override val supportedActionTypes: List