From f2ace167194c3fdac693974d073d429ab45602d6 Mon Sep 17 00:00:00 2001
From: Miguel Gasca
l}4mnGciGMs`=ohE1lj4xGH2RHoBtl7mp$6+xJ`Txc zW-5`ZA;A0$1SlS6V`zl4ibP!d<3P|!aY#aIrg07$`l)Kj2$GdP4jN47$}(X?Kh<5O zi3jP^Oe7M9{v|^>NV3pJZc^z&k?5@b!Sq0x5r_x7No5G?sx6uEg9dNVYz?hY+JBfw zV&72EKbnKeD$$Y|Y3X1ja)y5VkuquG!9utGNc5JZt3N`{CJ_&Mw^--xLvcwuHV*%= z37xnE+M5%xp&K(fdh$fYW1;KO0A2VZ z5!e0>x|@D5MZqS}9zVpj|Ia+Ym48AM0kVxzEVNsu9JT*FOyTWxwMkSw-qPCnbk1#P zhkuwv#p5lF^7I32@-!*XKZZob<1OvfDc^H{yzQ<_R6N1{X`IF!P1Zj{<9|(S5GByA z7Adg6LKaebCum$x_C7MLAN{kG;Hi}?f)uZOn>Vr-E_-C zqZWGXLQ95 wKSVz{!Erz zL@(!9xuL1&l6yjvPHk&)2{(TCs>ITiQ?K{^5HqCrgKnp&m TRt$~(V_@fL{QJ_> LN)W&_GMAgr4_r% `q75A4YD zXp}4up~>>NpDYhn%JK-SJP!-Y@>sS!55UXvsKGoBb CV>Hq)#r*obD{{EU?j+a`CnOu#YWs{p ta_sT{Qcq2-TL_Yon(@hS&IDn``gCXw1}*rXqC&X#eeMK>e|KDoMDjn^7i`n z`M8d-t9haM_W8`N#@N8qwTi9h*yO>Ty1$sW$*IGyfTr~B^YQ8MvV^Jh?enR3pSzQ^ z D=bE ziL9k@o1$!(*MGm#?&a*WhO7AW_qdI(*um7vsKUgezT3su_3-q$kFoLT@8i?p>fPt} z^Y+H2!29_6$ELx>qraqXnYfRz(YDN{a-9DC{rvj;u70JXZJFiP;=Y%*@#ydJ>+!je zvg_aI-)e50akm!)r--O1UZX_l^krM#51^6K#T^MCiMc%bUu=%jC&wv4Xn-RG8B zi{{wm>)+}2@%5;5o~n4Eyp^@!&fVhC->rS5!=JqU{r%?G =-cM7fv3u=#J!ic!JN7F z@b$5SsDGz&o!`ye?BMFisKV#j<>AoY?&a;hmbK~I=KcHq!kxR?!_^Z~TsHs!2k%Kl zK~#9!?VR^l6iF0?n=lpwLuN>V;~*d?NDvfO2};xvM3l5fOsI%i6vZsMin}VtU0vh4 z;lEs27?|m~)&07=dS*GN?vGk^`#VkdtM^`2kAI>_Nl8gbNl8gbNl8gbNl8gbNl8gb z{cn}v^(HJW;^^})r*dPaemNhtbeP{2uzB_NL@y2G_+}?tRNuZO0X-AQ%vj~YCBR(j zV8N=5x8#@IFj(8xcJxJ6l6}ms5vJBX3wE8;tNb>5PT2NZ4~8(aN9NrMD*}zWV{Lj1 zD}O?v)7F`F+pT)k_SyseBLiwMX*OQ1G8%5OgV|e&ptqTHs~6^^sRqQ%HkdQpHx%Mj zoh_a@b`69!`6%84R_nz>zfFVCyUn@%0IXJshpvarj>qQQ`hnMsaOSf+AatuqyFI|G zQ8@J9kU9OPNxN=fwnsQLehqh-wR;83vVS@RGha$Z%)=RG{d$nyUBS?+;f=m++OG-O z-4YC42cfG>`^6mscAJc)LS0>5Ra(hmh66D-n$OIl8i8N80fVaA;qNn?#m_c#hpd1% z`l*FK-7?_!o;*9X-tC_>>Erbbt|I2*vlgJyk~l#9%O&*%1|3c`$QPL_@$*~ sd$dVM03QopX5%|5zl0Uj=gPM0<465yFkxzQ&>=hcC6e ubGPn{fdZ8g@`_! -XWDPGs!`dxFuSB*wefr0%FmzVFYTzhL zmO3p%|Au^TXwP}bIPy^*={oCm1q>f$np4JNc+l#eNh=z3z&i9+ kD0Ah z4Xv C)A5V7xur-~@kC5!`;LLSr6?D3-S&XMV}2 zjNa!%tN!$x(2z4Cp!Xx^xBRy49SvqxQ4GUrxd`Yg^oaIwhnH&IXm)aI;8?WUvJ4T> z30c6oo*QiXnWnq}V-wMPn16mmNC&7FShw*8-_mAgHF`@xo0zs$GqApx&l%jP10A?> zJhkmT#JvwThR2raFtarQxz;Xb=xs5i-;wuP+@2ZHgLVf5%UU#Gi=h5-Iw_x@)eEMp z#U0J4G=nREA)yT6GE7Me^r4?E1`n9xi8?fZwiha+sZYN;QZKZ9Yk!#0tj35oB6qNj zlSsJHjCE*6$Bm+(Q9FPYhK}1rVP1TNTa9Px22@15B_E1O& O`^7=$ zpl!3Ppd#x!<{Lo&*w1a(yHAUQh6T3cT)=?ynE5Jf<+^7V#fdK_+x2Ai=$t@HdVry? zWW#pM;B4;^RdLlKp)+*aMVuRb-G!SVZBd+vS0WO6Gpfiv3xCUt_X38_Q 7|<3fr$Nq*j{Tlu=W-g6dQY4H!E&z4-x}}5(6h@=4&0IbBN`vn^a+J7 vmNmjRA|x16mi*F z#=Fs{0)y9qh(W_P-A_Kw<%3p #`yx1h^B@_*pVzf3SwidE)_hIU<~*Hh|! zlVh}5;tx41SLw_fRA)=Gt4Y(2i-&$lW JnoGz5USn>H5(8!*B6=d;JCZc0lJ` zvv8w16 =*6H%WPp*4(ma$)SRB@hy-+!PEI|n{VK(ti3GH= zK^t1{Tg^CkB%o_FnTK?LC}JiO&`&g>^*(t$&UHwDLydq636;&+X0#Ko4p_zc9N` zW;gT=e%(}=LNPeo8-8fz0r#4PG=m0b_@P(uE_sx?2-kC$ahF}t>p4l2V~pyM$}0zQ z$8EdSpYRobHINJxhch+RNSZuGe#&QW*r7Yn`QaEtP1>mDa6>D1G S|*~#&jqi2&?zCi_mjpy@;;Xp&N!`veoeMx)J!|`XDb27xU!!IN9bdwTzqP3 zXOo@K!QY^zl~0&+$x}TcU+w)x!l>Jemr6JPSxQxmy}Lx}mw#b@=whh_o6&%uhj5oF zb}O-Mz7;)Y$;MmmU(qM+XI7M)nva>8(?8Zf{z)yn?O}D9_p+6xC09zcA|)jyB_$;# oB_$;#B_$;#B_$;#C1pkb0xb!T*v;4Um;e9(07*qoM6N<$f)F~GbpQYW diff --git a/docs/rest-api/source/images/favicon-192x192.png b/docs/rest-api/source/images/favicon-192x192.png index 41fdb478540601097e10848dcbf636a7a057b15f..834bba3d3b00e54e18d27326b5d7c2e0022e6fe9 100644 GIT binary patch delta 1980 zcmV;t2SfPx6v_{fB!4_mOjJcja7=$xv;Y79eNwW2RI`6nvwu{xe^j%7RI`6nv+LyZ z_VfCNUAMEE(vD}m-qY;DuG^V*!qC9uq=m_;*wCf`000VfQchC<|NsAaks0j8JuTH# zmjD0 AvoAL`=)3){HT)5k{_g~rhCq#X5b^r9$y}vV@ID~T!-T#k3Ceo!XvuQ06gt{p+ zYEAvw|5KS+t26I^S)ShVr_8Xu#Ng882+;g0`P`OSw)UK_0N!MfX9B$>QMLiNhaleo z+(S^88MiuL3xE1GqMS46Cx^1mpj!^Lc>}ch12lO9G)0+tI{*%V1K aOqmPLGqy5n$2Pqjh730ApW0TW^NM077-i^JjVO z|Fzj6A%J?QtK5uX(A7s9(*da2+|^~~tpYH9_P^=?C4WNzYB0StH{}M@tpKVq4S<^7 zgS!pEIsjJ{q)DMk%WsUhQ30sBAq9XkA-Nf^0PKqjK)-!RZbn;BIsg-to6<3$sy0g@ z9du`e&5{5~L` 1(n$fiEVhMtvpw{ZVLW{@5dMS#A(!2sSWaps;f9t7SZq?``-(T|x#M=FON#ZZ+6 zmD6y5KP}keqZm5UAQ2#K2pMEn21eahrVbj@Eq`p0E{8;zs6q@nm4SU40hmK=$RGot zA}eV-But?|8&Y$J+*sN~$E1t9fi`5Ij*;=jM|ub#$cE_5vzP=lTx&r9V^FPYv7XuB zLKI)v3b7%y#*dT$1`uFF^fY44O<%j8k^+R-5EFnEcdP`!)oHN}DQ1?t&Tg_0GAb>A ze1B5L%u&`imj+!@1Bk03HUJ+grq?_q^F0h;5zd(C%qBiYOGO|7Py@nAnVQ%De0+>1 zl`Ps|074sb1mLrGHf|Jv_sB$RLv&)3;wQBtT6x6)Xd9YjE1g95XjJyX;Ubo;4LJkw zmLTyOMk7ED_?!(n1MoJK(PA+wJ-~#kn17nba5tDvwlO_`Xo=brfVb3()-b$NZ_!-k zHe1L$JanO}>S6%An6SS7KO;xOiZl!ZENIQ;55RLIpsy<1Jb@2;pv8ng0M8_+O-QzV zg#wV-YVmmt*+trfmMs97Vq(C{XtD^g(I$i+cruWR7o`8cK11S)3JC$gh)fBv0DnBP zgdQGcCWHe3Dpt-inh+1R^p!{h6QE)=0Cyv$T~xS-j-Oi8-f+VKc;wy~Z$dbLU&YE( zMz8BY6K_HS0Kbai0Q?Ufy?7@*$%6y vdIG@YkWMiSh%_*8C*-*@`giVY;Ua0=5`Tc3!g6Kw zucN3123WQP;8rqy0pNDCQw#$#fQ1&SF95zBgRmX$kQ5@f$}~5Rc`dc)9s`gcs1*Uw z8$uS?Peo_TdRu_fR8^3l!0Q$AL**g>C73iK8`}E=ps7q4Iu< 7Se>3A0aR}YY%(7JBGJ^+qoHWQhH`vCA2 zDY!qxhj1qVwgcdloOhx M{!C$Y&{%-;1K5jy*xO$}92Y|^T6Q6}UOey)nNECiX z275(-+9>EmD0a$;#ybK%06f29t&swzN8VInW01XEN zygR84dt{Y~ucpJFc4^}Kf4xW_by6h?;ALK8htTIesn_!oeb95Sf&tWYC+Nl@{CQPs zwvQm_CFly_4@_0VsJl*^xF@!$`_p^yXVfMG_<6vu;OV~3%YRJ4QH* `W5kr%a> zf@tSclS}%X3pKi3azJQKmx6DXy8UW~rmbDw|4F1P!*@b7?X~x- hRdW)BXJYnOu#aXO+vW#i41IpJtS!Y?z&5 zk^K4l`uO_%`~36l@~L*8{{8;1f2EgMi{{qjrg58_V2}3k^?&&F_?caf_w)9>m9^T$ z*6G{ksCAyenz_iR!o8NZonw;b*W<;azMNu_(Y4FMp1aw@)#%#gqHCAIoVuG}km1kX z-pkv}uExiv!K-?r|Nj5bw9Cn%B#erZkqAv@9pF3&$G(<_xZh+wVGd#v4g0$ zjIR6m`sCE%s(*K&_3rce_4uZ8oY%n9yOXoLl(fX5z4Pqxt$d^S^Y^H9p7!wdmRX9j zgsGfikhY7i;LqLN$=dJb?Z%|P*}~M*xz4S9q|UO*`1JRubDhPbzpsF%rEi=kDaw z;L*0s!k@g>z0vXM@1A6n*uvD*y3e6%m-_bkpJ$Z)`+xk>xXrhXue67&&aueh(B8U` zvi|-3=Gf%! 9vWh z%BsY^n77Ta$l}r8@8<3D>+##h*V@F@#-+gN+~?Q7($u@pxR0>2hN{@X)7;3|)x6N) z&fL+r%zw+P#j%8`#iYNmf~fQD^7rxfxsS2Pt6b{<00<^YL_t(|+U;ETTU19BeY-3S z%kI(^se(#fil7t$QBdrmDPoNc3rYlI!HS6`DoQk>#%?P4L$gxCmV4*jd2eRlN4_~f z!rU|WyfSmk>@y4v4Gj$q4Gj$q4Gj$q4Gj$q4Sx*{4Gj$q4GGAry7Uv9X}CGKJl*eo z5M=Z4UHGGL`T4)Y#g>pVx@o!je{L6x!qf5P<$qQ#i@^i84Bm=RYU++e2 ImRaDue*#EY8|CuY>7NvjSz!Fz`%^s!xyKeyk*iKuVuT!GPO7c_+@T5Qj((F)f zjDKZ&cvzu;X2^eb)CS{?U>w~K>ii#E0SdTggK}Lw-HbW`6QKX>j*VRfdL~b)@;?*y zpS4)0{0-u(S0!LGBH)R2$}))Wf+GKmk0AoCTjx9g@g*HpBVZcw-&<^*6I_;aGSmoo zfC$)YgR>lDOjhE5#w_grSe*^hG>~#yiGP3}kQ?xw9ny;+ x0Uh>&E0D8Gg@EI*|2_6zxXWw6bxn|Eq-xUZ^|mzflEcD>uz*iCuZtHV?d}z! z$iBE35W4y~Pmm85aLA?rcpdeKB%|L%7*3$W>S6!y7TXk%4$=+?_&-In{1Fx~tbZoH z>Z7kMu4v2sSK)Ev2VeoV^S>dG_7&B9bxW-2J9ywB*#9lI7q?`PH%K+#7H7KuGC1%g zEMS9e0Znj-OSHZ^6>s_+9N7h@ViIzvB#`+7&A5Y_ULm>IJqa7+6OcKTcKdPwQy=Y7 zo5NNK0#ZTdjU?NxVjoC--%qLO!I$+d zb**E+NQn~LLGCPb!@MA)-n~kAq@3~xoT*k=Efa d$jgFOned9%wiV}DZI^8bnp zI3GKB7#x;gASqb|ld_;mr=~Oo7ZA6+!^N9iza#;>tt4gV83(bpKI@yM^`wly425|9 zv)`i!*D V&fG0&uy>wj!O`X@N3Jxi8=%R$kcCNRYp zz ;ZQgrceP1T=yZ;5DjIGymC^ z?dCz#b$J4|i7nF^t4sw%wMaVPArenIZHGHoSmWe`x&N#&V*CquyOvq`_K7r_+IA5H zG(;F|Q5lNP1CET}!-J0!{EsTogRvGnDO12(ft27B#ebG^&Mfov$bS^D6P#i lM;5DS8iyAKW=Tik7Su|+? z|NU96fC*~lvWx0JTf1m-+FH>-3>~J6`8@WL{b$b>A9DELx8(|`pw(^a?$HG-hSZr% zWS6vDnSfLD!=$JWL4WH)_7A5#>*WeSQi2ezQ3s=(SH3TjeI)59 %@oHJUh zwj#kVA4gA omn_t>?b?gszd%n^_(TL9eVV4V>|da)bO zXTbdz_VpA&HJtzV%NAhdg7Z{HjA%go@9xEpQ-<(_SpFgTG=H;`3`04HfJ=taiMMK! znVmkg0M3H*yfka%ghRgYhP4S!x>kgDtA7#^%Ha|9Qk@yiV=jLE??&`IWx9CX(i#G@ z+`P#@i7laX@A*0OC`ZBs7-QVS7)RQ9H^4Ap`MyEpGx(1;3It4WYX|EP|F0}G^xk2| znYz>`#N4+ye}Dd;w;BHi7zJ>m(~XoQR0vqf*~;9Hu{~h1qv}RFHUs;>v#wbR1Z0-r zdwwN$113!Rc&x;ipj8wNTE12ns`p`gz|t}IUXegmjR5!>J`HDvRi=4w_!*iM5O7P0 zfDo4Z1xgS 2MP6|MdNPiNSpm_`i9Tb37`e{qh0?M2eFm09x z5Wb<1<#JGfafd&7DTSlnNdcd@b5D}Kp-@=vpn%)lxrry;+H#x|kg $uzK`$Y@p?R7}KIT(s$ *BF_US%=_0yz yq{3`ds(s@A$%4|C;T+Piy#T_c8>Rf%M=hkAOc%AYRS?LY8}Po+lj?`nk|iZ2ZmL z&$N-Nn%ISK30&%12qhkkBk)T3+iOuZIW# oS`m36(4Ka28ij$VxwL} zybXXcfHiUIGO?S#9?UEneUj8?M^PPWmdCXsZQ+?8V|(=|s`<$kxdy=_g&tPX#kr&- zYiEyzbB=2ELL<*3Z)ZD=BZL-9X{s8$lm>FC1!B+o|D0>l6eG(+DFp 6Y*`z5h~T z4=#0&`%i2AX^)0itZRC&fv&ZyzdkX=soyF{6L*eltcxZ49)cp7>OW~rp=95guVGWO z%u_~wyxJ2gg?M^Uav~ou0=E&<-Rq~Tgs7zhli`=)nH}NR@9uRhQfY$1@|g<*_5k9b z -s29<^uKh+3)?+D4yzX|eOeM9tN~IlX$ip}npl<8%XJYTdl8j37t}5h*)e zsJVJRn4CsFq1b2loWBpH*y%TFSc9tB_xEb@e>x6swVhs5k*H6#s?!hBz>bj*KZ9Y4 z-Z!!wr79Vl(C_C1!ZgI}HAm|#ZDJ8Pi;mRGj4XwSv-VZz^Kc%5*A-c2Gz|l<`=1|M zQgFQ^P0%P=e^? <0)wu|jOt^A<0T-Z)- zILZukYwp>sonDtBgx- To#nm69QEI4zVfX&T4LBb1gG?tn71Ac z=AH;stO9hqnIR5P&ScvOx}{jWCVl{cvOiIo=To~YfFo683Zg?oauA+giu`o+4{Oh& z$Lhb%L_$NO3wy>Bp4xFD_^ls=O*67kimG&U?aJH(wTvQ0)|1y#dTLd~R|R51mq4CB zB9aAS*FbS*c?YY58l0)B6~%T59+k4ZIcUpQ5>BtkLt| Gj^zEn|z(t#T_QJx`=nAb+3vK@* zS)62v*JXs ?d{p`X^6&~fNhv;LRY0Z0}&+sO&NXXnaRndad@leX#q z{tw_faLh&Q&8h$n0+@__v6Oy5LR;@b2Y-<%5=>-stT0buzBRd_16* c-q)OWH2__RwBar!-(O^s84Y9xVcJxR?l1nL`hpe8b&K z8F7lS|D>eSpMb?fSK>VAs6A^Uu*f-n+gt(r=EFND3B`~JZ_(&4E^HB_$9Ox(FmV$9 zE)sm(wN^Vh%$lM>NuEb5gzAh#M-i#W@R%~hioWtf3PaOdDzX_YL(FLprKU03yAlS_ zbEhC!;ZU4!{=**uszYX^H*qsv4MF9Ra?Pt_Eh&%4K@BpSUh)qkx?Fy(Y&`+)N^Yl^ zEF$}&S_2dE=^6Kv(Y2=kvQE=cCp>N8iouRM*-l-JdUTLt3x{*Mh)MI5lYh1B_3-!~ zxxnl5mDY0&DDm<=gbT0yF r3FwWM9vKxd0#AQU5lb!2IIJnue@^K8W1=a6Q*z&$rKYnUfNQo_aQw<-YjXTs z7#H7l8LM!WPF6|&LP3nm?x!Rd -)4i|SCK3(wBmprikvtSHmYp`P`SRUK*wMAe>=PZiq>PY z-pJPY>`IutyH-P#4igy79nJ(d6O*Fq3y}@7V#%dzlTx&$bISolp2A4 lFfEihB{ay1uVPw@ z;HrE|y+|Ur 0g4cut+f$QWCX%&0egWR2SZLIQ@?9DP< z=!{bnuXG4T3 i{8q`K@(Duj&3vWT_OiQJe$J*MCJq1iN$_ZmnMz%u#;%k2=kJ# zZa9PW7+7cXW&84sZe9Sy&T1Kpm2QQ8gHgVh{d`RL&hpzkwgAzv!Pjl2{DRv3`n{Ug zv8|;2`2{T}F=_&J_@ijvvhW6@9ZZ!IrXIcUKmMf%0RuCmLP_2|D9|{UaVL7Ccq8gj za`(~=?B2%yevadgDN*6ZLF-D)D#K2sBJT)Cq!IoH-TdXAVLSZecOiH|>Pf1BG;+cK zqkt(s^*#juqDc3iBek}Tn6HJo`J$|lI)=dI8uo>NGencJh1Qo5a52Sg^oWlG`NKbs zwh|@u7Ofzkb613uKX}OTgk>fHYOTnz4>wwvZ+8Lb7gKG%Rs9o~rKyFOnmnY^9r*U^ zrMuCs_PJYeK1>Z4nlJO?^&zT!J?_oSN6AX3tZp9evx2hM i?2o(ib#{-<2e;d+ zuJX*Q4Mq3J33bbO{<)IPD@2v|ok=ZL&L5LZ3`RIBdB;(-i+(?g2)Pk)?Jwd&j14UG JU+TIh{0H^6g2Mm+ literal 3789 zcma)<`#%$o_s1vIP?X%6OP6<%H@!oQBoTAVtw=6W?)NQn$=!0vWoWL=WlZjukhwFL znY)QxN1MADBe%@$+xI{C{BR!6*X#UzUXRD?d@|IBu(JuW0RRAYZLO!q000xm{|^iE z-;IGMYuaC&cYN~f2>?))aOwEXg} 9xApEIoDzl9af_0JT%%RD_f`B6W(v9SK-H6q 9OJh=Xu`ha!w6eWQ-&)yhLVictB#t%=W!ocBHVL(#%E|&T zo#^I@;I9We`-@|g%|%*E8g}SgPft-tnt2@K4+G >m6Yn8AIi&wP{EkXTh8Ux|$=uZ=0M zf*0jFr_w0YrJwVw(@R~st-q;TV@< zeaDrdSU*2!yGcAvZR+p5Bg2nFp$ z?{j_)l6nd|Xymm?+-QbnJRZ|Fg6*fytx|^PraFI4w2h9p{vZ#LI Zpw2@KRG(ddYeQZnx)RJl=z|hO1mc8$L2?V4O9|Je9_;d%XT+@?``c? zh8EOBln@GVIgTlPC0%77@<@cqY5e%|#6tN)I2Zu9?5_Rv$qT>fjjTg!9tFOrg_Ho< zG~x5-pH3xfsH&R!OBHcnFz2bAH+`DCKAu;!#U_of+cd9>sT`l39%opa;@A#kkVw1R z$c!s1N3uxdQKGi~kYexEaJ5`Rac;JV{{i7CydI@`+Ec-|Knef$M3n69)})$7@PgxR z&NwCexS99J{l4oICpa!`DH!l>Raqk@hL1Ywr~ZsP5N7w}2fyCe0vV4Byf+Z*V`?a0 z$yy=nU;ZDwb()YaE$Rza)jS-H+u=$zA(k1@1A>iSVuM0*C8EZz8!znlsEEdNg&hy< z9VpF~i00JL)EytLGdXPnI@x)?p|G@?zV$xKn;6%hr+%%$qSU+z|M@r}6jvIh1- z;+w#!ll?}z2k3xZ9&zlw;?DcZ+*Rv`?#-a&XK=wG&wW&1`Jnq~ET;G(j?-HN!q!Pj zO)-b%1LRhX2{%QZA+O*@>+_$zc{{2`m!@3W^g$D1g1ZI;_g05#i}EB*v}oS_skQo% zSUvdqgZarT90<;p_zFisTIF~Yv$=h7RhK_zK`&OFb=qmyP7VB n!_yYL)AGMp&lbS}BXX08nuw)H3WMB;xZxM``?AR}vGYzMy*( zvQI0QSRSbtFgF!h$GxGAz!zlXz{cDH*)}LCXou!@%7dI2V&u^ipt-#e55>te9^<|~ zlbcbCc9>VDJMgl=M^La5Q#rAG8wHX-Y|~P>!D@JKOn=o%vC!=Yo6Ls8q-%@4u3qw~ z+e5 `) zkHf{5PguTE!;JY6-+?Ums-fBXJ0l)Mz3sA;3N`Gai9+Z?m|DWh9jx==Vu#FBG+leu z%K1l>ZCYn#- (o Vr*98n61>WhK!!XZZv4@*FERvbc%xr-uXBdm^Hj xg&Xv83V8Dp _ U|>7!UFpxVFgCHWUz==*wn?GiE6p3X# jQLu`*fZH?r>1hTrlxqnSvUYcAM+BG3tp;>)& z!HIYsX=N#~rT0k}tdj*DH2`Wr4Uv+(*)=&%PVZO=m9*fvckXd7Sw{ZrHyZ;J+3zGb z#ZD!h?SPbKq%`3o`_ddX5vavi^!^JlJ1ib+xX*s{Nxgysb`9WI_{4Zx`VaQg0~~W! z1;x) `gJ-dy&;E5-G^A^`sNLwTE0jNKGzr@{Ds0$Wn zhwl;yjbJpm%4u!i^cl98OnAv>#G1u@smZ$`ikVHF>ESvh`Z7XtBwqEOclOfrfT8SJ z$EeZlej=JnjngejJlUNm`dPq;j!ZtZY?ZUuliy>0()Es(qRtX`(A^~qF}|eD8wg+8 zqhQhXbO&3*YmgV(c63eP=p!s
1I3APsV?F7Dj6i$qBP90YI%kH2bGPH>$z% za({2qiw8(n%&ZQCj H*%3tGnY)%JTA?*!RFW7!Pe7@#bEFAQ($<>@91yZp8 zTw<8a |1wT*xvRD`?H{L;^t4qhfoW z87FJ O#q46&iL}{*$by64jYPd&k1Q9=9%x }|GB;T9?|-%KR4kTnV{k6a?lx2xAtMljb&!{^952IRGk^|1pUh>_%~Pm zI3c~*X^N>_>UmNe>zhJILv?7%4Jx%-wpR>cE9+`>W!Lxl(lu%YXHt$X;g4{YJeZ0+ zZV1I}Nk9xI;b3%Ap}~OSNWalgQb*Hp|G &=zJnQ>-kdtxhLH<9 z?(!L*b2?P*(?QSF)CIRsycq6{wTwXM-;NAj@0W?@1tCF~R=f*XEA}LEylkgE4X+lB zBEjf~8Li;AAsukr&B4k};$3e6-1cKBuoz$N@y^ioOLLQzLfJFY-!Jdc_sJb $oN&9FTmw+i4QFAF210OT7>6RqwjBrL4(yf=K{*r-s2W}v9oe~Ge^w zEZyKbvirww@QmWu|GVaX}A4P`@@?wXOeV=B{*%Yh=J#$}1Z$(nSPj8Vca7^8BLc z5~PO&xVn-5Xb5qv+R!10jLuz-mV7;b5TiA41f{;wcO&s2OB4nr*`62g%!^)cy$1tf zwK1Jkjp1PN+5GN%i9C~C+p8re{e1SQ0mb4ggogo5<> f4VRNSAoRr4s6#yOT=C-gCcymS*bOophp>u6ZFOT9dY~sKduOdrklP zMU726713P*#5E(3S`Mp3AVn60pM5LG8#~M6U)*gCRwLk{i?I(xeF@g!9*O%9?OpkU zGQki^G@pg($C-P3d +ElUMoJ#A`(HZT8eRa!x^UwdAHS r)J@n+#W7eR(K{;RfosfdwTCJl?H0{;3ufb+ z+M*P*{+|<3KAV*~lvv-7&7rnjFH_7g!dwXruhWhR+595bUfz4jkGTAm1JZhB49Mi@ zbkj-Yc7B3MAd1WiGSI}G%_z>SPq_agi-oA4G;gRTGbDeJoi1{KO~J5P590=F=*OwT z%Z-;Z|NFs+rEr003%JPV*8fPJlOGm->~|ix!(|qPnAMxbi-;c9inO=C8+!@%wWO=% zT*=&(u&Ul%6aWr-FaKeNOv>c@505Bqzx-EZFuVN;*24Qfp5kuO^xlZr{eezieejs! zGk=BP=&mbb%S0!OZ3s(m+}HhgF3lz*$|;e^%q93p2Zx$4J5`v|1#9`x#O1T^DskpG z`WzQ3r}^pnkDy|DlW&&%`#?BzC1?D>wRw9Hb{)D=dCVb4PvU`7;QW1|KU!`^t}fua zr}tm0H5aL_dDmJmJ_#swd0sAMa@I)u Jv@Tuj}{ErUj*|JRzY d=5KNasI<7s_>Az>`}>aqv^DgfR;by8{~urNx@Q0Y diff --git a/docs/rest-api/source/images/favicon-32x32.png b/docs/rest-api/source/images/favicon-32x32.png index 9c264278f0ee44cbb846fc9f5e6bfa235c093470..177ff70357bd792f0e4aa265f8d56e09d9c3ad59 100644 GIT binary patch delta 430 zcmV;f0a5<)1;_)CB!5LvOjJcja7=zvv;6!1gj%+LRI~s8|9w%ie^j%7RI`6nv*z9K zww=_FYre|3;PLGDsEW(@_WRMn B4gj#`0*J|*9RNdc{+Oojje4RAps4ZPEu19V zG~*dCY3#@HZ^ntK2Zxx4y@fDhETysUBhlC+dOC=dika3$u;g)LTmdE{CL!PqW@8hg z8{hz-z&pl5$A1JY*SH0L0C?FaVDT(JilkM-24EX(OA;J`HR^Z*z)~abdEtC@U1uRG z!^p`j1~S%jtHTnNY3r}W#Ce*cQtM(d6dl=I0CILhKs1$Ay4(klBV6iCDo$<}Mwol? zMxqr1TmUk0OiN?0?=4CX*A?$zm~Fl?SeU{H8`y{L1}CrpP?u!k=WD+M9-rRh<9&Mm YAN8dcjR# delta 730 zcmV<00ww*(1M&rsB!2{FK}|sb0I`n?{9y$E0004VQb$4nuFf3k00042P)t-s|NsB4 zf2GExzy19ErEr^`Ws|#;w3b+k%dEwgS&Fubt-O`B@#*jR_4v@V%DR!V(Y4F+>+zjq zlK%bvnqH2gYnSWa=+wH;`uO_1l(d*zjF(!B_VD$@p}nVcoqzZ9_Wu6 BpzR&augy zVv*(6;`;acx_^?g-pbn6zS8yX^YrfX{Q3L)`TNkd%h0sT;LY6G!qupCp7H4K;?dvf z+~?}v=*FeM`uF+Br@_FRxvYAk$*IHs{r$I$ukq>d`S$tBtHi^fyr*-WqimSv)#2pT z;kl5p$*RNHz|*F2o0nRP_3-qUS&Qe{ kDP5 zsyBfIaG?Wp0GuXp0_ v*aZrQ4Hp1DK1FT`ge|}+ zpQn|nE`OkV6M5nrQ45gAxKEjW8hGVB2_&p@m|`5_I@7IyBDtrIYJ&;_6I{Af9pCay zJ=1_iU_`_-ZUA_Ap$ut2go#_Wsn@*kUMU_8@T%V88^Gq?CGhnK1oh2wtKL>ATXI0X z0cLHxIS8z|fMlmEr~2r%0WGO`USwe1aE&&g+*3%GRA*y`4cUMSBb`(>rp67Q{X}tp zQ5nq+J{M?USey1W^btGI+=kYSFKGt?IfvE}?#TMm;yV9-__H$C53ceqLWZ#1hX4Qo M07*qoM6N<$f?61>Qvd(} diff --git a/docs/rest-api/source/images/logo.png b/docs/rest-api/source/images/logo.png index 2415244b6976504cef914070fc017bcf7e29e79f..4e1967f3772f3e03224e30ccba618cb0ded29518 100644 GIT binary patch literal 4263 zcmZXXcQhMN+rWcbv4z^TTBEh2wL)vt+MA+gLRD)OiM>^0t5ssFEp3WgF^U+`*otbk zp{>0~5kBAdp7Z|m-E+_H{+|2X^PK0Od+xcfOpJ6HfZRX;0KlNHr)35JkYTRejfUci zDiKoIS4*jhfrYlNk3opd`}@jzkpCMhDk^HK4_=rRUPV`aMK#F%|D+Js=>N!nQ~wRG z1YzA6YKys&kpDYjCPc^J*vbO03aWymGiz{S_}SFM}-VPXC=pIJ{5uYd_dIS^ByW z4{OSg>|Ou8Uzs{IJ+#tUH#^+%15-5qwa>BOs@q!uW=7_KT8jA-b7tU${5oI2V4K*d z1Z}sj>{7y>)_aNSl{7fUb;h*mi#a sQ38?Y%OK{>~YEgTaSa`%+78VmEVI)=Y`9(PNJQqV$gx*Iz z`Eir*0r$^e{Y(wyZ6A3)P?tBDhg&hxL0sCGk_u$GUPmlgDXSG`E_Z-*C5@gAGA2CQ zt2Q`OUMIc(%chs%kb;x%qHVksSNWJ;2xZ?Z%Em}bdQLz|bmNDq+c-wd!BL)8NTDnb zZRdal)`4k~)dA@lsHe}xsvujV6k>!R!F04~FxS#WQr}r( ecwRi&b@ 9-O^;&hcNXH9?0xdti#pNLR&6{%4-%f4GqG+n;t2}IJFN$Mkti8dyw4?;(BGa0Q< z^5~(>L$QD6?oAHpqm(%BZ6>WYuh!OZSJiBXEFtohxIZsoxGzNR5wiY#PrK)HKflkV zS*64xr~~(mR9)@c>TfRmu#$H$_jeQ>7cF;;?GCos6~mYl^{`aH^QVj0Jw{(*#-_?o zp$`(;rV?|D7PDSCqfZ*GwubpmFtXIL`z?zJSZSrdH7^c=LKYe{bq`g4DNSWBjy$&f z<3ts_?*u Z?fmyttw{9;S>BKl{%_yNe<<$R5*B_gn*3YVTJOI0!}M`LqixDSkHi=1SfWw| zrI2FhXhj;_tsG4p2{9s{&y1Nvfkx#TEg77e`Dw_(qx36O(!Y{c4x{(PX_iTPgPOVe zP`4%H73NV>1h&*!k~~Z0ruVc%B{jHC+(xv Fn#d2!0m^?)ym2`H+)egY$^Ft?&y>49^X*EG1f(Fo zqe%nW{vf{_v-T@Wst*(XQ%|fjMV8C9F0tIY`I;=eDg@~FwZ$^fjh@N9taTi{Ttz!a z-cn!Z4Fp%HFvYKb%*_)3+l@bcpX5e4v7zlS !6geoxj(p`^uzXJLQjX+VkRq9#$VOXg--)>r%@+&f0vz7X@kqhG+1UI^*lm z5Zt@V=6;9X){(^Q8r!4k9zReMHODP(M>wgN1hV_&^Q$aM_2z;2uTSCp1f*r;7A$V) zMRf)<;gnbRL&?*p4x!jIwrJ6k8>d=Li@PzCnlUkjMRJk09z?47Cyzt+vH?)b!0PR{ zrwbozfT6CWjIHVs!~W(YGcz7`udz($($AW%I^m)Rrqa%mhc{4MUQ9oj5Xwgng=+_7 z*I81ojoKvwuy+&B!?*ytVcJ#hK0|#w{97QIJ2`}fK-`;pL#G+b*c(Ezi)Nf@WNjRE z?(Qq#JjRvB=6!Tp8b8uZQ$iKag5zasyEsY@dO<`k=9Evg&?$H(aNm)NOup&{SsJK0 zGBP<;Jhx{>UMTf5COp|`2b1ka?Oc553{7;7PpV)q-Ig@>D@x5 t4oR~LJ|D36!V;+4EA;qzRg$MN(t zBgq0)5?fz)m&uvAnj8|6o_&4%BH5Mdx-?Nj%qC2)7)Y-;s(C=9RAj)?lGA;Mu!B`M z9wyw4gT*$LTc}~AqVt#u=rQSbIlPkD*71%(Z?1SWgyOqS&QiCsrd+p%rMuFNc?swr zx>=8Z0B9mC-eCXP&~uZYVLU}1#dRK=Rh?(fAUL!PSCzq1>E%NQ<0oji-zI#i3+ea$ z`3RCH-ii(6{XKwnj4ss5b0pB`Ob9$Yz?QhYUZjz>@DO!xr$0d(E_1P-Heqq~`73{k zhX$IhA1}nx5d)vB%V%R(t}R)1o?9&bn!?cSihO`YKx);y{jecr-`PDfZbmqpJP#i# z9W`sJVA`X9w}@CUFf$!F{B1a%+)(^{Igxn_YW>enT_OV6BeJ=|17SR+h(>Pss^f3I zC= ej=r$X?v^v{7-*K3#nJ3H>}!R H2%5C%ZMFFk>Jlai3=+d9B z%|F{w-uR`)tQ=LKk;smLMp#jF=WS43`^{$KWE3|Go`!*`#rh%Dg9L>NWyzV}E6M*L z!87auIhpTdz(Q{8W@j*q;BO3Ui$@fnUdCPP4nX}>MSZ9^p5IAt^?asU0%u;10`Ah! zWP|m=2>xVy5|S9xElDWAM0`$Z{*LO#n|w!ur2i_&ouk932lYCdLGXA7rv5O^PRQ2n z HW}K-l2*|F>^M&Uy({=;Ib*WvdRy|4TbE| z@!3`3PrfpVlJdSN&!{H;Jl*ku;7XgvkVbJULh+d5#90Bvy!>sZ%ETRGWN?eHu()A$ zFm)h8N@G9SBP~Z)%oLUPmL1HqR2Eif_5jik-ZtdQJ6C@*;ryZsbv>mQa5}A#yPKR^ z7xDLi#oSN>EYAx{Ya(%es|=qqS9^zx0~;$;9WGk(=~dj}*}l4R^0*pN
mD=GM1q*%<&R0VjFMK-n2tDJFx%cDvj_fgL@=Q4g=NY8T z;e(ZWMJcmT)ug*!E4BUn8r&)sX;*v88Mv^n`eNxp0qh8YgpP(XznUv;6YQD`atZ~@ zq6(!U`TZT9H(a#XXWHcNENxTC{+4NVY|su=+8MCIb<`pvb#y^VI|}qyuTLe nfJrkh|(gN+uG+kOiKH} z<_!RW_c1Zn=%vAUV0AvKa;}o1pbT4*sgCcK;rIiS&szP8;n?RTD2@Qb1$y}9i?dws zbE6-<<45Xtyp81&$nRezumz30I-R9kW`56J|9xhC6PVT~G!U*0kB`GPm9}j82#;q! zl*M0zQ{59#ikAY9#mKf-2zjs(?b!|wI~8PhL}*9X6`<{{oL`*Xp0cfQg|iM@MST}y z5{k(>D;fb;7PAoV$&UjPg4g0M-iU6@okrx}QOrz&t}(c+r?^-p|3Q?v(^H%wfBcmk z^B6Yc=j48h=rUg6G9MdEhH-Ow5ce^{|Gd^vMbVW*JC`ky?Gf>!X35X<;$$)*I7=j% z>IJfqDK^|qE}ERPe-zpCXAkL!SUZl%3W&`u+#_ wmF@7~K zKq*d@Rghm_KDmt86qtUXL7pBnkKZ^obRC0Z-sWul_MV{4^WgJ+312}-8q;71-?*ro zdLLzn?ZMc`yKYvsa!ibAZ#a`AoZcWX{Xt^*LB73PWMfkS|2$%G{$DFrdmALjB8 ^t7Tz=8vhJIOm4iR^6jpS=5RnCR>H`(H#$pMFGFEj zPitMh=55%1kr 6Ny*VIpP4R4pGf~MpvPh@W`avWi-OTOy) zsKh&RscVYiqg_uGUNJx49#UGN5&h?U{Rx$}?~PL|0BSAVY;=n74U35S^6T)OXRY^? z9bfWMG55OR-Ot2%blyvAp7TR$wQ<~}4Sw`|kK=+Rm2y#G$8~|U!GsE&PO?>1gi844 z{7rr*ns6|rHUSygH!G<#EUM$oTJaprlJ# w|KMH-23k zMoWy?yb19-GIIwJmTm%fIE}KO>z^mi^H10jtQ3d3Uj5pScEwwmx}f?el_R4P83kZd z!SnitI@Y gJi5xqHra#7OelTqlirfkck_hx9n~JTV)|Kf369;Jz4!g#hRqR;7 zQhF!vwR7(&uJ~pZ=tkj#1m(YPd1&v@EyQ)u4Fmx{6UEZVC|Dkk<3&Uk3te>7@j;O8 zG7g4T*d;g-8K$~v)J!qhm-7*J%yY1RFiLSS-bUF}?+2UW^>3tLY&p&Ua~t}^wLY;W z^vB|hl1X3b^&f+UEcSd%+&vNL?T4YH;J{VX@~}auGCIUbHP4%!wTVx@{Nw^X3IWYm zKZKoLP<|;{UO>$b((q~xxiq;OM!7QB&NRiWK6os#dIYzYBbU&sbl=QG0_f7y{uf@h ycZO)#Ooy85_zV>PcQ3eGV_B0yhG+PKjQit4dD}Db=BvLvKwsNPt3ksl`hNhAS}r00 literal 9108 zcmd6tWmg 4)IzkLVB;=;eC@tlA#~&FlHY}eTRKQX| zaU$i$!xpk5A3--udW{_WRrY(vK|0G#3?3uR&l_nuJT!i^MN((~mAwXeiyB)k%8*r& z_{V3Pqr&3i@}s7yDe e)t5fBg>$!>MhG3F)Lj{c|j2m>W)x&;i%N;sT5 zRJ$x+K&-eRAITg>Hf(Ts8@Pq+y2lQK&Vl44@f2y0s1*0NTt8(@FsA?J^Di!!vy=L4 zqT4kcufXP9v`O$zl<>=*R6dm*tGV_Vx~Z*tYU3+8jeT~cC`n2bm*R<$55^#A@i)O? zxeyF1*I9bss|D9cRkh=n37>%p3`med?c|C-31&(CI$tUs1Vw|Eyoubjf0w@KVU?m) zH#i?{HJ7hv5*20?EF-fHkyI5FYKR%ueourbmp_cxKV~-qy6*+)BHOwDSfoFnas-S~ zIt_P_wc5;{b@XKl Pm@GT-v^REhbJ$ z>#cJ>`)nnAlbOSarg4F)A7qN$U0Cv!n2I+oUVgkqV)&!ZDW)zRzo&shh$z<{CYjD@ zY|k_kawU-;xFpKsB1++5HQjd7mN{~wIaoyL-&;*~MgQ}J+Io7SmQ6(#8czF}wDwYz zP*a=hpY)Xw$#fOY>(DB4z_-{;=Z`ky=6QRxornQb56ov9Q>!+EoEKq2?%k0J`V|E+ zNohpIl882Wd!NX@jPJ<(w=uY1jOvSu)H7OF->YLVyctX$_gWst4&7Dka>xH!hk{pE zS2@V~zXm5H`BI PoLq%QUq&|)D(FJ3{J(aHlDwrKXZ(pZT$N1d!!+Eg zY=0X=Zw}GB*4?h;eo& =`iS P JUe5t5WvhfJCO*C>wEK5vNks%=-Xm7%+f2ZuOqWtB0Y zc)BfiBUP6su(}lU4TEZ!49xDaI8clNe<-{4o{iw+&3H#G@rzmwl!-XWj>0KHjN+;i z1_Bbz)XFJ~JdwQZf~*>qi %-0z z!+*Q2fg-(KXVj^0r`qd8)?UQ=aADE7G!$C(uuO#yCxffJz8rrztG{O0uH6dwn_1Q3 zQx^Q_0ya3EhQl{wC6$$xuR;jac0@jmW{>lHYw<0e)_sNaEtj}Uf1(qSYk=7;tT7I< z*wi`GKE6m(6*kx->I}yJc5TUiQE3#xtwaNkXQ;lI>+$AdRubH!mPzR{)71>cs)z)o z^QAOXdM-E%eH)?1tox015BwCAdpEsaA$~Qp9zT@Mwf!qdDM;|`{1*w>{l#DPF^|ff zGVXwfo24v42>G9dg$FsKCt&}cM)o~`LePP$r4mtoWXDo`?hf}D)Sc8|6m~i+ zkc;+MbGIUU>$Ve$OG?G6RgiVFP{MfH)W<_2Y=jQDu9@mtdpP_qANw^D9qr+Fvz3zO z@_WB0LnptGfXQ_)oN3s}FyB~ w#y_Ntqs|tO}DJy9rWDeCTI9 zlvEe3KAdl8SP4Hv HM3eGZxNj%~#bpts7`CadxvuOM;{(#mO zYE|v_v5ffAckC{^?gNBrg81OkDqAC-%l@9|+ k+7BTv%N sR#sy*+Z+ zH6yg>N-7RV Z+bf)T0G!v2A_jO@*ndOM8?RD`)!!ezg-bR)OQAb9R`e zv!&&_j~`9|w?%h^20DwKD?M15Jc@63GL*be1(fB ))6Cc=-xD#c3gRm-H+*4mdddH&|(^u`W(VuwH z$yRKJaI5RKCA32^P&Bj2P L8s0keaS&7i{W?=?%%?@qW|1LZE8>A^$RB%UM%C&<~Rxcj(4mTL-H&ee=Dbvpyy zbm%^x07AT>Cgdgp{)u%YLRtd>yfVDQKllZ2#{6i@(AGL7&slNd7TL-o 0hySwx8vQ{3^dghioSIcQtIRU@xjL>GQweHBpC~J+4 z4gHzV)K-AV4>IG}9RhIN6b}9^-aMkT5a$D{hFfcV!rH@@vh$1F3@Y1NxTLH%Q$on0 zniQ`Goz-3uXED?lGi9Tn5p_a3YobLvy^aQ{$?_yc7}dq|mDPLJAm&9f?wE4`BgaWi zC97TVE+oc$b_Nw$LMBA0PbVQR3Z3q6%43Ehz!eCN#y%S`;@!Dgr&NwvK^Rc`YGjUJ zw&$yrI5Tv2kmboO^j3d4l(LlG7M9vH8rnU9%QlZq&!Ni|O>H{v!;a6+CsA}~#LI&n z)+4rF7QlMZM)*gbm|+dpO4Mx4bGXmArc8 zW!PY5ye#+*6`hXVj%R_94$-imbC~HxgpVx_EQz~wRUAAUfei_cVo=$xP_V$j`u#`V z$86$K%iN^kN4mAx8)(iFye`rlM8fNS+5&?xK9Ym#?scIi{^7D*#3M)HG~?RBr1g=2 z@b@$uXsat$E>gi^(XkFfkwLB0sB0p61z1=92* pMxe}b>DF!`zqX}wrGmLJ$9m4c!|}Js z$PCF0=x>#k^xBNJQWaCAsMFf~(-ZQrDxW;wo{RS%QUTnyEzLID+1W_>)$lNjRWLjP z4L;(d F>` te8^Cz+E7I+^Pe9v7uFh;@6c;t1&P0LW?q~L#YnI6&UU2}B^ZB!*)#G;l zxLJZkp!m57IWhm4SYHl2X|4wyu=~OKz(qg$!(Pfh88W)%DRl1OgBMy>$B}ii^1T1d zqb^C-3IDSzAZCny|B#*YQtOHa1L6G5lJQwQ`!^^xy7?xE#t+94C-q~XOhJQxEIv$z zhQa= Wn=Pymf9$_WYj`m73 zkE6|T$3v?@^+NP188>f5$m}rxF3n=Tu6ajSF1%mU*=}ygz(%m `9N%dG{v3VVYUCUpRC&=p;05KrHB}$)qd4f!zIPp&aZ6 zq|4`dI46Jvx%Pzg(T^eSIsr03GganY3MZXU1WO%M8C`e53dMV-omg)P4Fhv%5rSOx zpEj^;Y7D?wydMg3j=w#j2YDy~k$lk#mhUmhlZ$e(2)*TGS36kn&)C-YS-1wRd48BW z&iFd|P9Wx*XeN|wOc6~}Lw~QVPSIa}ZP}n#6^JMBxtP;oghG=q2pCS|WI1Db*>!&2 zP+){v&1NU~`%8T$32M(p^@a^@#7;Xjg-oVH$-VSdT(`4zv=IF`sJ5z3mIIe18N*!Z zvGzRBrHpV{wbrt3I28jGxQM&>@$9P2?TF0@of&qm$+%n24K&r|=Scp_<9{fFJk=M{ zjpaL)nCg!E21W=dhQZhNaQ^D7^6}IovF1%fN>&})RltMgzw5bfiz^q6`nid{J&lpee=>Kjn>x#f zLR{1=EGF3a$-1V7UY&&m`N&W3hR|MG75Za-jEA?Z{^llob}xhc+-B6a!w)0mYBQdA z4kgxYYlCUFZ9fkLg`d!(%^Na6ENN%?ZO8L_@Mrml%w8X`U2Hl~PGdfkKF-jn&p-6V zd(s3xoci{anqFzwJ1z$xq8*#m$00n|qX60*;;F@nSHHSrqFKF`w7n+`eAH}3*Qbi` zZd_jVtkanL1g|uXt_f4;FOj3aEj+yZ*a^T<>ZL7fJYX9=p_orTLhN@}G2*lCXCD@H zU8D%$gBQB9(r7L3V2UbNSsEwFlwky!WTz0qWXY%dG>!YrcrcM|CeeO~)1BJ^Nb`Ch zMiHx&ADqx;eWR-fK9p6R)+4VOMBIk=NpZuG;_c#uy|htV!$w^_4`z5!Oxi(+wK-p; ziM$f+Z>kv9EDYf%kN0MI{#TgBL%rQ(e(@woebElhw62&|)O7<*OW{kU%bMyt>sMN0 zXzT8=TA;gD!f$(*n%*QLJa(Jg&T8ppTMG$7$s?OIVl}9o37hR4j2!qu$~SDb2usM_ zY#d7)ICfOdq@1);vU6cSvQrexJS{N~Yc7#N`2=)b^fi*4X94XaIsIpM?(LT~un D32qrBvF@hxFOB~NYacC z4tAV0`bv{b3*%f$R!#%XGYxq2oX)TN(+KgsdwabJQEujO`entmw(Eq3hB{CH=8CaC z{YvmW (_mXbdR3Bq=GaoAZ0a zc&)M_1G*h)9CdzP-Hta{^VE*u^z|7q_|EVZ5c{K0ES$StEW$~?0K^}%77!_={FQ3j zuBt99i`1nV6ZwpJMIa}v zSBPv$)f$7M?R$E)nv*mSF`;jVGmyi_ASs7u1_9l`2GGnPd?Vz$i7+~z!mr;xUtI+c z&^#Fgi-TD)-w6?D6q0&vBf>nurdc6`Sz;iH-7W<8p4pl~8yTe|N3^D47IYWq4lHEa zUYu(xaY-|pe5%pvg*b=mQ`&FO?fZ{KknhH$d^&IJa=g|eAIZtdTj{!kXG0^Q(9Z2a zT=P9eERV&YoaZ*fC Y z_|4vv)ZQgI@D19Vd8suci_4^4=mkAhh}SY2PZw3^nLqD^Tq$*>P0C~)J^u-My@a>j z5@zfcWUH3sqgNo2zKEZ5P6GN`GJa-{SAo-k;N#c*_`tDqE3oB^OY~#jBAFxnerE_} z#@p&g1v2;Z8H?uPcE|Szo(9jTzvyNcXjO#Tp$ _bdpU$@zl!Y1T;ww((7U zo$Y~E9XSI{E0^Sk%A^RKXagL!#$1*J>WBWg(_}kC+rfZtcZmukq_bv>#)WWr^l4yR z+I&q*hU$#cp47+S%16c>izZX#dIdTLA%5$}#*@NiwdRQ{vf|+_+2Ke9gGhduVdRC~ z6A@sclCDA6rWs#0Td>7l@%1|sV(aABi9Yi 91vW$)uNL4i+|g>TUdW+<}CnW)u*8noiSlrMs&WehKBY~~#t8KJBRSv6#+D7}I5 zjrhmUF;cgXgQlC;n&sEsM$6335XxzK)viovQPE+bJeWNtn9@l(Hr@IpNcAz1)P-qj z8B(b|G@Dt60YEo^ B&bwecqyD0Bk45QlDzpcJQ$|aDHC%;zbYk-<9M4 z{?M5d$$EeyS8?BL97`kgx1Ju$N}}PVyr++AQOcc&lmj<^KednlZtERVj_wLX6bFB! zi!01t6YMzCPxV{mWE1PjGtGO~5A7Bf1E>Ls=&E?5n$6QN<#O;4kS>I+i$^-703?!? zoeqc?yL=PO?|vtxVY4iyb9(*SP4dy7> wDIPS$#hb;f m8B80-~gRxR9}wR584(3_`^l#J7%pf zT{!B}xSMkJ>1{V+-T2y?b!I&&FeM=_5$CP4V$hOKHTowXR|fO+PL&?%dzvX~cdoYG zRhGn>=Drano(#P%n*&0$&qt)$Tj|>9hOu%qC2M-{$a*fsS=|K!kaVx{X--uVx^v(4 z_7wxh&`wf7T88Vv@P%htX5^qd`Ad~ozqpT5D_WJQm;?V+WD#j-_rQS!yz>d9&!GA3 zs0hv(cj>5WbpKB2_T6{+#o#LwqIwaerD;^@*~tjHH`ijsUewrY1r6^{l3SJg6w?%g zbN4h$jI2z34+X-EuZd>nV{){QaQh==69_MmP)w6Bke~SsWis$jqY@(J3=X{M3}5%0 z)&S-h2zEz_iq#hQ#Ip(`qeJ9eXu;jA-Jb*wlxj#jlf1rx>kVHS``z{67*a}fC0|~K z9x=U(3b>tbH>f?Nh3b=vg%`Aq-5rzcKJvZP@51UqEJ;tJI_ygA2utzI)i#6k`a3O) z_YwyJCm(jsI;-wey~G9Wi12|i>(<^KI0obUu)GcX`c3j)Ec-m4G^0PxtBx6dGww`9 zgwlQ{urOKn#3~gKPX;JLYDMIH$>k_|ks-h?5qt;% `yohJ~=o9u+T^vj_SL3urRgIatL_|ZaSdglshvw)-yLK3%@d1DO zQ;x-6+B(_N7 kVBrG)z@ zxym`FTMaNuRNX@$1_ga@Q`ycFuo=$qUzLmNO}@2lp@}+Yc0P)tuR;iB>i^kwi%J `nyR(9Qh#yS#AGms#1ne15lcbEG!$n+DH7b9Zoyq1|YHJbi zN!9|tk ~azT5_zn}2c#`%@sxy`tmna%u!4qx@3k0g7|Jpk zr|FSsc2oAL5eOLNsE-z;Q4090T}mO~`xIUj7mm!dU(OI0vZ~B-;}|DGaiR2+`Lyf# z!_Nc_`vzm-?N=)6^R3EvIxnP*@Jlydzd%F(G;+wVH{b}T*;~4wIi>r $ DQTH l-6Qf#__qc_ zoEIwF!ml5@Bt$bDhyTSC;vzorRS!4PeP5Qj8c%7wg3Q+moWs-MA3k31W$xBGwzVjn z-~ZOJGBG`MF-oo86FB@h*Z>Z)c XPPYG??|}r0uHRG!4z!Y{~>>=e279! z1$GwH!7eTjY)iP;2# zE+}V_q$a1 GdRF-JNGiVL)>-I=0i;rCOPlL_xIw*=T@nZCYL){`l7ZtY&w_ig}IaCMKEwc zJI
epV3_o$-b8ntq5^Nh&?3gF6(r8dXWN-IuE<7q?bdm#HOl3~s)E zFG}=fP(f0~AQeaMo_aKgyF?uVIVPKRIGSMSS0*5tTmNzVl3q!?h~UpjX{Z7Oz!BoA zo3E;M?e=!IGj6n=wAK7&4KGywKUgb`Ynv`k3Vc76-h}stZ?iEX?Kh|!HF(M3dTxne zZ5r lBi^;@G3svcFAQ-JMX#Y^{#-;MUmTK>5#s{Ip6N}WXj z!6%9BFjj%t1ta(^=i(I$(~7}A?;_IXuXbdZA{Pg`#zkR4L`)gA|NgQcDbmzWeg7Y= zm&CJ;|LkLMcp7mzDxhyUl&0!0Ku1qJX#jqC*K$h4%R2lUq(&zD$*Uc4#xFM7q5oFZ z4u6S}yXl7vkS~~9?D;Q?S7E6BQ2Hm3jU17&0vOQpUv~o9wCBicrD r!QJ12|SEUZn!+JwG|)#Uz!T=G}PJ2^SY1F80~GP %T`H$|E0wqB%*&0ut!Ncx|5;h`lJ4Xg YJWzK`kl1SZ(q@AImQ$0hlr{_cAHAGpx&QzG diff --git a/docs/rest-api/source/stylesheets/_lang-selector.scss b/docs/rest-api/source/stylesheets/_lang-selector.scss index 4b3b20268ae..6988d32149f 100644 --- a/docs/rest-api/source/stylesheets/_lang-selector.scss +++ b/docs/rest-api/source/stylesheets/_lang-selector.scss @@ -6,11 +6,11 @@ &:active, &:focus, &:hover { - border-top-color: #9b5c8f; + border-top-color: #7f54b3; } &.active { - border-top-color: #9b5c8f; + border-top-color: #7f54b3; } } } diff --git a/docs/rest-api/source/stylesheets/_variables.scss b/docs/rest-api/source/stylesheets/_variables.scss index d4b22d70bcd..7203a19c0de 100644 --- a/docs/rest-api/source/stylesheets/_variables.scss +++ b/docs/rest-api/source/stylesheets/_variables.scss @@ -26,18 +26,18 @@ $examples-bg: #393939 !default; $code-bg: #292929 !default; $code-annotation-bg: #191d1f !default; $nav-subitem-bg: #f7f7f7 !default; -$nav-active-bg: #9b5c8f !default; +$nav-active-bg: #7f54b3 !default; $nav-active-parent-bg: #f7f7f7 !default; // parent links of the current section $lang-select-border: #000 !default; $lang-select-bg: #222 !default; $lang-select-active-bg: $examples-bg !default; // feel free to change this to blue or something $lang-select-pressed-bg: #111 !default; // color of language tab bg when mouse is pressed $main-bg: #ffffff !default; -$aside-notice-bg: #9b5c8f !default; +$aside-notice-bg: #7f54b3 !default; $aside-warning-bg: #ca4949 !default; $aside-success-bg: #38a845 !default; $search-notice-bg: #c97a7e !default; -$link-color: #804877; +$link-color: #7f54b3; // TEXT COLORS //////////////////// From a30b1ac0942e81d54947ec0561a22a492e919101 Mon Sep 17 00:00:00 2001 From: Guilherme Pressutto Date: Tue, 23 Jan 2024 12:25:10 -0300 Subject: [PATCH 05/52] Revert "Revert "Displaying Clearpay instead of Afterpay for UK based stores"" (#8046) --- assets/images/payment-methods/afterpay.svg | 5 - assets/images/payment-methods/clearpay.svg | 4 + changelog/afterpay-clearpay | 4 + .../connect-account-page/payment-methods.tsx | 7 +- client/payment-methods-icons.tsx | 5 + client/payment-methods-map.tsx | 42 +- includes/class-wc-payment-gateway-wcpay.php | 21 +- includes/class-wc-payments-checkout.php | 5 +- includes/class-wc-payments.php | 4 +- .../PaymentMethodsCompatibility.php | 2 +- .../class-afterpay-payment-method.php | 29 + .../class-cc-payment-method.php | 6 +- .../class-upe-payment-method.php | 8 +- ...st-class-payment-methods-compatibility.php | 2 + .../test-class-upe-payment-gateway.php | 1029 +++++++++++++ .../test-class-upe-split-payment-gateway.php | 1290 +++++++++++++++++ .../test-class-wc-payment-gateway-wcpay.php | 30 +- .../unit/test-class-wc-payments-checkout.php | 3 + 18 files changed, 2457 insertions(+), 39 deletions(-) create mode 100644 assets/images/payment-methods/clearpay.svg create mode 100644 changelog/afterpay-clearpay create mode 100644 tests/unit/payment-methods/test-class-upe-payment-gateway.php create mode 100644 tests/unit/payment-methods/test-class-upe-split-payment-gateway.php diff --git a/assets/images/payment-methods/afterpay.svg b/assets/images/payment-methods/afterpay.svg index a769af42cdd..3795553025f 100644 --- a/assets/images/payment-methods/afterpay.svg +++ b/assets/images/payment-methods/afterpay.svg @@ -1,9 +1,5 @@ diff --git a/assets/images/payment-methods/clearpay.svg b/assets/images/payment-methods/clearpay.svg new file mode 100644 index 00000000000..bce4db33418 --- /dev/null +++ b/assets/images/payment-methods/clearpay.svg @@ -0,0 +1,4 @@ + diff --git a/changelog/afterpay-clearpay b/changelog/afterpay-clearpay new file mode 100644 index 00000000000..23a06913442 --- /dev/null +++ b/changelog/afterpay-clearpay @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Displaying Clearpay instead of Afterpay for UK based stores diff --git a/client/connect-account-page/payment-methods.tsx b/client/connect-account-page/payment-methods.tsx index 9add778d558..1930ed10616 100644 --- a/client/connect-account-page/payment-methods.tsx +++ b/client/connect-account-page/payment-methods.tsx @@ -12,6 +12,7 @@ import './style.scss'; import { AffirmIcon, AfterpayIcon, + ClearpayIcon, AmericanExpressIcon, ApplePayIcon, DinersClubIcon, @@ -38,7 +39,11 @@ const PaymentMethods: React.FC = () => { - + { 'GB' === wcpaySettings?.connect?.country ? ( + + ) : ( + + ) } & more. > diff --git a/client/payment-methods-icons.tsx b/client/payment-methods-icons.tsx index 1cad55b62f2..bc29d2518f7 100644 --- a/client/payment-methods-icons.tsx +++ b/client/payment-methods-icons.tsx @@ -18,6 +18,7 @@ import IdealAsset from 'assets/images/payment-methods/ideal.svg?asset'; import BankDebitAsset from 'assets/images/payment-methods/bank-debit.svg?asset'; import AffirmAsset from 'assets/images/payment-methods/affirm.svg?asset'; import AfterpayAsset from 'assets/images/payment-methods/afterpay.svg?asset'; +import ClearpayAsset from 'assets/images/payment-methods/clearpay.svg?asset'; import JCBAsset from 'assets/images/payment-methods/jcb.svg?asset'; import KlarnaAsset from 'assets/images/payment-methods/klarna.svg?asset'; import VisaAsset from 'assets/images/cards/visa.svg?asset'; @@ -57,6 +58,10 @@ export const AfterpayIcon = iconComponent( AfterpayAsset, __( 'Afterpay', 'woocommerce-payments' ) ); +export const ClearpayIcon = iconComponent( + ClearpayAsset, + __( 'Clearpay', 'woocommerce-payments' ) +); export const AmericanExpressIcon = iconComponent( AmexAsset, __( 'American Express', 'woocommerce-payments' ) diff --git a/client/payment-methods-map.tsx b/client/payment-methods-map.tsx index d0a17f282ea..be758dd10ad 100644 --- a/client/payment-methods-map.tsx +++ b/client/payment-methods-map.tsx @@ -10,6 +10,7 @@ import { __ } from '@wordpress/i18n'; import { AffirmIcon, AfterpayIcon, + ClearpayIcon, BancontactIcon, BankDebitIcon, CreditCardIcon, @@ -23,6 +24,18 @@ import { SofortIcon, } from 'wcpay/payment-methods-icons'; +declare global { + interface Window { + wcpaySettings: { + accountStatus: { + country: string; + }; + }; + } +} + +const accountCountry = window.wcpaySettings?.accountStatus?.country || 'US'; + export interface PaymentMethodMapEntry { id: string; label: string; @@ -208,16 +221,29 @@ const PaymentMethodInformationObject: Record< }, afterpay_clearpay: { id: 'afterpay_clearpay', - label: __( 'Afterpay', 'woocommerce-payments' ), + label: + 'GB' === accountCountry + ? __( 'Clearpay', 'woocommerce-payments' ) + : __( 'Afterpay', 'woocommerce-payments' ), brandTitles: { - afterpay_clearpay: __( 'Afterpay', 'woocommerce-payments' ), + afterpay_clearpay: + 'GB' === accountCountry + ? __( 'Clearpay', 'woocommerce-payments' ) + : __( 'Afterpay', 'woocommerce-payments' ), }, - description: __( - // translators: %s is the store currency. - 'Allow customers to pay over time with Afterpay. Available to all customers paying in %s.', - 'woocommerce-payments' - ), - icon: AfterpayIcon, + description: + 'GB' === accountCountry + ? __( + // translators: %s is the store currency. + 'Allow customers to pay over time with Clearpay. Available to all customers paying in %s.', + 'woocommerce-payments' + ) + : __( + // translators: %s is the store currency. + 'Allow customers to pay over time with Afterpay. Available to all customers paying in %s.', + 'woocommerce-payments' + ), + icon: 'GB' === accountCountry ? ClearpayIcon : AfterpayIcon, currencies: [ 'USD', 'AUD', 'CAD', 'NZD', 'GBP', 'EUR' ], stripe_key: 'afterpay_clearpay_payments', allows_manual_capture: false, diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 983539be3cf..0068a32e33f 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -497,6 +497,7 @@ public function __construct( * @return void */ public function init_hooks() { + add_action( 'init', [ $this, 'maybe_update_properties_with_country' ] ); // Only add certain actions/filter if this is the main gateway (i.e. not split UPE). if ( self::GATEWAY_ID === $this->id ) { add_action( 'woocommerce_order_actions', [ $this, 'add_order_actions' ] ); @@ -526,6 +527,22 @@ public function init_hooks() { $this->maybe_init_subscriptions_hooks(); } + /** + * Updates icon and title using the account country. + * This method runs on init is not in the controller because get_account_country might + * make a request to the API if the account data is not cached. + * + * @return void + */ + public function maybe_update_properties_with_country(): void { + if ( Afterpay_Payment_Method::PAYMENT_METHOD_STRIPE_ID !== $this->stripe_id ) { + return; + } + $account_country = $this->get_account_country(); + $this->icon = $this->payment_method->get_icon( $account_country ); + $this->title = $this->payment_method->get_title( $account_country ); + } + /** * Displays HTML tags for WC payment gateway radio button content. */ @@ -2106,7 +2123,7 @@ public function set_payment_method_title_for_order( $order, $payment_method_type return; } - $payment_method_title = $payment_method->get_title( $payment_method_details ); + $payment_method_title = $payment_method->get_title( $this->get_account_country(), $payment_method_details ); $payment_gateway = in_array( $payment_method->get_id(), [ Payment_Method::CARD, Payment_Method::LINK ], true ) ? self::GATEWAY_ID : self::GATEWAY_ID . '_' . $payment_method_type; @@ -2887,7 +2904,7 @@ protected function get_deposit_delay_days( int $default_value = 7 ): int { * * @return string code of the country. */ - protected function get_account_country( string $default_value = Country_Code::UNITED_STATES ): string { + public function get_account_country( string $default_value = Country_Code::UNITED_STATES ): string { try { if ( $this->is_connected() ) { return $this->account->get_account_country() ?? $default_value; diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index 98a647b5bff..5ba354b3810 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -295,10 +295,11 @@ public function get_enabled_payment_method_config() { } $payment_method = $this->gateway->wc_payments_get_payment_method_by_id( $payment_method_id ); + $account_country = $this->account->get_account_country(); $settings[ $payment_method_id ] = [ 'isReusable' => $payment_method->is_reusable(), - 'title' => $payment_method->get_title(), - 'icon' => $payment_method->get_icon(), + 'title' => $payment_method->get_title( $account_country ), + 'icon' => $payment_method->get_icon( $account_country ), 'showSaveOption' => $this->should_upe_payment_method_show_save_option( $payment_method ), 'countries' => $payment_method->get_countries(), ]; diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index b4b64d4ea23..94d30cd636f 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -293,6 +293,8 @@ public static function init() { include_once __DIR__ . '/class-wc-payments-utils.php'; include_once __DIR__ . '/core/class-mode.php'; + self::$mode = new Mode(); + include_once __DIR__ . '/class-database-cache.php'; self::$database_cache = new Database_Cache(); self::$database_cache->init_hooks(); @@ -539,8 +541,6 @@ public static function init() { self::$card_gateway->init_hooks(); self::$wc_payments_checkout->init_hooks(); - self::$mode = new Mode(); - self::$webhook_processing_service = new WC_Payments_Webhook_Processing_Service( self::$api_client, self::$db_helper, self::$account, self::$remote_note_service, self::$order_service, self::$in_person_payments_receipts_service, self::get_gateway(), self::$customer_service, self::$database_cache ); self::$webhook_reliability_service = new WC_Payments_Webhook_Reliability_Service( self::$api_client, self::$action_scheduler_service, self::$webhook_processing_service ); diff --git a/includes/multi-currency/PaymentMethodsCompatibility.php b/includes/multi-currency/PaymentMethodsCompatibility.php index 8f4f0f88e8b..7f48df769d5 100644 --- a/includes/multi-currency/PaymentMethodsCompatibility.php +++ b/includes/multi-currency/PaymentMethodsCompatibility.php @@ -82,7 +82,7 @@ function ( $result, $method ) { $result[ $method ] = [ 'currencies' => $payment_method_instance->get_currencies(), - 'title' => $payment_method_instance->get_title(), + 'title' => $payment_method_instance->get_title( $this->gateway->get_account_country() ), ]; return $result; diff --git a/includes/payment-methods/class-afterpay-payment-method.php b/includes/payment-methods/class-afterpay-payment-method.php index 9984ae212b7..674f648d5e2 100644 --- a/includes/payment-methods/class-afterpay-payment-method.php +++ b/includes/payment-methods/class-afterpay-payment-method.php @@ -65,6 +65,35 @@ public function __construct( $token_service ) { ]; } + /** + * Returns payment method title. + * + * @param string|null $account_country Country of merchants account. + * @param array|false $payment_details Optional payment details from charge object. + * @return string|null + */ + public function get_title( string $account_country = null, $payment_details = false ) { + if ( 'GB' === $account_country ) { + return __( 'Clearpay', 'woocommerce-payments' ); + } + + return __( 'Afterpay', 'woocommerce-payments' ); + } + + /** + * Returns payment method icon. + * + * @param string|null $account_country Country of merchants account. + * @return string|null + */ + public function get_icon( string $account_country = null ) { + if ( 'GB' === $account_country ) { + return plugins_url( 'assets/images/payment-methods/clearpay.svg', WCPAY_PLUGIN_FILE ); + } + + return plugins_url( 'assets/images/payment-methods/afterpay.svg', WCPAY_PLUGIN_FILE ); + } + /** * Returns testing credentials to be printed at checkout in test mode. * diff --git a/includes/payment-methods/class-cc-payment-method.php b/includes/payment-methods/class-cc-payment-method.php index dfd20c3eaf6..ae7f0a485dd 100644 --- a/includes/payment-methods/class-cc-payment-method.php +++ b/includes/payment-methods/class-cc-payment-method.php @@ -33,11 +33,11 @@ public function __construct( $token_service ) { /** * Returns payment method title * - * @param array|bool $payment_details Optional payment details from charge object. - * + * @param string|null $account_country Account country. + * @param array|false $payment_details Payment details. * @return string */ - public function get_title( $payment_details = false ) { + public function get_title( string $account_country = null, $payment_details = false ) { if ( ! $payment_details ) { return $this->title; } diff --git a/includes/payment-methods/class-upe-payment-method.php b/includes/payment-methods/class-upe-payment-method.php index ddc954e719f..6f0fa965a88 100644 --- a/includes/payment-methods/class-upe-payment-method.php +++ b/includes/payment-methods/class-upe-payment-method.php @@ -113,11 +113,12 @@ public function get_id() { /** * Returns payment method title * - * @param array|bool $payment_details Optional payment details from charge object. + * @param string|null $account_country Country of merchants account. + * @param array|false $payment_details Optional payment details from charge object. * * @return string */ - public function get_title( $payment_details = false ) { + public function get_title( string $account_country = null, $payment_details = false ) { return $this->title; } @@ -224,9 +225,10 @@ abstract public function get_testing_instructions(); /** * Returns the payment method icon URL or an empty string. * + * @param string|null $account_country Optional account country. * @return string */ - public function get_icon() { + public function get_icon( string $account_country = null ) { return isset( $this->icon_url ) ? $this->icon_url : ''; } diff --git a/tests/unit/multi-currency/test-class-payment-methods-compatibility.php b/tests/unit/multi-currency/test-class-payment-methods-compatibility.php index 6a0f24eeba8..7a6e910db46 100644 --- a/tests/unit/multi-currency/test-class-payment-methods-compatibility.php +++ b/tests/unit/multi-currency/test-class-payment-methods-compatibility.php @@ -54,9 +54,11 @@ public function set_up() { ->setMethods( [ 'get_upe_enabled_payment_method_ids', + 'get_account_country', ] ) ->getMock(); + $this->gateway_mock->method( 'get_account_country' )->willReturn( 'US' ); $this->payment_methods_compatibility = new \WCPay\MultiCurrency\PaymentMethodsCompatibility( $this->multi_currency_mock, $this->gateway_mock ); $this->payment_methods_compatibility->init_hooks(); diff --git a/tests/unit/payment-methods/test-class-upe-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-payment-gateway.php new file mode 100644 index 00000000000..8260f8707b5 --- /dev/null +++ b/tests/unit/payment-methods/test-class-upe-payment-gateway.php @@ -0,0 +1,1029 @@ + 'success', + 'payment_needed' => true, + 'redirect' => 'testURL/key=mock_order_key', + ]; + + /** + * WC_Payments_Localization_Service instance. + * + * @var WC_Payments_Localization_Service + */ + private $mock_localization_service; + + /** + * Mock Fraud Service. + * + * @var WC_Payments_Fraud_Service|MockObject; + */ + private $mock_fraud_service; + + /** + * Pre-test setup + */ + public function set_up() { + parent::set_up(); + + // Arrange: Mock WC_Payments_API_Client so we can configure the + // return value of create_and_confirm_intention(). + // Note that we cannot use createStub here since it's not defined in PHPUnit 6.5. + $this->mock_api_client = $this->getMockBuilder( 'WC_Payments_API_Client' ) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get_payment_method', + 'is_server_connected', + 'get_timeline', + ] + ) + ->getMock(); + + $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class ); + $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'US' ); + $this->mock_wcpay_account->method( 'get_account_default_currency' )->willReturn( 'USD' ); + + // Mock the main class's cache service. + $this->_cache = WC_Payments::get_database_cache(); + $this->mock_cache = $this->createMock( Database_Cache::class ); + WC_Payments::set_database_cache( $this->mock_cache ); + + $payment_methods = [ + 'link' => [ + 'base' => 0.1, + ], + ]; + + $this->mock_wcpay_account + ->expects( $this->any() ) + ->method( 'get_fees' ) + ->willReturn( $payment_methods ); + + $this->mock_woopay_utilities = $this->createMock( WooPay_Utilities::class ); + + // Arrange: Mock WC_Payments_Customer_Service so its methods aren't called directly. + $this->mock_customer_service = $this->getMockBuilder( 'WC_Payments_Customer_Service' ) + ->disableOriginalConstructor() + ->getMock(); + + // Arrange: Mock WC_Payments_Customer_Service so its methods aren't called directly. + $this->mock_token_service = $this->getMockBuilder( 'WC_Payments_Token_Service' ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'add_payment_method_to_user' ] ) + ->getMock(); + + // Arrange: Mock WC_Payments_Action_Scheduler_Service so its methods aren't called directly. + $this->mock_action_scheduler_service = $this->getMockBuilder( 'WC_Payments_Action_Scheduler_Service' ) + ->disableOriginalConstructor() + ->getMock(); + + $this->mock_dpps = $this->createMock( Duplicate_Payment_Prevention_Service::class ); + + $this->mock_localization_service = $this->createMock( WC_Payments_Localization_Service::class ); + $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class ); + + $this->mock_payment_methods = []; + $payment_method_classes = [ + CC_Payment_Method::class, + Giropay_Payment_Method::class, + Sofort_Payment_Method::class, + Bancontact_Payment_Method::class, + EPS_Payment_Method::class, + P24_Payment_Method::class, + Ideal_Payment_Method::class, + Sepa_Payment_Method::class, + Becs_Payment_Method::class, + Link_Payment_Method::class, + Affirm_Payment_Method::class, + Afterpay_Payment_Method::class, + ]; + + $this->mock_rate_limiter = $this->createMock( Session_Rate_Limiter::class ); + foreach ( $payment_method_classes as $payment_method_class ) { + $mock_payment_method = $this->getMockBuilder( $payment_method_class ) + ->setConstructorArgs( [ $this->mock_token_service ] ) + ->onlyMethods( [ 'is_subscription_item_in_cart', 'get_icon' ] ) + ->getMock(); + $this->mock_payment_methods[ $mock_payment_method->get_id() ] = $mock_payment_method; + } + + $this->mock_order_service = $this->getMockBuilder( WC_Payments_Order_Service::class ) + ->setConstructorArgs( + [ + $this->mock_api_client, + ] + ) + ->onlyMethods( + [ + 'get_payment_method_id_for_order', + ] + ) + ->getMock(); + + $this->mock_payment_method = $this->getMockBuilder( $payment_method_class ) + ->setConstructorArgs( [ $this->mock_token_service ] ) + ->onlyMethods( [ 'is_subscription_item_in_cart', 'get_icon' ] ) + ->getMock(); + $this->mock_payment_methods[ $this->mock_payment_method->get_id() ] = $this->mock_payment_method; + + // Arrange: Mock WC_Payment_Gateway_WCPay so that some of its methods can be + // mocked, and their return values can be used for testing. + $this->mock_gateway = $this->getMockBuilder( WC_Payment_Gateway_WCPay::class ) + ->setConstructorArgs( + [ + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + $this->mock_payment_method, + $this->mock_payment_methods, + $this->mock_rate_limiter, + $this->mock_order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service, + ] + ) + ->setMethods( + [ + 'get_return_url', + 'manage_customer_details_for_order', + 'parent_process_payment', + 'get_upe_enabled_payment_method_statuses', + 'is_payment_recurring', + ] + ) + ->getMock(); + + // Arrange: Set the return value of get_return_url() so it can be used in a test later. + $this->mock_gateway + ->expects( $this->any() ) + ->method( 'get_return_url' ) + ->will( + $this->returnValue( $this->return_url ) + ); + $this->mock_gateway + ->expects( $this->any() ) + ->method( 'parent_process_payment' ) + ->will( + $this->returnValue( $this->mock_payment_result ) + ); + + // Arrange: Define a $_POST array which includes the payment method, + // so that get_payment_method_from_request() does not throw error. + $_POST = [ + 'wcpay-payment-method' => 'pm_mock', + 'payment_method' => WC_Payment_Gateway_WCPay::GATEWAY_ID, + ]; + + // Mock the level3 service to always return an empty array. + $mock_level3_service = $this->createMock( Level3Service::class ); + $mock_level3_service->expects( $this->any() ) + ->method( 'get_data_from_order' ) + ->willReturn( [] ); + wcpay_get_test_container()->replace( Level3Service::class, $mock_level3_service ); + + // Mock the order service to always return an empty array for meta. + $mock_order_service = $this->createMock( OrderService::class ); + $mock_order_service->expects( $this->any() ) + ->method( 'get_payment_metadata' ) + ->willReturn( [] ); + wcpay_get_test_container()->replace( OrderService::class, $mock_order_service ); + } + + /** + * Cleanup after tests. + * + * @return void + */ + public function tear_down() { + parent::tear_down(); + WC_Payments::set_database_cache( $this->_cache ); + wcpay_get_test_container()->reset_all_replacements(); + } + + public function test_process_payment_returns_correct_redirect_when_using_saved_payment() { + $order = WC_Helper_Order::create_order(); + $_POST = $this->setup_saved_payment_method(); + $intent = WC_Helper_Intention::create_intention(); + + $this->mock_gateway->expects( $this->once() ) + ->method( 'manage_customer_details_for_order' ) + ->will( + $this->returnValue( [ wp_get_current_user(), 'cus_123' ] ) + ); + $this->mock_wcpay_request( Create_And_Confirm_Intention::class, 1, $intent->get_id() ) + ->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $intent ); + + $this->set_cart_contains_subscription_items( false ); + + $result = $this->mock_gateway->process_payment( $order->get_id() ); + + $this->assertEquals( 'success', $result['result'] ); + $this->assertEquals( $this->return_url, $result['redirect'] ); + } + + public function test_process_payment_returns_correct_redirect_when_using_payment_request() { + $order = WC_Helper_Order::create_order(); + $intent = WC_Helper_Intention::create_intention(); + $_POST['payment_request_type'] = 'google_pay'; + + $this->mock_gateway->expects( $this->once() ) + ->method( 'manage_customer_details_for_order' ) + ->will( + $this->returnValue( [ wp_get_current_user(), 'cus_123' ] ) + ); + $this->mock_wcpay_request( Create_And_Confirm_Intention::class, 1, $intent->get_id() ) + ->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $intent ); + $this->set_cart_contains_subscription_items( false ); + + $result = $this->mock_gateway->process_payment( $order->get_id() ); + + $this->assertEquals( 'success', $result['result'] ); + $this->assertEquals( $this->return_url, $result['redirect'] ); + } + + public function is_proper_intent_used_with_order_returns_false() { + $this->assertFalse( $this->mock_gateway->is_proper_intent_used_with_order( WC_Helper_Order::create_order(), 'wrong_intent_id' ) ); + } + + public function test_process_redirect_payment_intent_processing() { + $order = WC_Helper_Order::create_order(); + $order_id = $order->get_id(); + $save_payment_method = false; + $user = wp_get_current_user(); + $intent_status = Intent_Status::PROCESSING; + $intent_metadata = [ 'order_id' => (string) $order_id ]; + $charge_id = 'ch_mock'; + $customer_id = 'cus_mock'; + $intent_id = 'pi_mock'; + $payment_method_id = 'pm_mock'; + + // Supply the order with the intent id so that it can be retrieved during the redirect payment processing. + $order->update_meta_data( '_intent_id', $intent_id ); + $order->save(); + + $payment_intent = WC_Helper_Intention::create_intention( + [ + 'status' => $intent_status, + 'metadata' => $intent_metadata, + ] + ); + + $this->mock_gateway->expects( $this->once() ) + ->method( 'manage_customer_details_for_order' ) + ->will( + $this->returnValue( [ $user, $customer_id ] ) + ); + + $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->will( $this->returnValue( $payment_intent ) ); + + $this->set_cart_contains_subscription_items( false ); + + $this->mock_gateway->process_redirect_payment( $order, $intent_id, $save_payment_method ); + + $result_order = wc_get_order( $order_id ); + $note = wc_get_order_notes( + [ + 'order_id' => $order_id, + 'limit' => 1, + ] + )[0]; + + $this->assertStringContainsString( 'authorized', $note->content ); + $this->assertEquals( $intent_id, $result_order->get_meta( '_intent_id', true ) ); + $this->assertEquals( $charge_id, $result_order->get_meta( '_charge_id', true ) ); + $this->assertEquals( $intent_status, $result_order->get_meta( '_intention_status', true ) ); + $this->assertEquals( $payment_method_id, $result_order->get_meta( '_payment_method_id', true ) ); + $this->assertEquals( $customer_id, $result_order->get_meta( '_stripe_customer_id', true ) ); + $this->assertEquals( Order_Status::ON_HOLD, $result_order->get_status() ); + } + + public function test_process_redirect_payment_intent_succeded() { + $order = WC_Helper_Order::create_order(); + $order_id = $order->get_id(); + $save_payment_method = false; + $user = wp_get_current_user(); + $intent_status = Intent_Status::SUCCEEDED; + $intent_metadata = [ 'order_id' => (string) $order_id ]; + $charge_id = 'ch_mock'; + $customer_id = 'cus_mock'; + $intent_id = 'pi_mock'; + $payment_method_id = 'pm_mock'; + + // Supply the order with the intent id so that it can be retrieved during the redirect payment processing. + $order->update_meta_data( '_intent_id', $intent_id ); + $order->save(); + + $payment_intent = WC_Helper_Intention::create_intention( + [ + 'status' => $intent_status, + 'metadata' => $intent_metadata, + ] + ); + + $this->mock_gateway->expects( $this->once() ) + ->method( 'manage_customer_details_for_order' ) + ->will( + $this->returnValue( [ $user, $customer_id ] ) + ); + + $request = $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->will( $this->returnValue( $payment_intent ) ); + + $this->set_cart_contains_subscription_items( false ); + + $this->mock_gateway->process_redirect_payment( $order, $intent_id, $save_payment_method ); + + $result_order = wc_get_order( $order_id ); + + $this->assertEquals( $intent_id, $result_order->get_meta( '_intent_id', true ) ); + $this->assertEquals( $charge_id, $result_order->get_meta( '_charge_id', true ) ); + $this->assertEquals( $intent_status, $result_order->get_meta( '_intention_status', true ) ); + $this->assertEquals( $payment_method_id, $result_order->get_meta( '_payment_method_id', true ) ); + $this->assertEquals( $customer_id, $result_order->get_meta( '_stripe_customer_id', true ) ); + $this->assertEquals( Order_Status::PROCESSING, $result_order->get_status() ); + } + + public function test_validate_order_id_received_vs_intent_meta_order_id_throw_exception() { + $order = WC_Helper_Order::create_order(); + $intent_metadata = [ 'order_id' => (string) ( $order->get_id() + 100 ) ]; + + $this->expectException( Process_Payment_Exception::class ); + $this->expectExceptionMessage( "We're not able to process this payment due to the order ID mismatch. Please try again later." ); + + \PHPUnit_Utils::call_method( + $this->mock_gateway, + 'validate_order_id_received_vs_intent_meta_order_id', + [ $order, $intent_metadata ] + ); + } + + public function test_validate_order_id_received_vs_intent_meta_order_id_returning_void() { + $order = WC_Helper_Order::create_order(); + $intent_metadata = [ 'order_id' => (string) ( $order->get_id() ) ]; + + $res = \PHPUnit_Utils::call_method( + $this->mock_gateway, + 'validate_order_id_received_vs_intent_meta_order_id', + [ $order, $intent_metadata ] + ); + + $this->assertSame( null, $res ); + } + + public function test_correct_payment_method_title_for_order() { + $order = WC_Helper_Order::create_order(); + + $visa_credit_details = [ + 'type' => 'card', + 'card' => [ + 'network' => 'visa', + 'funding' => 'credit', + ], + ]; + $visa_debit_details = [ + 'type' => 'card', + 'card' => [ + 'network' => 'visa', + 'funding' => 'debit', + ], + ]; + $mastercard_credit_details = [ + 'type' => 'card', + 'card' => [ + 'network' => 'mastercard', + 'funding' => 'credit', + ], + ]; + $eps_details = [ + 'type' => 'eps', + ]; + $giropay_details = [ + 'type' => 'giropay', + ]; + $p24_details = [ + 'type' => 'p24', + ]; + $sofort_details = [ + 'type' => 'sofort', + ]; + $bancontact_details = [ + 'type' => 'bancontact', + ]; + $sepa_details = [ + 'type' => 'sepa_debit', + ]; + $ideal_details = [ + 'type' => 'ideal', + ]; + $becs_details = [ + 'type' => 'au_becs_debit', + ]; + + $charge_payment_method_details = [ + $visa_credit_details, + $visa_debit_details, + $mastercard_credit_details, + $giropay_details, + $sofort_details, + $bancontact_details, + $eps_details, + $p24_details, + $ideal_details, + $sepa_details, + $becs_details, + ]; + + $expected_payment_method_titles = [ + 'Visa credit card', + 'Visa debit card', + 'Mastercard credit card', + 'giropay', + 'Sofort', + 'Bancontact', + 'EPS', + 'Przelewy24 (P24)', + 'iDEAL', + 'SEPA Direct Debit', + 'BECS Direct Debit', + ]; + + foreach ( $charge_payment_method_details as $i => $payment_method_details ) { + $this->mock_gateway->set_payment_method_title_for_order( $order, $payment_method_details['type'], $payment_method_details ); + $this->assertEquals( $expected_payment_method_titles[ $i ], $order->get_payment_method_title() ); + } + } + + public function test_payment_methods_show_correct_default_outputs() { + $mock_token = WC_Helper_Token::create_token( 'pm_mock' ); + $this->mock_token_service->expects( $this->any() ) + ->method( 'add_payment_method_to_user' ) + ->will( + $this->returnValue( $mock_token ) + ); + + $mock_user = 'mock_user'; + $mock_payment_method_id = 'pm_mock'; + + $mock_visa_details = [ + 'type' => 'card', + 'card' => [ + 'network' => 'visa', + 'funding' => 'debit', + ], + ]; + $mock_mastercard_details = [ + 'type' => 'card', + 'card' => [ + 'network' => 'mastercard', + 'funding' => 'credit', + ], + ]; + $mock_giropay_details = [ + 'type' => 'giropay', + ]; + $mock_p24_details = [ + 'type' => 'p24', + ]; + $mock_sofort_details = [ + 'type' => 'sofort', + ]; + $mock_bancontact_details = [ + 'type' => 'bancontact', + ]; + $mock_eps_details = [ + 'type' => 'eps', + ]; + $mock_sepa_details = [ + 'type' => 'sepa_debit', + ]; + $mock_ideal_details = [ + 'type' => 'ideal', + ]; + $mock_becs_details = [ + 'type' => 'au_becs_debit', + ]; + $mock_affirm_details = [ + 'type' => 'affirm', + ]; + $mock_afterpay_details = [ + 'type' => 'afterpay_clearpay', + ]; + + $this->set_cart_contains_subscription_items( false ); + $card_method = $this->mock_payment_methods['card']; + $giropay_method = $this->mock_payment_methods['giropay']; + $p24_method = $this->mock_payment_methods['p24']; + $sofort_method = $this->mock_payment_methods['sofort']; + $bancontact_method = $this->mock_payment_methods['bancontact']; + $eps_method = $this->mock_payment_methods['eps']; + $sepa_method = $this->mock_payment_methods['sepa_debit']; + $ideal_method = $this->mock_payment_methods['ideal']; + $becs_method = $this->mock_payment_methods['au_becs_debit']; + $affirm_method = $this->mock_payment_methods['affirm']; + $afterpay_method = $this->mock_payment_methods['afterpay_clearpay']; + + $this->assertEquals( 'card', $card_method->get_id() ); + $this->assertEquals( 'Credit card / debit card', $card_method->get_title( 'US' ) ); + $this->assertEquals( 'Visa debit card', $card_method->get_title( 'US', $mock_visa_details ) ); + $this->assertEquals( 'Mastercard credit card', $card_method->get_title( 'US', $mock_mastercard_details ) ); + $this->assertTrue( $card_method->is_enabled_at_checkout( 'US' ) ); + $this->assertTrue( $card_method->is_reusable() ); + $this->assertEquals( $mock_token, $card_method->get_payment_token_for_user( $mock_user, $mock_payment_method_id ) ); + + $this->assertEquals( 'giropay', $giropay_method->get_id() ); + $this->assertEquals( 'giropay', $giropay_method->get_title( 'US' ) ); + $this->assertEquals( 'giropay', $giropay_method->get_title( 'US', $mock_giropay_details ) ); + $this->assertTrue( $giropay_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $giropay_method->is_reusable() ); + + $this->assertEquals( 'p24', $p24_method->get_id() ); + $this->assertEquals( 'Przelewy24 (P24)', $p24_method->get_title( 'US' ) ); + $this->assertEquals( 'Przelewy24 (P24)', $p24_method->get_title( 'US', $mock_p24_details ) ); + $this->assertTrue( $p24_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $p24_method->is_reusable() ); + + $this->assertEquals( 'sofort', $sofort_method->get_id() ); + $this->assertEquals( 'Sofort', $sofort_method->get_title( 'US' ) ); + $this->assertEquals( 'Sofort', $sofort_method->get_title( 'US', $mock_sofort_details ) ); + $this->assertTrue( $sofort_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $sofort_method->is_reusable() ); + + $this->assertEquals( 'bancontact', $bancontact_method->get_id() ); + $this->assertEquals( 'Bancontact', $bancontact_method->get_title( 'US' ) ); + $this->assertEquals( 'Bancontact', $bancontact_method->get_title( 'US', $mock_bancontact_details ) ); + $this->assertTrue( $bancontact_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $bancontact_method->is_reusable() ); + + $this->assertEquals( 'eps', $eps_method->get_id() ); + $this->assertEquals( 'EPS', $eps_method->get_title( 'US' ) ); + $this->assertEquals( 'EPS', $eps_method->get_title( 'US', $mock_eps_details ) ); + $this->assertTrue( $eps_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $eps_method->is_reusable() ); + + $this->assertEquals( 'sepa_debit', $sepa_method->get_id() ); + $this->assertEquals( 'SEPA Direct Debit', $sepa_method->get_title( 'US' ) ); + $this->assertEquals( 'SEPA Direct Debit', $sepa_method->get_title( 'US', $mock_sepa_details ) ); + $this->assertTrue( $sepa_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $sepa_method->is_reusable() ); + + $this->assertEquals( 'ideal', $ideal_method->get_id() ); + $this->assertEquals( 'iDEAL', $ideal_method->get_title( 'US' ) ); + $this->assertEquals( 'iDEAL', $ideal_method->get_title( 'US', $mock_ideal_details ) ); + $this->assertTrue( $ideal_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $ideal_method->is_reusable() ); + + $this->assertEquals( 'au_becs_debit', $becs_method->get_id() ); + $this->assertEquals( 'BECS Direct Debit', $becs_method->get_title( 'US' ) ); + $this->assertEquals( 'BECS Direct Debit', $becs_method->get_title( 'US', $mock_becs_details ) ); + $this->assertTrue( $becs_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $becs_method->is_reusable() ); + + $this->assertSame( 'affirm', $affirm_method->get_id() ); + $this->assertSame( 'Affirm', $affirm_method->get_title( 'US' ) ); + $this->assertSame( 'Affirm', $affirm_method->get_title( 'US', $mock_affirm_details ) ); + $this->assertTrue( $affirm_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $affirm_method->is_reusable() ); + + $this->assertSame( 'afterpay_clearpay', $afterpay_method->get_id() ); + $this->assertSame( 'Afterpay', $afterpay_method->get_title( 'US' ) ); + $this->assertSame( 'Afterpay', $afterpay_method->get_title( 'US', $mock_afterpay_details ) ); + $this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $afterpay_method->is_reusable() ); + $this->assertSame( 'Clearpay', $afterpay_method->get_title( 'GB' ) ); + $this->assertSame( 'Clearpay', $afterpay_method->get_title( 'GB', $mock_afterpay_details ) ); + $this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'GB' ) ); + } + + public function test_only_reusabled_payment_methods_enabled_with_subscription_item_present() { + $this->set_cart_contains_subscription_items( true ); + + $card_method = $this->mock_payment_methods['card']; + $giropay_method = $this->mock_payment_methods['giropay']; + $sofort_method = $this->mock_payment_methods['sofort']; + $bancontact_method = $this->mock_payment_methods['bancontact']; + $eps_method = $this->mock_payment_methods['eps']; + $sepa_method = $this->mock_payment_methods['sepa_debit']; + $p24_method = $this->mock_payment_methods['p24']; + $ideal_method = $this->mock_payment_methods['ideal']; + $becs_method = $this->mock_payment_methods['au_becs_debit']; + $affirm_method = $this->mock_payment_methods['affirm']; + $afterpay_method = $this->mock_payment_methods['afterpay_clearpay']; + + $this->assertTrue( $card_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $giropay_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $sofort_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $bancontact_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $eps_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $sepa_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $p24_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $ideal_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $becs_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $affirm_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $afterpay_method->is_enabled_at_checkout( 'US' ) ); + } + + public function test_only_valid_payment_methods_returned_for_currency() { + $card_method = $this->mock_payment_methods['card']; + $giropay_method = $this->mock_payment_methods['giropay']; + $sofort_method = $this->mock_payment_methods['sofort']; + $bancontact_method = $this->mock_payment_methods['bancontact']; + $eps_method = $this->mock_payment_methods['eps']; + $sepa_method = $this->mock_payment_methods['sepa_debit']; + $p24_method = $this->mock_payment_methods['p24']; + $ideal_method = $this->mock_payment_methods['ideal']; + $becs_method = $this->mock_payment_methods['au_becs_debit']; + $affirm_method = $this->mock_payment_methods['affirm']; + $afterpay_method = $this->mock_payment_methods['afterpay_clearpay']; + + WC_Helper_Site_Currency::$mock_site_currency = 'EUR'; + + $account_domestic_currency = 'USD'; + $this->assertTrue( $card_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $giropay_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $sofort_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $bancontact_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $eps_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $sepa_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $p24_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $ideal_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $becs_method->is_currency_valid( $account_domestic_currency ) ); + // BNPLs can accept only domestic payments. + $this->assertFalse( $affirm_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $afterpay_method->is_currency_valid( $account_domestic_currency ) ); + + WC_Helper_Site_Currency::$mock_site_currency = 'USD'; + + $this->assertTrue( $card_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $giropay_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $sofort_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $bancontact_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $eps_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $sepa_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $p24_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $ideal_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $becs_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $affirm_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $afterpay_method->is_currency_valid( $account_domestic_currency ) ); + + WC_Helper_Site_Currency::$mock_site_currency = 'AUD'; + $this->assertTrue( $becs_method->is_currency_valid( $account_domestic_currency ) ); + + // BNPLs can accept only domestic payments. + WC_Helper_Site_Currency::$mock_site_currency = 'USD'; + $account_domestic_currency = 'CAD'; + $this->assertFalse( $affirm_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $afterpay_method->is_currency_valid( $account_domestic_currency ) ); + + WC_Helper_Site_Currency::$mock_site_currency = ''; + } + + public function test_payment_method_compares_correct_currency() { + $card_method = $this->mock_payment_methods['card']; + $giropay_method = $this->mock_payment_methods['giropay']; + $sofort_method = $this->mock_payment_methods['sofort']; + $bancontact_method = $this->mock_payment_methods['bancontact']; + $eps_method = $this->mock_payment_methods['eps']; + $sepa_method = $this->mock_payment_methods['sepa_debit']; + $p24_method = $this->mock_payment_methods['p24']; + $ideal_method = $this->mock_payment_methods['ideal']; + $becs_method = $this->mock_payment_methods['au_becs_debit']; + $affirm_method = $this->mock_payment_methods['affirm']; + $afterpay_method = $this->mock_payment_methods['afterpay_clearpay']; + + WC_Helper_Site_Currency::$mock_site_currency = 'EUR'; + $account_domestic_currency = 'USD'; + + $this->assertTrue( $card_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $giropay_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $sofort_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $bancontact_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $eps_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $sepa_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $p24_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $ideal_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $becs_method->is_currency_valid( $account_domestic_currency ) ); + + global $wp; + $order = WC_Helper_Order::create_order(); + $order_id = $order->get_id(); + $wp->query_vars = [ 'order-pay' => strval( $order_id ) ]; + $order->set_currency( 'USD' ); + + $this->assertTrue( $card_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $giropay_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $sofort_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $bancontact_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $eps_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $sepa_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $p24_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $ideal_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $becs_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $affirm_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $afterpay_method->is_currency_valid( $account_domestic_currency ) ); + + $wp->query_vars = []; + } + + public function test_create_token_from_setup_intent_adds_token() { + $mock_token = WC_Helper_Token::create_token( 'pm_mock' ); + $mock_setup_intent_id = 'si_mock'; + $mock_user = wp_get_current_user(); + + $request = $this->mock_wcpay_request( Get_Setup_Intention::class, 1, $mock_setup_intent_id ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( + WC_Helper_Intention::create_setup_intention( + [ + 'id' => $mock_setup_intent_id, + 'payment_method' => 'pm_mock', + ] + ) + ); + + $this->mock_token_service->expects( $this->once() ) + ->method( 'add_payment_method_to_user' ) + ->with( 'pm_mock', $mock_user ) + ->will( + $this->returnValue( $mock_token ) + ); + + $this->assertEquals( $mock_token, $this->mock_gateway->create_token_from_setup_intent( $mock_setup_intent_id, $mock_user ) ); + } + + public function test_exception_will_be_thrown_if_phone_number_is_invalid() { + $order = WC_Helper_Order::create_order(); + $order->set_billing_phone( '+1123456789123456789123' ); + $order->save(); + $this->expectException( Exception::class ); + $this->expectExceptionMessage( 'Invalid phone number.' ); + $this->mock_gateway->process_payment( $order->get_id() ); + } + + public function test_remove_link_payment_method_if_card_disabled() { + $this->mock_gateway->settings['upe_enabled_payment_method_ids'] = [ 'link' ]; + + $this->mock_gateway + ->expects( $this->once() ) + ->method( 'get_upe_enabled_payment_method_statuses' ) + ->will( + $this->returnValue( [ 'link_payments' => [ 'status' => 'active' ] ] ) + ); + + $this->assertSame( $this->mock_gateway->get_payment_method_ids_enabled_at_checkout(), [] ); + } + + /** + * @dataProvider available_payment_methods_provider + */ + public function test_get_upe_available_payment_methods( $payment_methods, $expected_result ) { + $mock_wcpay_account = $this->createMock( WC_Payments_Account::class ); + $mock_wcpay_account + ->expects( $this->any() ) + ->method( 'get_fees' ) + ->willReturn( $payment_methods ); + + $gateway = new WC_Payment_Gateway_WCPay( + $this->mock_api_client, + $mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + $this->mock_payment_method, + $this->mock_payment_methods, + $this->mock_rate_limiter, + $this->mock_order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service + ); + + $this->assertEquals( $expected_result, $gateway->get_upe_available_payment_methods() ); + } + + public function available_payment_methods_provider() { + return [ + 'card only' => [ + [ 'card' => [ 'base' => 0.1 ] ], + [ 'card' ], + ], + 'no match with fees' => [ + [ 'some_other_payment_method' => [ 'base' => 0.1 ] ], + [], + ], + 'multiple matches with fees' => [ + [ + 'card' => [ 'base' => 0.1 ], + 'bancontact' => [ 'base' => 0.2 ], + ], + [ 'card', 'bancontact' ], + ], + 'no fees no methods' => [ + [], + [], + ], + ]; + } + + /** + * Helper function to mock subscriptions for internal UPE payment methods. + */ + private function set_cart_contains_subscription_items( $cart_contains_subscriptions ) { + foreach ( $this->mock_payment_methods as $mock_payment_method ) { + $mock_payment_method->expects( $this->any() ) + ->method( 'is_subscription_item_in_cart' ) + ->will( + $this->returnValue( $cart_contains_subscriptions ) + ); + } + } + + private function setup_saved_payment_method() { + $token = WC_Helper_Token::create_token( 'pm_mock' ); + + return [ + 'payment_method' => WC_Payment_Gateway_WCPay::GATEWAY_ID, + 'wc-' . WC_Payment_Gateway_WCPay::GATEWAY_ID . '-payment-token' => (string) $token->get_id(), + ]; + } + + private function set_get_upe_enabled_payment_method_statuses_return_value( $return_value = null ) { + if ( null === $return_value ) { + $return_value = [ + 'card_payments' => [ + 'status' => 'active', + ], + ]; + } + $this->mock_gateway + ->expects( $this->any() ) + ->method( 'get_upe_enabled_payment_method_statuses' ) + ->will( $this->returnValue( $return_value ) ); + } +} diff --git a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php new file mode 100644 index 00000000000..1e3162dead3 --- /dev/null +++ b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php @@ -0,0 +1,1290 @@ + 'success', + 'payment_needed' => true, + 'redirect' => 'testURL/key=mock_order_key', + ]; + + /** + * WC_Payments_Localization_Service instance. + * + * @var WC_Payments_Localization_Service + */ + private $mock_localization_service; + + /** + * Mock Fraud Service. + * + * @var WC_Payments_Fraud_Service|MockObject + */ + private $mock_fraud_service; + + /** + * Mapping for payment ID to payment method. + * + * @var array + */ + private $payment_method_classes = [ + Payment_Method::CARD => CC_Payment_Method::class, + Payment_Method::GIROPAY => Giropay_Payment_Method::class, + Payment_Method::SOFORT => Sofort_Payment_Method::class, + Payment_Method::BANCONTACT => Bancontact_Payment_Method::class, + Payment_Method::EPS => EPS_Payment_Method::class, + Payment_Method::P24 => P24_Payment_Method::class, + Payment_Method::IDEAL => Ideal_Payment_Method::class, + Payment_Method::SEPA => Sepa_Payment_Method::class, + Payment_Method::BECS => Becs_Payment_Method::class, + Payment_Method::LINK => Link_Payment_Method::class, + ]; + + /** + * Pre-test setup + */ + public function set_up() { + parent::set_up(); + + $this->mock_payment_gateways = []; + $this->mock_payment_methods = []; + + // Mock the main class's cache service. + $this->_cache = WC_Payments::get_database_cache(); + $this->mock_cache = $this->createMock( Database_Cache::class ); + WC_Payments::set_database_cache( $this->mock_cache ); + + // Arrange: Mock WC_Payments_API_Client so we can configure the + // return value of create_and_confirm_intention(). + // Note that we cannot use createStub here since it's not defined in PHPUnit 6.5. + $this->mock_api_client = $this->getMockBuilder( 'WC_Payments_API_Client' ) + ->disableOriginalConstructor() + ->setMethods( + [ + 'create_intention', + 'create_setup_intention', + 'update_intention', + 'get_intent', + 'get_payment_method', + 'is_server_connected', + 'get_charge', + 'get_timeline', + ] + ) + ->getMock(); + + $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class ); + $this->mock_wcpay_account->method( 'get_account_country' )->willReturn( 'US' ); + $this->mock_wcpay_account->method( 'get_account_default_currency' )->willReturn( 'USD' ); + + $payment_methods = [ + 'link' => [ + 'base' => 0.1, + ], + ]; + + $this->mock_wcpay_account + ->expects( $this->any() ) + ->method( 'get_fees' ) + ->willReturn( $payment_methods ); + + $this->mock_woopay_utilities = $this->createMock( WooPay_Utilities::class ); + + // Arrange: Mock WC_Payments_Customer_Service so its methods aren't called directly. + $this->mock_customer_service = $this->getMockBuilder( 'WC_Payments_Customer_Service' ) + ->disableOriginalConstructor() + ->getMock(); + + // Arrange: Mock WC_Payments_Customer_Service so its methods aren't called directly. + $this->mock_token_service = $this->getMockBuilder( 'WC_Payments_Token_Service' ) + ->disableOriginalConstructor() + ->setMethods( [ 'add_payment_method_to_user' ] ) + ->getMock(); + + // Arrange: Mock WC_Payments_Action_Scheduler_Service so its methods aren't called directly. + $this->mock_action_scheduler_service = $this->getMockBuilder( 'WC_Payments_Action_Scheduler_Service' ) + ->disableOriginalConstructor() + ->getMock(); + + $this->mock_rate_limiter = $this->createMock( Session_Rate_Limiter::class ); + $this->order_service = new WC_Payments_Order_Service( $this->mock_api_client ); + + $this->mock_dpps = $this->createMock( Duplicate_Payment_Prevention_Service::class ); + + $this->mock_localization_service = $this->createMock( WC_Payments_Localization_Service::class ); + $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class ); + + // Arrange: Define a $_POST array which includes the payment method, + // so that get_payment_method_from_request() does not throw error. + $_POST = [ + 'wcpay-payment-method' => 'pm_mock', + 'payment_method' => WC_Payment_Gateway_WCPay::GATEWAY_ID, + ]; + + $get_payment_gateway_by_id_return_value_map = []; + + foreach ( $this->payment_method_classes as $payment_method_id => $payment_method_class ) { + $mock_payment_method = $this->getMockBuilder( $payment_method_class ) + ->setConstructorArgs( [ $this->mock_token_service ] ) + ->setMethods( [ 'is_subscription_item_in_cart', 'get_icon' ] ) + ->getMock(); + $this->mock_payment_methods[ $mock_payment_method->get_id() ] = $mock_payment_method; + + $mock_gateway = $this->getMockBuilder( WC_Payment_Gateway_WCPay::class ) + ->setConstructorArgs( + [ + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + $mock_payment_method, + $this->mock_payment_methods, + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service, + ] + ) + ->setMethods( + [ + 'get_return_url', + 'manage_customer_details_for_order', + 'parent_process_payment', + 'get_upe_enabled_payment_method_statuses', + 'is_payment_recurring', + 'get_payment_method_ids_enabled_at_checkout', + 'wc_payments_get_payment_gateway_by_id', + 'get_selected_payment_method', + 'get_upe_enabled_payment_method_ids', + ] + ) + ->getMock(); + + // Arrange: Set the return value of get_return_url() so it can be used in a test later. + $mock_gateway + ->expects( $this->any() ) + ->method( 'get_return_url' ) + ->will( + $this->returnValue( $this->return_url ) + ); + $mock_gateway + ->expects( $this->any() ) + ->method( 'parent_process_payment' ) + ->will( + $this->returnValue( $this->mock_payment_result ) + ); + + $this->mock_payment_gateways[ $payment_method_id ] = $mock_gateway; + + $get_payment_gateway_by_id_return_value_map[] = [ $payment_method_id, $mock_gateway ]; + + WC_Helper_Site_Currency::$mock_site_currency = ''; + } + + foreach ( $this->mock_payment_gateways as $id => $mock_gateway ) { + $mock_gateway->expects( $this->any() ) + ->method( 'wc_payments_get_payment_gateway_by_id' ) + ->will( + $this->returnValueMap( $get_payment_gateway_by_id_return_value_map ) + ); + } + + // Mock the level3 service to always return an empty array. + $mock_level3_service = $this->createMock( Level3Service::class ); + $mock_level3_service->expects( $this->any() ) + ->method( 'get_data_from_order' ) + ->willReturn( [] ); + wcpay_get_test_container()->replace( Level3Service::class, $mock_level3_service ); + + // Mock the order service to always return an empty array for meta. + $mock_order_service = $this->createMock( OrderService::class ); + $mock_order_service->expects( $this->any() ) + ->method( 'get_payment_metadata' ) + ->willReturn( [] ); + wcpay_get_test_container()->replace( OrderService::class, $mock_order_service ); + } + + /** + * Cleanup after tests. + * + * @return void + */ + public function tear_down() { + parent::tear_down(); + WC_Payments::set_database_cache( $this->_cache ); + wcpay_get_test_container()->reset_all_replacements(); + } + + /** + * Test the UI container that will hold the payment method. + * + * @return void + */ + public function test_display_gateway_html_for_multiple_gateways() { + foreach ( $this->mock_payment_gateways as $payment_method_id => $mock_payment_gateway ) { + /** + * This tests each payment method output separately without concatenating the output + * into 1 single buffer. Each iteration has 1 assertion. + */ + ob_start(); + $mock_payment_gateway->display_gateway_html(); + $actual_output = ob_get_contents(); + ob_end_clean(); + + $this->assertStringContainsString( '', $actual_output ); + } + } + + public function test_should_not_use_stripe_platform_on_checkout_page_for_upe() { + $payment_gateway = $this->mock_payment_gateways[ Payment_Method::SEPA ]; + $this->assertFalse( $payment_gateway->should_use_stripe_platform_on_checkout_page() ); + } + + public function test_link_payment_method_requires_mandate_data() { + $mock_upe_gateway = $this->mock_payment_gateways[ Payment_Method::CARD ]; + + $mock_upe_gateway + ->expects( $this->once() ) + ->method( 'get_upe_enabled_payment_method_ids' ) + ->will( + $this->returnValue( [ 'link' ] ) + ); + + $this->assertTrue( $mock_upe_gateway->is_mandate_data_required() ); + } + + public function test_sepa_debit_payment_method_requires_mandate_data() { + $mock_upe_gateway = $this->mock_payment_gateways[ Payment_Method::SEPA ]; + $this->assertTrue( $mock_upe_gateway->is_mandate_data_required() ); + } + + public function test_non_required_mandate_data() { + $mock_gateway_not_requiring_mandate_data = $this->mock_payment_gateways[ Payment_Method::GIROPAY ]; + $this->assertFalse( $mock_gateway_not_requiring_mandate_data->is_mandate_data_required() ); + } + + public function test_non_reusable_payment_method_is_not_available_when_subscription_is_in_cart() { + $non_reusable_payment_method = Payment_Method::BANCONTACT; + $payment_gateway = $this->mock_payment_gateways[ $non_reusable_payment_method ]; + + $this->set_cart_contains_subscription_items( true ); + + $this->assertFalse( $payment_gateway->is_available() ); + } + + public function test_process_payment_returns_correct_redirect_when_using_saved_payment() { + $mock_card_payment_gateway = $this->mock_payment_gateways[ Payment_Method::CARD ]; + $user = wp_get_current_user(); + $customer_id = 'cus_mock'; + + $order = WC_Helper_Order::create_order(); + $_POST = $this->setup_saved_payment_method(); + $mock_card_payment_gateway->expects( $this->once() ) + ->method( 'manage_customer_details_for_order' ) + ->will( + $this->returnValue( [ $user, $customer_id ] ) + ); + $mock_card_payment_gateway->expects( $this->any() ) + ->method( 'get_upe_enabled_payment_method_ids' ) + ->will( + $this->returnValue( [ Payment_Method::CARD ] ) + ); + $this->mock_wcpay_request( Create_And_Confirm_Intention::class, 1 ) + ->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( + WC_Helper_Intention::create_intention( [ 'status' => Intent_Status::PROCESSING ] ) + ); + + $this->set_cart_contains_subscription_items( false ); + + $result = $mock_card_payment_gateway->process_payment( $order->get_id() ); + + $this->assertEquals( 'success', $result['result'] ); + $this->assertEquals( $this->return_url, $result['redirect'] ); + } + + public function test_upe_process_payment_check_session_order_redirect_to_previous_order() { + $_POST['wc_payment_intent_id'] = 'pi_mock'; + $mock_upe_gateway = $this->mock_payment_gateways[ Payment_Method::SEPA ]; + + $response = [ + 'dummy_result' => 'xyz', + ]; + + // Arrange the order is being processed. + $order = WC_Helper_Order::create_order(); + $order_id = $order->get_id(); + + // Arrange the DPPs to return a redirect. + $this->mock_dpps->expects( $this->once() ) + ->method( 'check_against_session_processing_order' ) + ->with( wc_get_order( $order ) ) + ->willReturn( $response ); + + // Act: process the order but redirect to the previous/session paid order. + $result = $mock_upe_gateway->process_payment( $order_id ); + + // Assert: the result of check_against_session_processing_order. + $this->assertSame( $response, $result ); + } + + public function test_process_redirect_payment_intent_processing() { + + $mock_upe_gateway = $this->mock_payment_gateways[ Payment_Method::CARD ]; + $order = WC_Helper_Order::create_order(); + + $order_id = $order->get_id(); + $save_payment_method = false; + $user = wp_get_current_user(); + $intent_status = Intent_Status::PROCESSING; + $intent_metadata = [ 'order_id' => (string) $order_id ]; + $charge_id = 'ch_mock'; + $customer_id = 'cus_mock'; + $intent_id = 'pi_mock'; + $payment_method_id = 'pm_mock'; + + // Supply the order with the intent id so that it can be retrieved during the redirect payment processing. + $order->update_meta_data( '_intent_id', $intent_id ); + $order->save(); + + $card_method = $this->mock_payment_methods['card']; + + $payment_intent = WC_Helper_Intention::create_intention( + [ + 'status' => $intent_status, + 'metadata' => $intent_metadata, + ] + ); + + $mock_upe_gateway->expects( $this->once() ) + ->method( 'manage_customer_details_for_order' ) + ->will( + $this->returnValue( [ $user, $customer_id ] ) + ); + + $mock_upe_gateway->expects( $this->any() ) + ->method( 'get_selected_payment_method' ) + ->willReturn( $card_method ); + + $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id ) + ->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $payment_intent ); + + $this->set_cart_contains_subscription_items( false ); + + $mock_upe_gateway->process_redirect_payment( $order, $intent_id, $save_payment_method ); + + $result_order = wc_get_order( $order_id ); + $note = wc_get_order_notes( + [ + 'order_id' => $order_id, + 'limit' => 1, + ] + )[0]; + + $this->assertStringContainsString( 'authorized', $note->content ); + $this->assertEquals( $intent_id, $result_order->get_meta( '_intent_id', true ) ); + $this->assertEquals( $charge_id, $result_order->get_meta( '_charge_id', true ) ); + $this->assertEquals( $intent_status, $result_order->get_meta( '_intention_status', true ) ); + $this->assertEquals( $payment_method_id, $result_order->get_meta( '_payment_method_id', true ) ); + $this->assertEquals( $customer_id, $result_order->get_meta( '_stripe_customer_id', true ) ); + $this->assertEquals( Order_Status::ON_HOLD, $result_order->get_status() ); + } + + public function test_process_redirect_payment_intent_succeded() { + + $mock_upe_gateway = $this->mock_payment_gateways[ Payment_Method::CARD ]; + $order = WC_Helper_Order::create_order(); + + $order_id = $order->get_id(); + $save_payment_method = false; + $user = wp_get_current_user(); + $intent_status = Intent_Status::SUCCEEDED; + $intent_metadata = [ 'order_id' => (string) $order_id ]; + $charge_id = 'ch_mock'; + $customer_id = 'cus_mock'; + $intent_id = 'pi_mock'; + $payment_method_id = 'pm_mock'; + + // Supply the order with the intent id so that it can be retrieved during the redirect payment processing. + $order->update_meta_data( '_intent_id', $intent_id ); + $order->save(); + + $card_method = $this->mock_payment_methods['card']; + + $payment_intent = WC_Helper_Intention::create_intention( + [ + 'status' => $intent_status, + 'metadata' => $intent_metadata, + ] + ); + + $mock_upe_gateway->expects( $this->once() ) + ->method( 'manage_customer_details_for_order' ) + ->will( + $this->returnValue( [ $user, $customer_id ] ) + ); + + $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id ) + ->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $payment_intent ); + + $mock_upe_gateway->expects( $this->any() ) + ->method( 'get_selected_payment_method' ) + ->willReturn( $card_method ); + + $this->set_cart_contains_subscription_items( false ); + + $mock_upe_gateway->process_redirect_payment( $order, $intent_id, $save_payment_method ); + + $result_order = wc_get_order( $order_id ); + + $this->assertEquals( $intent_id, $result_order->get_meta( '_intent_id', true ) ); + $this->assertEquals( $charge_id, $result_order->get_meta( '_charge_id', true ) ); + $this->assertEquals( $intent_status, $result_order->get_meta( '_intention_status', true ) ); + $this->assertEquals( $payment_method_id, $result_order->get_meta( '_payment_method_id', true ) ); + $this->assertEquals( $customer_id, $result_order->get_meta( '_stripe_customer_id', true ) ); + $this->assertEquals( Order_Status::PROCESSING, $result_order->get_status() ); + } + + public function is_proper_intent_used_with_order_returns_false() { + $this->assertFalse( $this->mock_upe_gateway->is_proper_intent_used_with_order( WC_Helper_Order::create_order(), 'wrong_intent_id' ) ); + } + + public function test_process_redirect_setup_intent_succeded() { + + $order = WC_Helper_Order::create_order(); + $mock_upe_gateway = $this->mock_payment_gateways[ Payment_Method::CARD ]; + + $order_id = $order->get_id(); + $save_payment_method = true; + $user = wp_get_current_user(); + $intent_status = Intent_Status::SUCCEEDED; + $client_secret = 'cs_mock'; + $customer_id = 'cus_mock'; + $intent_id = 'si_mock'; + $payment_method_id = 'pm_mock'; + $token = WC_Helper_Token::create_token( $payment_method_id ); + + // Supply the order with the intent id so that it can be retrieved during the redirect payment processing. + $order->update_meta_data( '_intent_id', $intent_id ); + $order->save(); + + $card_method = $this->mock_payment_methods['card']; + + $order->set_shipping_total( 0 ); + $order->set_shipping_tax( 0 ); + $order->set_cart_tax( 0 ); + $order->set_total( 0 ); + $order->save(); + + $setup_intent = WC_Helper_Intention::create_setup_intention( + [ + 'id' => $intent_id, + 'client_secret' => $client_secret, + 'status' => $intent_status, + 'payment_method' => $payment_method_id, + 'payment_method_options' => [ + 'card' => [ + 'request_three_d_secure' => 'automatic', + ], + ], + 'last_setup_error' => [], + ] + ); + + $mock_upe_gateway->expects( $this->once() ) + ->method( 'manage_customer_details_for_order' ) + ->will( + $this->returnValue( [ $user, $customer_id ] ) + ); + + $request = $this->mock_wcpay_request( Get_Setup_Intention::class, 1, $intent_id ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $setup_intent ); + + $this->mock_token_service->expects( $this->once() ) + ->method( 'add_payment_method_to_user' ) + ->will( + $this->returnValue( $token ) + ); + + $mock_upe_gateway->expects( $this->any() ) + ->method( 'get_selected_payment_method' ) + ->willReturn( $card_method ); + + $this->set_cart_contains_subscription_items( true ); + + $mock_upe_gateway->process_redirect_payment( $order, $intent_id, $save_payment_method ); + + $result_order = wc_get_order( $order_id ); + + $this->assertEquals( $intent_id, $result_order->get_meta( '_intent_id', true ) ); + $this->assertEquals( $intent_status, $result_order->get_meta( '_intention_status', true ) ); + $this->assertEquals( $payment_method_id, $result_order->get_meta( '_payment_method_id', true ) ); + $this->assertEquals( $customer_id, $result_order->get_meta( '_stripe_customer_id', true ) ); + $this->assertEquals( Order_Status::PROCESSING, $result_order->get_status() ); + $this->assertEquals( 1, count( $result_order->get_payment_tokens() ) ); + } + + public function test_process_redirect_payment_save_payment_token() { + + $mock_upe_gateway = $this->mock_payment_gateways[ Payment_Method::CARD ]; + + $order = WC_Helper_Order::create_order(); + $order_id = $order->get_id(); + $save_payment_method = true; + $user = wp_get_current_user(); + $intent_status = Intent_Status::PROCESSING; + $intent_metadata = [ 'order_id' => (string) $order_id ]; + $charge_id = 'ch_mock'; + $customer_id = 'cus_mock'; + $intent_id = 'pi_mock'; + $payment_method_id = 'pm_mock'; + $token = WC_Helper_Token::create_token( $payment_method_id ); + + // Supply the order with the intent id so that it can be retrieved during the redirect payment processing. + $order->update_meta_data( '_intent_id', $intent_id ); + $order->save(); + + $card_method = $this->mock_payment_methods['card']; + + $payment_intent = WC_Helper_Intention::create_intention( + [ + 'status' => $intent_status, + 'metadata' => $intent_metadata, + ] + ); + + $mock_upe_gateway->expects( $this->once() ) + ->method( 'manage_customer_details_for_order' ) + ->will( + $this->returnValue( [ $user, $customer_id ] ) + ); + + $this->mock_wcpay_request( Get_Intention::class, 1, $intent_id ) + ->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $payment_intent ); + + $this->mock_token_service->expects( $this->once() ) + ->method( 'add_payment_method_to_user' ) + ->will( + $this->returnValue( $token ) + ); + + $mock_upe_gateway->expects( $this->any() ) + ->method( 'get_selected_payment_method' ) + ->willReturn( $card_method ); + + $this->set_cart_contains_subscription_items( false ); + + $mock_upe_gateway->process_redirect_payment( $order, $intent_id, $save_payment_method ); + + $result_order = wc_get_order( $order_id ); + $note = wc_get_order_notes( + [ + 'order_id' => $order_id, + 'limit' => 1, + ] + )[0]; + + $this->assertStringContainsString( 'authorized', $note->content ); + $this->assertEquals( $intent_id, $result_order->get_meta( '_intent_id', true ) ); + $this->assertEquals( $charge_id, $result_order->get_meta( '_charge_id', true ) ); + $this->assertEquals( $intent_status, $result_order->get_meta( '_intention_status', true ) ); + $this->assertEquals( $payment_method_id, $result_order->get_meta( '_payment_method_id', true ) ); + $this->assertEquals( $customer_id, $result_order->get_meta( '_stripe_customer_id', true ) ); + $this->assertEquals( Order_Status::ON_HOLD, $result_order->get_status() ); + $this->assertEquals( 1, count( $result_order->get_payment_tokens() ) ); + } + + public function test_correct_payment_method_title_for_order() { + $order = WC_Helper_Order::create_order(); + + $visa_credit_details = [ + 'type' => 'card', + 'card' => [ + 'network' => 'visa', + 'funding' => 'credit', + ], + ]; + $visa_debit_details = [ + 'type' => 'card', + 'card' => [ + 'network' => 'visa', + 'funding' => 'debit', + ], + ]; + $mastercard_credit_details = [ + 'type' => 'card', + 'card' => [ + 'network' => 'mastercard', + 'funding' => 'credit', + ], + ]; + $eps_details = [ + 'type' => 'eps', + ]; + $giropay_details = [ + 'type' => 'giropay', + ]; + $p24_details = [ + 'type' => 'p24', + ]; + $sofort_details = [ + 'type' => 'sofort', + ]; + $bancontact_details = [ + 'type' => 'bancontact', + ]; + $sepa_details = [ + 'type' => 'sepa_debit', + ]; + $ideal_details = [ + 'type' => 'ideal', + ]; + $becs_details = [ + 'type' => 'au_becs_debit', + ]; + + $charge_payment_method_details = [ + $visa_credit_details, + $visa_debit_details, + $mastercard_credit_details, + $giropay_details, + $sofort_details, + $bancontact_details, + $eps_details, + $p24_details, + $ideal_details, + $sepa_details, + $becs_details, + ]; + + $expected_payment_method_titles = [ + 'Visa credit card', + 'Visa debit card', + 'Mastercard credit card', + 'giropay', + 'Sofort', + 'Bancontact', + 'EPS', + 'Przelewy24 (P24)', + 'iDEAL', + 'SEPA Direct Debit', + 'BECS Direct Debit', + ]; + + foreach ( $charge_payment_method_details as $i => $payment_method_details ) { + $payment_method_id = $payment_method_details['type']; + $mock_upe_gateway = $this->mock_payment_gateways[ $payment_method_id ]; + $payment_method = $this->mock_payment_methods[ $payment_method_id ]; + $mock_upe_gateway->expects( $this->any() ) + ->method( 'get_selected_payment_method' ) + ->willReturn( $payment_method ); + $mock_upe_gateway->set_payment_method_title_for_order( $order, $payment_method_id, $payment_method_details ); + $this->assertEquals( $expected_payment_method_titles[ $i ], $order->get_payment_method_title() ); + } + } + + public function test_payment_methods_show_correct_default_outputs() { + $mock_token = WC_Helper_Token::create_token( 'pm_mock' ); + $this->mock_token_service->expects( $this->any() ) + ->method( 'add_payment_method_to_user' ) + ->will( + $this->returnValue( $mock_token ) + ); + + $mock_user = 'mock_user'; + $mock_payment_method_id = 'pm_mock'; + + $mock_visa_details = [ + 'type' => 'card', + 'card' => [ + 'network' => 'visa', + 'funding' => 'debit', + ], + ]; + $mock_mastercard_details = [ + 'type' => 'card', + 'card' => [ + 'network' => 'mastercard', + 'funding' => 'credit', + ], + ]; + $mock_giropay_details = [ + 'type' => 'giropay', + ]; + $mock_p24_details = [ + 'type' => 'p24', + ]; + $mock_sofort_details = [ + 'type' => 'sofort', + ]; + $mock_bancontact_details = [ + 'type' => 'bancontact', + ]; + $mock_eps_details = [ + 'type' => 'eps', + ]; + $mock_sepa_details = [ + 'type' => 'sepa_debit', + ]; + $mock_ideal_details = [ + 'type' => 'ideal', + ]; + $mock_becs_details = [ + 'type' => 'au_becs_debit', + ]; + + $this->set_cart_contains_subscription_items( false ); + $card_method = $this->mock_payment_methods['card']; + $giropay_method = $this->mock_payment_methods['giropay']; + $p24_method = $this->mock_payment_methods['p24']; + $sofort_method = $this->mock_payment_methods['sofort']; + $bancontact_method = $this->mock_payment_methods['bancontact']; + $eps_method = $this->mock_payment_methods['eps']; + $sepa_method = $this->mock_payment_methods['sepa_debit']; + $ideal_method = $this->mock_payment_methods['ideal']; + $becs_method = $this->mock_payment_methods['au_becs_debit']; + + $this->assertEquals( 'card', $card_method->get_id() ); + $this->assertEquals( 'Credit card / debit card', $card_method->get_title( 'US' ) ); + $this->assertEquals( 'Visa debit card', $card_method->get_title( 'US', $mock_visa_details ) ); + $this->assertEquals( 'Mastercard credit card', $card_method->get_title( 'US', $mock_mastercard_details ) ); + $this->assertTrue( $card_method->is_enabled_at_checkout( 'US' ) ); + $this->assertTrue( $card_method->is_reusable() ); + $this->assertEquals( $mock_token, $card_method->get_payment_token_for_user( $mock_user, $mock_payment_method_id ) ); + + $this->assertEquals( 'giropay', $giropay_method->get_id() ); + $this->assertEquals( 'giropay', $giropay_method->get_title( 'US' ) ); + $this->assertEquals( 'giropay', $giropay_method->get_title( 'US', $mock_giropay_details ) ); + $this->assertTrue( $giropay_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $giropay_method->is_reusable() ); + + $this->assertEquals( 'p24', $p24_method->get_id() ); + $this->assertEquals( 'Przelewy24 (P24)', $p24_method->get_title( 'US' ) ); + $this->assertEquals( 'Przelewy24 (P24)', $p24_method->get_title( 'US', $mock_p24_details ) ); + $this->assertTrue( $p24_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $p24_method->is_reusable() ); + + $this->assertEquals( 'sofort', $sofort_method->get_id() ); + $this->assertEquals( 'Sofort', $sofort_method->get_title( 'US' ) ); + $this->assertEquals( 'Sofort', $sofort_method->get_title( 'US', $mock_sofort_details ) ); + $this->assertTrue( $sofort_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $sofort_method->is_reusable() ); + + $this->assertEquals( 'bancontact', $bancontact_method->get_id() ); + $this->assertEquals( 'Bancontact', $bancontact_method->get_title( 'US' ) ); + $this->assertEquals( 'Bancontact', $bancontact_method->get_title( 'US', $mock_bancontact_details ) ); + $this->assertTrue( $bancontact_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $bancontact_method->is_reusable() ); + + $this->assertEquals( 'eps', $eps_method->get_id() ); + $this->assertEquals( 'EPS', $eps_method->get_title( 'US' ) ); + $this->assertEquals( 'EPS', $eps_method->get_title( 'US', $mock_eps_details ) ); + $this->assertTrue( $eps_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $eps_method->is_reusable() ); + + $this->assertEquals( 'sepa_debit', $sepa_method->get_id() ); + $this->assertEquals( 'SEPA Direct Debit', $sepa_method->get_title( 'US' ) ); + $this->assertEquals( 'SEPA Direct Debit', $sepa_method->get_title( 'US', $mock_sepa_details ) ); + $this->assertTrue( $sepa_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $sepa_method->is_reusable() ); + + $this->assertEquals( 'ideal', $ideal_method->get_id() ); + $this->assertEquals( 'iDEAL', $ideal_method->get_title( 'US' ) ); + $this->assertEquals( 'iDEAL', $ideal_method->get_title( 'US', $mock_ideal_details ) ); + $this->assertTrue( $ideal_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $ideal_method->is_reusable() ); + + $this->assertEquals( 'au_becs_debit', $becs_method->get_id() ); + $this->assertEquals( 'BECS Direct Debit', $becs_method->get_title( 'US' ) ); + $this->assertEquals( 'BECS Direct Debit', $becs_method->get_title( 'US', $mock_becs_details ) ); + $this->assertTrue( $becs_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $becs_method->is_reusable() ); + } + + public function test_only_reusabled_payment_methods_enabled_with_subscription_item_present() { + // Setup $this->mock_payment_methods. + + $this->set_cart_contains_subscription_items( true ); + + $card_method = $this->mock_payment_methods['card']; + $giropay_method = $this->mock_payment_methods['giropay']; + $sofort_method = $this->mock_payment_methods['sofort']; + $bancontact_method = $this->mock_payment_methods['bancontact']; + $eps_method = $this->mock_payment_methods['eps']; + $sepa_method = $this->mock_payment_methods['sepa_debit']; + $p24_method = $this->mock_payment_methods['p24']; + $ideal_method = $this->mock_payment_methods['ideal']; + $becs_method = $this->mock_payment_methods['au_becs_debit']; + + $this->assertTrue( $card_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $giropay_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $sofort_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $bancontact_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $eps_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $sepa_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $p24_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $ideal_method->is_enabled_at_checkout( 'US' ) ); + $this->assertFalse( $becs_method->is_enabled_at_checkout( 'US' ) ); + } + + public function test_only_valid_payment_methods_returned_for_currency() { + // Setup $this->mock_payment_methods. + + $card_method = $this->mock_payment_methods['card']; + $giropay_method = $this->mock_payment_methods['giropay']; + $sofort_method = $this->mock_payment_methods['sofort']; + $bancontact_method = $this->mock_payment_methods['bancontact']; + $eps_method = $this->mock_payment_methods['eps']; + $sepa_method = $this->mock_payment_methods['sepa_debit']; + $p24_method = $this->mock_payment_methods['p24']; + $ideal_method = $this->mock_payment_methods['ideal']; + $becs_method = $this->mock_payment_methods['au_becs_debit']; + + WC_Helper_Site_Currency::$mock_site_currency = 'EUR'; + $account_domestic_currency = 'USD'; + + $this->assertTrue( $card_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $giropay_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $sofort_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $bancontact_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $eps_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $sepa_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $p24_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertTrue( $ideal_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $becs_method->is_currency_valid( $account_domestic_currency ) ); + + WC_Helper_Site_Currency::$mock_site_currency = 'USD'; + + $this->assertTrue( $card_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $giropay_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $sofort_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $bancontact_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $eps_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $sepa_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $p24_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $ideal_method->is_currency_valid( $account_domestic_currency ) ); + $this->assertFalse( $becs_method->is_currency_valid( $account_domestic_currency ) ); + + WC_Helper_Site_Currency::$mock_site_currency = 'AUD'; + $this->assertTrue( $becs_method->is_currency_valid( $account_domestic_currency ) ); + + WC_Helper_Site_Currency::$mock_site_currency = ''; + } + + public function test_create_token_from_setup_intent_adds_token() { + + $mock_token = WC_Helper_Token::create_token( 'pm_mock' ); + $mock_setup_intent_id = 'si_mock'; + $mock_user = wp_get_current_user(); + + $this->mock_token_service + ->method( 'add_payment_method_to_user' ) + ->with( 'pm_mock', $mock_user ) + ->will( + $this->returnValue( $mock_token ) + ); + + foreach ( $this->mock_payment_gateways as $mock_upe_gateway ) { + $request = $this->mock_wcpay_request( Get_Setup_Intention::class, 1, $mock_setup_intent_id ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( + WC_Helper_Intention::create_setup_intention( + [ + 'id' => $mock_setup_intent_id, + 'payment_method' => 'pm_mock', + ] + ) + ); + $this->assertEquals( $mock_token, $mock_upe_gateway->create_token_from_setup_intent( $mock_setup_intent_id, $mock_user ) ); + } + } + + /** + * Test get_payment_method_types with regular checkout post request context. + * + * @return void + */ + public function test_get_payment_methods_with_request_context() { + $mock_upe_gateway = $this->getMockBuilder( WC_Payment_Gateway_WCPay::class ) + ->setConstructorArgs( + [ + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + $this->mock_payment_methods[ Payment_Method::CARD ], + $this->mock_payment_methods, + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service, + ] + ) + ->setMethods( [ 'get_payment_methods_from_gateway_id' ] ) + ->getMock(); + + $order = WC_Helper_Order::create_order(); + $payment_information = new Payment_Information( 'pm_mock', $order ); + + $_POST['payment_method'] = 'woocommerce_payments'; + + $mock_upe_gateway->expects( $this->once() ) + ->method( 'get_payment_methods_from_gateway_id' ) + ->with( 'woocommerce_payments' ) + ->will( + $this->returnValue( [ Payment_Method::CARD ] ) + ); + + $payment_methods = $mock_upe_gateway->get_payment_method_types( $payment_information ); + + $this->assertSame( [ Payment_Method::CARD ], $payment_methods ); + + unset( $_POST['payment_method'] ); // phpcs:ignore WordPress.Security.NonceVerification + } + + /** + * Test get_payment_method_types without post request context. + * + * @return void + */ + public function test_get_payment_methods_without_request_context() { + $mock_upe_gateway = $this->getMockBuilder( WC_Payment_Gateway_WCPay::class ) + ->setConstructorArgs( + [ + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + $this->mock_payment_methods[ Payment_Method::CARD ], + $this->mock_payment_methods, + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service, + ] + ) + ->setMethods( [ 'get_payment_methods_from_gateway_id' ] ) + ->getMock(); + + $token = WC_Helper_Token::create_token( 'pm_mock' ); + $order = WC_Helper_Order::create_order(); + $payment_information = new Payment_Information( 'pm_mock', $order, null, $token ); + + unset( $_POST['payment_method'] ); // phpcs:ignore WordPress.Security.NonceVerification + + $mock_upe_gateway->expects( $this->once() ) + ->method( 'get_payment_methods_from_gateway_id' ) + ->with( $token->get_gateway_id(), $order->get_id() ) + ->will( + $this->returnValue( [ Payment_Method::CARD ] ) + ); + + $payment_methods = $mock_upe_gateway->get_payment_method_types( $payment_information ); + + $this->assertSame( [ Payment_Method::CARD ], $payment_methods ); + } + + /** + * Test get_payment_method_types without post request context or saved token. + * + * @return void + */ + public function test_get_payment_methods_without_request_context_or_token() { + $mock_upe_gateway = $this->getMockBuilder( WC_Payment_Gateway_WCPay::class ) + ->setConstructorArgs( + [ + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + $this->mock_payment_methods[ Payment_Method::CARD ], + $this->mock_payment_methods, + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service, + ] + ) + ->setMethods( + [ + 'get_payment_methods_from_gateway_id', + 'get_payment_method_ids_enabled_at_checkout', + ] + ) + ->getMock(); + + $payment_information = new Payment_Information( 'pm_mock' ); + + unset( $_POST['payment_method'] ); // phpcs:ignore WordPress.Security.NonceVerification + + $gateway = WC_Payments::get_gateway(); + WC_Payments::set_gateway( $mock_upe_gateway ); + + $mock_upe_gateway->expects( $this->never() ) + ->method( 'get_payment_methods_from_gateway_id' ); + + $mock_upe_gateway->expects( $this->once() ) + ->method( 'get_payment_method_ids_enabled_at_checkout' ) + ->willReturn( [ Payment_Method::CARD ] ); + + $payment_methods = $mock_upe_gateway->get_payment_method_types( $payment_information ); + + $this->assertSame( [ Payment_Method::CARD ], $payment_methods ); + + WC_Payments::set_gateway( $gateway ); + } + + /** + * Test get_payment_methods_from_gateway_id function with UPE enabled. + * + * @return void + */ + public function test_get_payment_methods_from_gateway_id_upe() { + WC_Helper_Order::create_order(); + $mock_upe_gateway = $this->getMockBuilder( WC_Payment_Gateway_WCPay::class ) + ->setConstructorArgs( + [ + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + $this->mock_payment_methods[ Payment_Method::CARD ], + $this->mock_payment_methods, + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service, + ] + ) + ->onlyMethods( + [ + 'get_upe_enabled_payment_method_ids', + 'get_payment_method_ids_enabled_at_checkout', + ] + ) + ->getMock(); + + $gateway = WC_Payments::get_gateway(); + WC_Payments::set_gateway( $mock_upe_gateway ); + + $mock_upe_gateway->expects( $this->any() ) + ->method( 'get_upe_enabled_payment_method_ids' ) + ->will( + $this->returnValue( [ Payment_Method::CARD, Payment_Method::LINK ] ) + ); + + $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( WC_Payment_Gateway_WCPay::GATEWAY_ID . '_' . Payment_Method::BANCONTACT ); + $this->assertSame( [ Payment_Method::BANCONTACT ], $payment_methods ); + + $mock_upe_gateway->expects( $this->any() ) + ->method( 'get_payment_method_ids_enabled_at_checkout' ) + ->will( + $this->onConsecutiveCalls( + [ Payment_Method::CARD, Payment_Method::LINK ], + [ Payment_Method::CARD ] + ) + ); + + $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( WC_Payment_Gateway_WCPay::GATEWAY_ID ); + $this->assertSame( [ Payment_Method::CARD, Payment_Method::LINK ], $payment_methods ); + + $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( WC_Payment_Gateway_WCPay::GATEWAY_ID ); + $this->assertSame( [ Payment_Method::CARD ], $payment_methods ); + + WC_Payments::set_gateway( $gateway ); + } + + /** + * Helper function to mock subscriptions for internal UPE payment methods. + */ + private function set_cart_contains_subscription_items( $cart_contains_subscriptions ) { + foreach ( $this->mock_payment_methods as $mock_payment_method ) { + $mock_payment_method->expects( $this->any() ) + ->method( 'is_subscription_item_in_cart' ) + ->will( + $this->returnValue( $cart_contains_subscriptions ) + ); + } + } + + private function setup_saved_payment_method() { + $token = WC_Helper_Token::create_token( 'pm_mock' ); + + return [ + 'payment_method' => WC_Payment_Gateway_WCPay::GATEWAY_ID, + 'wc-' . WC_Payment_Gateway_WCPay::GATEWAY_ID . '-payment-token' => (string) $token->get_id(), + ]; + } + + private function set_get_upe_enabled_payment_method_statuses_return_value( $mock_payment_gateway, $return_value = null ) { + if ( null === $return_value ) { + $return_value = [ + 'card_payments' => [ + 'status' => 'active', + ], + ]; + } + $mock_payment_gateway + ->expects( $this->any() ) + ->method( 'get_upe_enabled_payment_method_statuses' ) + ->will( $this->returnValue( $return_value ) ); + } +} diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index 62dd5c2f83a..f59ec8e6746 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -573,71 +573,77 @@ public function test_payment_methods_show_correct_default_outputs() { $this->assertEquals( 'card', $card_method->get_id() ); $this->assertEquals( 'Credit card / debit card', $card_method->get_title() ); - $this->assertEquals( 'Visa debit card', $card_method->get_title( $mock_visa_details ) ); - $this->assertEquals( 'Mastercard credit card', $card_method->get_title( $mock_mastercard_details ) ); + $this->assertEquals( 'Visa debit card', $card_method->get_title( 'US', $mock_visa_details ) ); + $this->assertEquals( 'Mastercard credit card', $card_method->get_title( 'US', $mock_mastercard_details ) ); $this->assertTrue( $card_method->is_enabled_at_checkout( 'US' ) ); $this->assertTrue( $card_method->is_reusable() ); $this->assertEquals( $mock_token, $card_method->get_payment_token_for_user( $mock_user, $mock_payment_method_id ) ); $this->assertEquals( 'giropay', $giropay_method->get_id() ); $this->assertEquals( 'giropay', $giropay_method->get_title() ); - $this->assertEquals( 'giropay', $giropay_method->get_title( $mock_giropay_details ) ); + $this->assertEquals( 'giropay', $giropay_method->get_title( 'US', $mock_giropay_details ) ); $this->assertTrue( $giropay_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $giropay_method->is_reusable() ); $this->assertEquals( 'p24', $p24_method->get_id() ); $this->assertEquals( 'Przelewy24 (P24)', $p24_method->get_title() ); - $this->assertEquals( 'Przelewy24 (P24)', $p24_method->get_title( $mock_p24_details ) ); + $this->assertEquals( 'Przelewy24 (P24)', $p24_method->get_title( 'US', $mock_p24_details ) ); $this->assertTrue( $p24_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $p24_method->is_reusable() ); $this->assertEquals( 'sofort', $sofort_method->get_id() ); $this->assertEquals( 'Sofort', $sofort_method->get_title() ); - $this->assertEquals( 'Sofort', $sofort_method->get_title( $mock_sofort_details ) ); + $this->assertEquals( 'Sofort', $sofort_method->get_title( 'US', $mock_sofort_details ) ); $this->assertTrue( $sofort_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $sofort_method->is_reusable() ); $this->assertEquals( 'bancontact', $bancontact_method->get_id() ); $this->assertEquals( 'Bancontact', $bancontact_method->get_title() ); - $this->assertEquals( 'Bancontact', $bancontact_method->get_title( $mock_bancontact_details ) ); + $this->assertEquals( 'Bancontact', $bancontact_method->get_title( 'US', $mock_bancontact_details ) ); $this->assertTrue( $bancontact_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $bancontact_method->is_reusable() ); $this->assertEquals( 'eps', $eps_method->get_id() ); $this->assertEquals( 'EPS', $eps_method->get_title() ); - $this->assertEquals( 'EPS', $eps_method->get_title( $mock_eps_details ) ); + $this->assertEquals( 'EPS', $eps_method->get_title( 'US', $mock_eps_details ) ); $this->assertTrue( $eps_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $eps_method->is_reusable() ); $this->assertEquals( 'sepa_debit', $sepa_method->get_id() ); $this->assertEquals( 'SEPA Direct Debit', $sepa_method->get_title() ); - $this->assertEquals( 'SEPA Direct Debit', $sepa_method->get_title( $mock_sepa_details ) ); + $this->assertEquals( 'SEPA Direct Debit', $sepa_method->get_title( 'US', $mock_sepa_details ) ); $this->assertTrue( $sepa_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $sepa_method->is_reusable() ); $this->assertEquals( 'ideal', $ideal_method->get_id() ); $this->assertEquals( 'iDEAL', $ideal_method->get_title() ); - $this->assertEquals( 'iDEAL', $ideal_method->get_title( $mock_ideal_details ) ); + $this->assertEquals( 'iDEAL', $ideal_method->get_title( 'US', $mock_ideal_details ) ); $this->assertTrue( $ideal_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $ideal_method->is_reusable() ); $this->assertEquals( 'au_becs_debit', $becs_method->get_id() ); $this->assertEquals( 'BECS Direct Debit', $becs_method->get_title() ); - $this->assertEquals( 'BECS Direct Debit', $becs_method->get_title( $mock_becs_details ) ); + $this->assertEquals( 'BECS Direct Debit', $becs_method->get_title( 'US', $mock_becs_details ) ); $this->assertTrue( $becs_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $becs_method->is_reusable() ); $this->assertSame( 'affirm', $affirm_method->get_id() ); $this->assertSame( 'Affirm', $affirm_method->get_title() ); - $this->assertSame( 'Affirm', $affirm_method->get_title( $mock_affirm_details ) ); + $this->assertSame( 'Affirm', $affirm_method->get_title( 'US', $mock_affirm_details ) ); $this->assertTrue( $affirm_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $affirm_method->is_reusable() ); $this->assertSame( 'afterpay_clearpay', $afterpay_method->get_id() ); $this->assertSame( 'Afterpay', $afterpay_method->get_title() ); - $this->assertSame( 'Afterpay', $afterpay_method->get_title( $mock_afterpay_details ) ); + $this->assertSame( 'Afterpay', $afterpay_method->get_title( 'US', $mock_afterpay_details ) ); $this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'US' ) ); $this->assertFalse( $afterpay_method->is_reusable() ); + + $this->assertSame( 'afterpay_clearpay', $afterpay_method->get_id() ); + $this->assertSame( 'Clearpay', $afterpay_method->get_title( 'GB' ) ); + $this->assertSame( 'Clearpay', $afterpay_method->get_title( 'GB', $mock_afterpay_details ) ); + $this->assertTrue( $afterpay_method->is_enabled_at_checkout( 'GB' ) ); + $this->assertFalse( $afterpay_method->is_reusable() ); } public function test_only_reusabled_payment_methods_enabled_with_subscription_item_present() { diff --git a/tests/unit/test-class-wc-payments-checkout.php b/tests/unit/test-class-wc-payments-checkout.php index d6273a1fcd2..72a7688187d 100644 --- a/tests/unit/test-class-wc-payments-checkout.php +++ b/tests/unit/test-class-wc-payments-checkout.php @@ -103,6 +103,9 @@ public function set_up() { ->disableOriginalConstructor() ->getMock(); $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class ); + $this->mock_wcpay_account + ->method( 'get_account_country' ) + ->willReturn( 'US' ); $this->mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class ); $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class ); From 2f2fa96934c0100f98a0cb9bba41c1202de96bff Mon Sep 17 00:00:00 2001 From: Rafael Zaleski+Date: Tue, 23 Jan 2024 15:40:56 -0300 Subject: [PATCH 06/52] Add blog ID validation to WooPay session (#8045) --- changelog/fix-woopay-2401-validate-session | 4 ++ .../woopay-express-checkout-button.js | 46 +++++++++++++------ includes/woopay/class-woopay-session.php | 8 ++++ 3 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 changelog/fix-woopay-2401-validate-session diff --git a/changelog/fix-woopay-2401-validate-session b/changelog/fix-woopay-2401-validate-session new file mode 100644 index 00000000000..b385b3dbadf --- /dev/null +++ b/changelog/fix-woopay-2401-validate-session @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Enhance WooPay session validation diff --git a/client/checkout/woopay/express-button/woopay-express-checkout-button.js b/client/checkout/woopay/express-button/woopay-express-checkout-button.js index 24413105304..ddd4dd1e65f 100644 --- a/client/checkout/woopay/express-button/woopay-express-checkout-button.js +++ b/client/checkout/woopay/express-button/woopay-express-checkout-button.js @@ -285,13 +285,22 @@ export const WoopayExpressCheckoutButton = ( { } ) .then( ( response ) => { - iframe.contentWindow.postMessage( - { - action: 'setPreemptiveSessionData', - value: response, - }, - getConfig( 'woopayHost' ) - ); + if ( + response?.blog_id && + response?.data?.session + ) { + iframe.contentWindow.postMessage( + { + action: 'setPreemptiveSessionData', + value: response, + }, + getConfig( 'woopayHost' ) + ); + } else { + // Set button's default onClick handle to use modal checkout flow. + initWoopayRef.current = onClickFallback; + throw new Error( response?.data ); + } } ) .catch( () => { const errorMessage = __( @@ -317,13 +326,22 @@ export const WoopayExpressCheckoutButton = ( { } ) .then( ( response ) => { - iframe.contentWindow.postMessage( - { - action: 'setPreemptiveSessionData', - value: response, - }, - getConfig( 'woopayHost' ) - ); + if ( + response?.blog_id && + response?.data?.session + ) { + iframe.contentWindow.postMessage( + { + action: 'setPreemptiveSessionData', + value: response, + }, + getConfig( 'woopayHost' ) + ); + } else { + // Set button's default onClick handle to use modal checkout flow. + initWoopayRef.current = onClickFallback; + throw new Error( response?.data ); + } } ) ?.catch( () => { const errorMessage = __( diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php index 42a5ec09c67..b36624163a5 100644 --- a/includes/woopay/class-woopay-session.php +++ b/includes/woopay/class-woopay-session.php @@ -543,6 +543,14 @@ public static function ajax_get_woopay_session() { ); } + $blog_id = Jetpack_Options::get_option('id'); + if ( empty( $blog_id ) ) { + wp_send_json_error( + __( 'Could not determine the blog ID.', 'woocommerce-payments' ), + 503 + ); + } + wp_send_json( self::get_frontend_init_session_request() ); } From e9e5be3a627103ec7be901861c17cbc12e963ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismael=20Mart=C3=ADn=20Alabarce?= Date: Wed, 24 Jan 2024 11:42:33 +0100 Subject: [PATCH 07/52] Update confetti animation (#8083) Co-authored-by: Daniel Mallory --- changelog/update-confetti-animation | 5 ++ .../components/confetti-animation/index.tsx | 65 +++++++++++-------- 2 files changed, 44 insertions(+), 26 deletions(-) create mode 100644 changelog/update-confetti-animation diff --git a/changelog/update-confetti-animation b/changelog/update-confetti-animation new file mode 100644 index 00000000000..195e2ce1c4d --- /dev/null +++ b/changelog/update-confetti-animation @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Confetti animation update. + + diff --git a/client/components/confetti-animation/index.tsx b/client/components/confetti-animation/index.tsx index 32c2d971112..9331f5b8e46 100644 --- a/client/components/confetti-animation/index.tsx +++ b/client/components/confetti-animation/index.tsx @@ -6,38 +6,51 @@ import confetti from 'canvas-confetti'; const defaultColors = [ '#889BF2', '#C3CDF9', '#6079ED' ]; +const randomInRange = ( min: number, max: number ): number => + Math.floor( Math.random() * ( max - min ) + min ); + +// Use a rectangle instead of an square on supported browsers. +const rectangle = + typeof Path2D === 'function' && typeof DOMMatrix === 'function' + ? confetti.shapeFromPath( { + path: 'M0,0 L2,0 L2,1 L0,1 Z', + } ) + : 'square'; + +// Adjust particle amount based on screen size. +const particleLength = ( window.innerWidth + window.innerHeight ) / 50; + const fireConfetti = ( colors: string[] ) => { const defaults = { - origin: { y: 0.3 }, spread: 360, + particleCount: 1, + startVelocity: 0, zIndex: 1000000, - colors, }; - const rectangle = confetti.shapeFromPath( { - path: 'M0,0 L2,0 L2,1 L0,1 Z', - } ); - - confetti( { - ...defaults, - particleCount: 20, - shapes: [ rectangle ], - scalar: 4, - startVelocity: 60, - } ); - confetti( { - ...defaults, - particleCount: 20, - shapes: [ rectangle ], - scalar: 2, - startVelocity: 40, - } ); - confetti( { - ...defaults, - particleCount: 40, - shapes: [ 'circle' ], - startVelocity: 20, - } ); + for ( let i = 0; i < particleLength; i++ ) { + confetti( { + ...defaults, + colors: [ colors[ randomInRange( 0, colors.length ) ] ], + origin: { + x: Math.random(), + y: Math.random() * 0.999 - 0.2, + }, + drift: randomInRange( -2, 2 ), + shapes: [ 'circle' ], + } ); + confetti( { + ...defaults, + colors: [ colors[ randomInRange( 0, colors.length ) ] ], + origin: { + x: Math.random(), + y: Math.random() * 0.999 - 0.2, + }, + shapes: [ rectangle ], + drift: randomInRange( -2, 2 ), + scalar: randomInRange( 2, 4 ), + } ); + } }; interface Props { From fbecab2f70351937023003858983a6a368748546 Mon Sep 17 00:00:00 2001 From: Rafael Zaleski Date: Wed, 24 Jan 2024 11:11:21 -0300 Subject: [PATCH 08/52] Add E2E tests for merchant on-boarding (#7955) Co-authored-by: Brian Borman <68524302+bborman22@users.noreply.github.com> Co-authored-by: Achyuth Ajoy --- .github/actions/e2e/run-log-tests/action.yml | 1 + changelog/e2e-7347-merchant-on-boarding | 4 + .../tasks/add-currencies-task/index.js | 9 +- .../test/__snapshots__/index.test.js.snap | 6 + .../enabled-currencies-list/modal-checkbox.js | 3 +- package.json | 2 +- .../config/jest-puppeteer-headless.config.js | 17 + ...t-admin-multi-currency-on-boarding.spec.js | 362 ++++++++++++++++++ ...erchant-admin-multi-currency-setup.spec.js | 1 + tests/e2e/utils/flows.js | 31 ++ tests/e2e/utils/helpers.js | 18 + 11 files changed, 450 insertions(+), 4 deletions(-) create mode 100644 changelog/e2e-7347-merchant-on-boarding create mode 100644 tests/e2e/config/jest-puppeteer-headless.config.js create mode 100644 tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-on-boarding.spec.js diff --git a/.github/actions/e2e/run-log-tests/action.yml b/.github/actions/e2e/run-log-tests/action.yml index 71e8d883865..142e085d0fe 100644 --- a/.github/actions/e2e/run-log-tests/action.yml +++ b/.github/actions/e2e/run-log-tests/action.yml @@ -39,6 +39,7 @@ runs: with: name: wp(${{ env.E2E_WP_VERSION }})-wc(${{ env.E2E_WC_VERSION }})-${{ env.E2E_GROUP }}-${{ env.E2E_BRANCH }} path: | + screenshots tests/e2e/screenshots tests/e2e/docker/wordpress/wp-content/debug.log ${{ env.E2E_RESULT_FILEPATH }} diff --git a/changelog/e2e-7347-merchant-on-boarding b/changelog/e2e-7347-merchant-on-boarding new file mode 100644 index 00000000000..e1de02a0b10 --- /dev/null +++ b/changelog/e2e-7347-merchant-on-boarding @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +E2E test - Merchant facing multi-currency on-boarding screen. diff --git a/client/multi-currency-setup/tasks/add-currencies-task/index.js b/client/multi-currency-setup/tasks/add-currencies-task/index.js index 335b6b78985..1795edaa6a0 100644 --- a/client/multi-currency-setup/tasks/add-currencies-task/index.js +++ b/client/multi-currency-setup/tasks/add-currencies-task/index.js @@ -173,13 +173,14 @@ const AddCurrenciesTask = () => { ); } ); - const displayCurrencyCheckbox = ( code ) => + const displayCurrencyCheckbox = ( code, testId = '' ) => availableCurrencyCodes.length && ( ); @@ -284,7 +285,11 @@ const AddCurrenciesTask = () => { { visibleRecommendedCurrencyCodes.map( - displayCurrencyCheckbox + ( code ) => + displayCurrencyCheckbox( + code, + 'recommended-currency' + ) ) } + @@ -692,8 +744,341 @@ exports[`PaymentDetailsSummary correctly renders a charge 1`] = ` @@ -388,8 +414,34 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders th { const handleChange = useCallback( ( enabled ) => { @@ -20,7 +21,7 @@ const EnabledCurrenciesModalCheckbox = ( { ); return ( -diff --git a/client/payment-details/summary/style.scss b/client/payment-details/summary/style.scss index 220e534985b..cb72ec859e6 100755 --- a/client/payment-details/summary/style.scss +++ b/client/payment-details/summary/style.scss @@ -86,8 +86,19 @@ display: flex; align-items: start; justify-content: initial; + flex-direction: column; + flex-wrap: nowrap; @include breakpoint( '>660px' ) { - justify-content: flex-end; + justify-content: flex-start; + align-items: flex-end; + } + + .payment-details-summary__id_wrapper { + white-space: nowrap; + } + + .payment-details-summary__id_value { + font-family: monospace; } } } diff --git a/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap b/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap index c6c02760b87..cc2f878f0a1 100644 --- a/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap +++ b/client/payment-details/summary/test/__snapshots__/index.test.tsx.snap @@ -62,8 +62,34 @@ exports[`PaymentDetailsSummary capture notification and fraud buttons renders ca+ `.theme[data-slug="${ themeSlug }"]`; +const ACTIVATE_THEME_BUTTON_SELECTOR = ( themeSlug ) => + `${ THEME_SELECTOR( themeSlug ) } .button.activate`; +const MULTI_CURRENCY_TOGGLE_SELECTOR = "[data-testid='multi-currency-toggle']"; +const RECOMMENDED_CURRENCY_LIST_SELECTOR = + 'li[data-testid="recommended-currency"]'; +const CURRENCY_NOT_IN_RECOMMENDED_LIST_SELECTOR = + 'li.enabled-currency-checkbox:not([data-testid="recommended-currency"])'; +const ENABLED_CURRENCY_LIST_SELECTOR = 'li.enabled-currency-checkbox'; +const GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR = + 'input[data-testid="enable_auto_currency"]'; +const PREVIEW_STORE_BTN_SELECTOR = '.multi-currency-setup-preview-button'; +const PREVIEW_STORE_IFRAME_SELECTOR = + '.multi-currency-store-settings-preview-iframe'; +const SUBMIT_STEP_BTN_SELECTOR = + '.add-currencies-task.is-active .task-collapsible-body.is-active > button.is-primary'; +const STOREFRONT_SWITCH_CHECKBOX_SELECTOR = + 'input[data-testid="enable_storefront_switcher"]'; + +let wasMulticurrencyEnabled; + +const goToThemesPage = async () => { + await page.goto( `${ WP_ADMIN_DASHBOARD }themes.php`, { + waitUntil: 'networkidle0', + } ); +}; + +const activateTheme = async ( themeSlug ) => { + await goToThemesPage(); + + // Check if the theme is already active. + const isActive = await page.evaluate( ( selector ) => { + const themeElement = document.querySelector( selector ); + return themeElement && themeElement.classList.contains( 'active' ); + }, THEME_SELECTOR( themeSlug ) ); + + // Activate the theme if it's not already active. + if ( ! isActive ) { + await page.click( ACTIVATE_THEME_BUTTON_SELECTOR( themeSlug ) ); + await page.waitForNavigation( { waitUntil: 'networkidle0' } ); + } +}; + +const goToOnboardingPage = async () => { + await page.goto( + `${ WP_ADMIN_DASHBOARD }admin.php?page=wc-admin&path=%2Fpayments%2Fmulti-currency-setup`, + { + waitUntil: 'networkidle0', + } + ); + await uiLoaded(); +}; + +const goToNextOnboardingStep = async () => { + await page.click( SUBMIT_STEP_BTN_SELECTOR ); +}; + +describe( 'Merchant On-boarding', () => { + let activeThemeSlug; + + beforeAll( async () => { + await merchant.login(); + // Get initial multi-currency feature status. + await merchantWCP.openWCPSettings(); + await page.waitForSelector( MULTI_CURRENCY_TOGGLE_SELECTOR ); + wasMulticurrencyEnabled = await page.evaluate( ( selector ) => { + const checkbox = document.querySelector( selector ); + return checkbox ? checkbox.checked : false; + }, MULTI_CURRENCY_TOGGLE_SELECTOR ); + await merchantWCP.activateMulticurrency(); + + await goToThemesPage(); + + // Get current theme slug. + activeThemeSlug = await page.evaluate( () => { + const theme = document.querySelector( '.theme.active' ); + return theme ? theme.getAttribute( 'data-slug' ) : ''; + } ); + } ); + + afterAll( async () => { + // Restore original theme. + await activateTheme( activeThemeSlug ); + + // Disable multi-currency if it was not initially enabled. + if ( ! wasMulticurrencyEnabled ) { + await merchant.login(); + await merchantWCP.deactivateMulticurrency(); + } + await merchant.logout(); + } ); + + describe( 'Currency Selection and Management', () => { + beforeAll( async () => { + await merchantWCP.disableAllEnabledCurrencies(); + } ); + + beforeEach( async () => { + await goToOnboardingPage(); + } ); + + it( 'Should disable the submit button when no currencies are selected', async () => { + await takeScreenshot( 'merchant-on-boarding-multicurrency-screen' ); + await setCheckboxState( + `${ ENABLED_CURRENCY_LIST_SELECTOR } .components-checkbox-control__input`, + false + ); + + await page.waitFor( 1000 ); + + const button = await page.$( SUBMIT_STEP_BTN_SELECTOR ); + expect( button ).not.toBeNull(); + + const isDisabled = await page.evaluate( + ( btn ) => btn.disabled, + button + ); + + expect( isDisabled ).toBeTruthy(); + } ); + + it( 'Should allow multiple currencies to be selectable', async () => { + await page.waitForSelector( + CURRENCY_NOT_IN_RECOMMENDED_LIST_SELECTOR, + { + timeout: 3000, + } + ); + + // Ensure the checkbox within the list item is present and not disabled. + const checkbox = await page.$( + `${ CURRENCY_NOT_IN_RECOMMENDED_LIST_SELECTOR } input[type="checkbox"]` + ); + expect( checkbox ).not.toBeNull(); + const isDisabled = await ( + await checkbox.getProperty( 'disabled' ) + ).jsonValue(); + expect( isDisabled ).toBe( false ); + + // Click the checkbox to select the currency and verify it's checked. + await checkbox.click(); + + const isChecked = await ( + await checkbox.getProperty( 'checked' ) + ).jsonValue(); + expect( isChecked ).toBe( true ); + } ); + + it( 'Should exclude already enabled currencies from the currency screen', async () => { + await merchantWCP.addCurrency( 'GBP' ); + + await goToOnboardingPage(); + + await page.waitForSelector( ENABLED_CURRENCY_LIST_SELECTOR, { + timeout: 3000, + } ); + + // Get the list of currencies as text + const currencies = await page.$$eval( + ENABLED_CURRENCY_LIST_SELECTOR, + ( items ) => items.map( ( item ) => item.textContent.trim() ) + ); + + expect( currencies ).not.toContain( 'GBP' ); + + await merchantWCP.removeCurrency( 'GBP' ); + } ); + + it( 'Should display some suggested currencies at the beginning of the list', async () => { + await page.waitForSelector( RECOMMENDED_CURRENCY_LIST_SELECTOR, { + timeout: 3000, + } ); + + // Get the list of recommended currencies + const recommendedCurrencies = await page.$$eval( + RECOMMENDED_CURRENCY_LIST_SELECTOR, + ( items ) => + items.map( ( item ) => ( { + code: item + .querySelector( 'input' ) + .getAttribute( 'code' ), + name: item + .querySelector( + 'span.enabled-currency-checkbox__code' + ) + .textContent.trim(), + } ) ) + ); + + expect( recommendedCurrencies.length ).toBeGreaterThan( 0 ); + } ); + + it( 'Should ensure selected currencies are enabled after submitting the form', async () => { + const testCurrencies = [ 'GBP', 'EUR', 'CAD', 'AUD' ]; + const addCurrenciesContentSelector = + '.add-currencies-task__content'; + const currencyCheckboxSelector = `${ addCurrenciesContentSelector } li input[type="checkbox"]`; + + await page.waitForSelector( addCurrenciesContentSelector, { + timeout: 3000, + } ); + + // Select the currencies + for ( const currency of testCurrencies ) { + await setCheckboxState( + `${ currencyCheckboxSelector }[code="${ currency }"]`, + true + ); + } + + // Submit the form. + await goToNextOnboardingStep(); + + await merchantWCP.openMultiCurrency(); + + // Ensure the currencies are enabled. + for ( const currency of testCurrencies ) { + const selector = `li.enabled-currency.${ currency.toLowerCase() }`; + await page.waitForSelector( selector, { timeout: 10000 } ); + const element = await page.$( selector ); + + expect( element ).not.toBeNull(); + } + } ); + } ); + + describe( 'Geolocation Features', () => { + beforeAll( async () => { + await merchantWCP.disableAllEnabledCurrencies(); + } ); + + beforeEach( async () => { + await goToOnboardingPage(); + } ); + + it( 'Should offer currency switch by geolocation', async () => { + await goToNextOnboardingStep(); + + const checkbox = await page.$( + GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR + ); + + // Check if exists and not disabled. + expect( checkbox ).not.toBeNull(); + const isDisabled = await ( + await checkbox.getProperty( 'disabled' ) + ).jsonValue(); + expect( isDisabled ).toBe( false ); + + // Click the checkbox to select it. + await page.click( GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR ); + + // Check if the checkbox is selected. + const isChecked = await ( + await checkbox.getProperty( 'checked' ) + ).jsonValue(); + expect( isChecked ).toBe( true ); + } ); + + it( 'Should preview currency switch by geolocation correctly with USD and GBP', async () => { + page.setViewport( { width: 1280, height: 1280 } ); // To take a better screenshot of the iframe preview. + + await goToNextOnboardingStep(); + + await takeScreenshot( + 'merchant-on-boarding-multicurrency-screen-2' + ); + + // Enable feature. + await setCheckboxState( + GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR, + true + ); + + // Click preview button. + await page.click( PREVIEW_STORE_BTN_SELECTOR ); + + await page.waitForSelector( PREVIEW_STORE_IFRAME_SELECTOR, { + timeout: 3000, + } ); + + const iframeElement = await page.$( PREVIEW_STORE_IFRAME_SELECTOR ); + const iframe = await iframeElement.contentFrame(); + + await iframe.waitForSelector( '.woocommerce-store-notice', { + timeout: 3000, + } ); + + await takeScreenshot( + 'merchant-on-boarding-multicurrency-geolocation-switcher-preview' + ); + + const noticeText = await iframe.$eval( + '.woocommerce-store-notice', + ( element ) => element.innerText + ); + expect( noticeText ).toContain( + // eslint-disable-next-line max-len + "We noticed you're visiting from United Kingdom (UK). We've updated our prices to Pound sterling for your shopping convenience." + ); + } ); + } ); + + describe( 'Currency Switcher Widget', () => { + it( 'Should offer the currency switcher widget while Storefront theme is active', async () => { + await activateTheme( 'storefront' ); + + await goToOnboardingPage(); + await goToNextOnboardingStep(); + + const checkbox = await page.$( + STOREFRONT_SWITCH_CHECKBOX_SELECTOR + ); + + // Check if exists and not disabled. + expect( checkbox ).not.toBeNull(); + const isDisabled = await ( + await checkbox.getProperty( 'disabled' ) + ).jsonValue(); + expect( isDisabled ).toBe( false ); + + // Click the checkbox to select it. + await page.click( STOREFRONT_SWITCH_CHECKBOX_SELECTOR ); + + // Check if the checkbox is selected. + const isChecked = await ( + await checkbox.getProperty( 'checked' ) + ).jsonValue(); + expect( isChecked ).toBe( true ); + } ); + + it( 'Should not offer the currency switcher widget when an unsupported theme is active', async () => { + await activateTheme( 'twentytwentyfour' ); + + await goToOnboardingPage(); + await goToNextOnboardingStep(); + + const checkbox = await page.$( + STOREFRONT_SWITCH_CHECKBOX_SELECTOR + ); + + expect( checkbox ).toBeNull(); + + await activateTheme( 'storefront' ); + } ); + } ); +} ); diff --git a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-setup.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-setup.spec.js index 51f7fc9cd67..800ce9d0e8b 100644 --- a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-setup.spec.js +++ b/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-setup.spec.js @@ -75,6 +75,7 @@ describe( 'Merchant Multi-Currency Settings', () => { beforeAll( async () => { await merchantWCP.activateMulticurrency(); + await merchantWCP.disableAllEnabledCurrencies(); await shopperWCP.goToShopWithCurrency( 'USD' ); await shopperWCP.goToProductPageBySlug( 'beanie' ); diff --git a/tests/e2e/utils/flows.js b/tests/e2e/utils/flows.js index 3399d5b9ffe..af662b55fee 100644 --- a/tests/e2e/utils/flows.js +++ b/tests/e2e/utils/flows.js @@ -513,6 +513,11 @@ export const merchantWCP = { } }, currencyCode ); + await page.waitForSelector( + 'div.wcpay-confirmation-modal__footer button.components-button.is-primary', + { timeout: 3000 } + ); + await page.click( 'div.wcpay-confirmation-modal__footer button.components-button.is-primary', { text: 'Update selected' } @@ -532,6 +537,7 @@ export const merchantWCP = { }, removeCurrency: async ( currencyCode ) => { + await merchantWCP.openMultiCurrency(); const currencyItemSelector = `li.enabled-currency.${ currencyCode.toLowerCase() }`; await page.waitForSelector( currencyItemSelector, { timeout: 10000 } ); await page.click( @@ -732,6 +738,31 @@ export const merchantWCP = { return wasInitiallyEnabled; }, + disableAllEnabledCurrencies: async () => { + await page.goto( WCPAY_MULTI_CURRENCY, { waitUntil: 'networkidle0' } ); + + await page.waitForSelector( '.enabled-currencies-list li', { + timeout: 10000, + } ); + + // Select all delete buttons for enabled currencies. + const deleteButtons = await page.$$( + '.enabled-currency .enabled-currency__action.delete' + ); + + // Loop through each delete button and click it. + for ( const button of deleteButtons ) { + await button.click(); + + await page.waitForSelector( '.components-snackbar', { + text: 'Enabled currencies updated.', + timeout: 10000, + } ); + + await page.waitFor( 1000 ); + } + }, + editCurrency: async ( currencyCode ) => { await merchantWCP.openMultiCurrency(); diff --git a/tests/e2e/utils/helpers.js b/tests/e2e/utils/helpers.js index ca829481a27..2d9b0e0932b 100644 --- a/tests/e2e/utils/helpers.js +++ b/tests/e2e/utils/helpers.js @@ -80,3 +80,21 @@ export const getProductPriceFromProductPage = async () => { return price; }; + +/** + * Sets the state of all checkboxes matching the specified selector. + * + * @param {string} selector The selector to use to find checkboxes. + * @param {boolean} desiredState The desired state of the checkboxes. + */ +export const setCheckboxState = async ( selector, desiredState ) => { + const checkboxes = await page.$$( selector ); + for ( const checkbox of checkboxes ) { + const isChecked = await ( + await checkbox.getProperty( 'checked' ) + ).jsonValue(); + if ( isChecked !== desiredState ) { + await checkbox.click(); + } + } +}; From 44ab94f7f77f4eebac1f8dd61e30975e58c25ff3 Mon Sep 17 00:00:00 2001 From: Hector Lovo Date: Wed, 24 Jan 2024 18:36:14 -0500 Subject: [PATCH 09/52] Prevent Coupon Usage Increase in a Woopay Preflight Check (#8065) Co-authored-by: Hector Lovo <> --- .../fix-2309-woopay-preflight-coupon-usage | 4 ++ includes/class-wc-payment-gateway-wcpay.php | 4 +- ...-payment-gateway-wcpay-process-payment.php | 57 ++++++++++++++++++- 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-2309-woopay-preflight-coupon-usage diff --git a/changelog/fix-2309-woopay-preflight-coupon-usage b/changelog/fix-2309-woopay-preflight-coupon-usage new file mode 100644 index 00000000000..44e56d952ec --- /dev/null +++ b/changelog/fix-2309-woopay-preflight-coupon-usage @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Prevent coupon usage increase in a WooPay preflight check. diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 0068a32e33f..e96b468c8ae 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -634,7 +634,7 @@ private function get_enabled_payment_method_config() { /** * If we're in a WooPay preflight check, remove all the checkout order processed - * actions to prevent reduce available resources quantity. + * actions to prevent a quantity reduction of the available resources. * * @param mixed $response The response object. * @param mixed $handler The handler used for the response. @@ -647,6 +647,8 @@ public function remove_all_actions_on_preflight_check( $response, $handler, $req if ( ! empty( $payment_data['is-woopay-preflight-check'] ) ) { remove_all_actions( 'woocommerce_store_api_checkout_update_order_meta' ); remove_all_actions( 'woocommerce_store_api_checkout_order_processed' ); + // Avoid increasing coupon usage count during preflight check. + remove_all_actions( 'woocommerce_order_status_pending' ); } return $response; diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php index b064ef08f90..a3038cc410c 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php @@ -702,7 +702,7 @@ public function test_failed_transaction_rate_limiter_is_limited() { } /** - * Tests that a draft order is updated to "pending" when the $_POST 'is-woopay-preflight-check` is present. + * Tests that a draft order is updated to "pending" when the $_POST 'is-woopay-preflight-check' is present. */ public function test_draft_order_is_set_to_pending_for_woopay_preflight_check_request() { $_POST['is-woopay-preflight-check'] = true; @@ -723,7 +723,60 @@ public function test_draft_order_is_set_to_pending_for_woopay_preflight_check_re } /** - * Tests that a success response and no redirect is returned when the $_POST 'is-woopay-preflight-check` is present. + * Tests that woocommerce_order_status_pending action is not called when the $_POST 'is-woopay-preflight-check' is present. + */ + public function test_woopay_preflight_request_does_not_call_woocommerce_order_status_pending() { + // Arrange: Add woocommerce_order_status_pending action to check if it's called. + $results = [ + 'has_called_woocommerce_order_status_pending' => false, + ]; + add_action( + 'woocommerce_order_status_pending', + function () use ( &$results ) { + $results['has_called_woocommerce_order_status_pending'] = true; + } + ); + + // Arrange: Add filter to change default order status to 'wc-checkout-draft'. + // Needed to avoid a default order status of 'pending'. + add_filter( + 'woocommerce_default_order_status', + function () { + return 'wc-checkout-draft'; + } + ); + + // Arrange: Create a request to simulate a woopay preflight request. + $_POST['is-woopay-preflight-check'] = true; + $request = new WP_REST_Request( 'POST', '' ); + $request->set_body_params( + [ + 'payment_data' => [ + [ + 'key' => 'is-woopay-preflight-check', + 'value' => true, + ], + ], + ] + ); + apply_filters( 'rest_request_before_callbacks', [], [], $request ); + + // Arrange: Create an order to test with. + $order_data = [ + 'status' => 'wc-checkout-draft', + 'total' => '100', + ]; + $order = wc_create_order( $order_data ); + + // Act: process payment. + $this->mock_wcpay_gateway->process_payment( $order->get_id() ); + + // Assert: woocommerce_order_status_pending was not called. + $this->assertFalse( $results['has_called_woocommerce_order_status_pending'] ); + } + + /** + * Tests that a success response and no redirect is returned when the $_POST 'is-woopay-preflight-check' is present. */ public function test_successful_result_no_redirect_for_woopay_preflight_check_request() { $_POST['is-woopay-preflight-check'] = true; From 3cae21525274f8340a92415ed36a6ce7148ec05b Mon Sep 17 00:00:00 2001 From: Rua Haszard Date: Thu, 25 Jan 2024 14:57:20 +1300 Subject: [PATCH 10/52] remove unnecessary tracks events for acceptDispute submit success/fail (#8068) Co-authored-by: Rua Haszard --- changelog/fix-remove-dispute-accept-log-tracks-events | 4 ++++ client/data/disputes/actions.js | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-remove-dispute-accept-log-tracks-events diff --git a/changelog/fix-remove-dispute-accept-log-tracks-events b/changelog/fix-remove-dispute-accept-log-tracks-events new file mode 100644 index 00000000000..e33d28ff524 --- /dev/null +++ b/changelog/fix-remove-dispute-accept-log-tracks-events @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Remove unnecessary tracks events for dispute accept success/error. diff --git a/client/data/disputes/actions.js b/client/data/disputes/actions.js index 3979390e3cf..e0618b24852 100644 --- a/client/data/disputes/actions.js +++ b/client/data/disputes/actions.js @@ -12,7 +12,6 @@ import { __, sprintf } from '@wordpress/i18n'; */ import { NAMESPACE, STORE_NAME } from '../constants'; import TYPES from './action-types'; -import wcpayTracks from 'tracks'; import { getPaymentIntent } from '../payment-intents/resolvers'; export function updateDispute( data ) { @@ -70,7 +69,6 @@ export function* acceptDispute( dispute ) { id, ] ); - wcpayTracks.recordEvent( 'wcpay_dispute_accept_success' ); const message = updatedDispute.order ? sprintf( /* translators: #%s is an order number, e.g. 15 */ @@ -91,7 +89,6 @@ export function* acceptDispute( dispute ) { 'There has been an error accepting the dispute. Please try again later.', 'woocommerce-payments' ); - wcpayTracks.recordEvent( 'wcpay_dispute_accept_failed' ); yield controls.dispatch( 'core/notices', 'createErrorNotice', message ); yield controls.dispatch( STORE_NAME, 'finishResolution', 'getDispute', [ id, From 0e7fda9c149c6b0fb066d78caee19becb8a8abeb Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 25 Jan 2024 09:31:52 +0100 Subject: [PATCH 11/52] chore: remove unused customGatewayTitle (#8071) --- .../frosso-clean-unused-custom-gateway-title | 5 +++++ client/utils/checkout.js | 19 ------------------- 2 files changed, 5 insertions(+), 19 deletions(-) create mode 100644 changelog/frosso-clean-unused-custom-gateway-title diff --git a/changelog/frosso-clean-unused-custom-gateway-title b/changelog/frosso-clean-unused-custom-gateway-title new file mode 100644 index 00000000000..84a0e09048b --- /dev/null +++ b/changelog/frosso-clean-unused-custom-gateway-title @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: removed unused customGatewyTitle function. + + diff --git a/client/utils/checkout.js b/client/utils/checkout.js index 4f341cecd61..4a67666e827 100644 --- a/client/utils/checkout.js +++ b/client/utils/checkout.js @@ -37,22 +37,3 @@ export const getUPEConfig = ( name ) => { return config[ name ] || null; }; - -/** - * Forms dynamic gateway title for UPE checkout from enabled methods - * - * @param {Object} paymentMethodsConfig Object containing map of enabled UPE payment methods to settings. - * @return {string} Dynamic title string dependent on payment methods enabled. - */ -export const getCustomGatewayTitle = ( paymentMethodsConfig ) => { - const enabledPaymentMethods = Object.keys( paymentMethodsConfig ).sort(); - let label = ''; - - if ( enabledPaymentMethods.length < 2 ) { - label = paymentMethodsConfig[ enabledPaymentMethods[ 0 ] ].title; - } else { - label = getConfig( 'checkoutTitle' ); - } - - return label; -}; From ff86d88bb5f9f192a3eb3f4a7c71429eaa863ad9 Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 25 Jan 2024 09:32:00 +0100 Subject: [PATCH 12/52] chore: remove unused checkout API functions (#8081) --- .../chore-remove-unused-checkout-api-methods | 4 ++ client/checkout/api/index.js | 45 ---------------- ...s-duplicate-payment-prevention-service.php | 10 ++-- includes/class-wc-payment-gateway-wcpay.php | 53 ------------------- includes/class-wc-payments-checkout.php | 3 -- ...s-duplicate-payment-prevention-service.php | 2 - 6 files changed, 8 insertions(+), 109 deletions(-) create mode 100644 changelog/chore-remove-unused-checkout-api-methods diff --git a/changelog/chore-remove-unused-checkout-api-methods b/changelog/chore-remove-unused-checkout-api-methods new file mode 100644 index 00000000000..bf7970a5bb0 --- /dev/null +++ b/changelog/chore-remove-unused-checkout-api-methods @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +chore: remove unused checkout API methods diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js index 1c42111e6e0..e3eaac75346 100644 --- a/client/checkout/api/index.js +++ b/client/checkout/api/index.js @@ -565,49 +565,4 @@ export default class WCPayAPI { ...paymentData, } ); } - - /** - * Log Payment Errors via Ajax. - * - * @param {string} chargeId Stripe Charge ID - * @return {boolean} Returns true irrespective of result. - */ - logPaymentError( chargeId ) { - return this.request( - buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'log_payment_error' ), - { - charge_id: chargeId, - _ajax_nonce: getConfig( 'logPaymentErrorNonce' ), - } - ).then( () => { - // There is not any action to take or harm caused by a failed update, so just returning true. - return true; - } ); - } - - /** - * Redirect to the order-received page for duplicate payments. - * - * @param {Object} response Response data to check if doing the redirect. - * @return {boolean} Returns true if doing the redirection. - */ - handleDuplicatePayments( { - wcpay_upe_paid_for_previous_order: previouslyPaid, - wcpay_upe_previous_successful_intent: previousSuccessfulIntent, - redirect, - } ) { - if ( redirect ) { - // Another order has the same cart content and was paid. - if ( previouslyPaid ) { - return ( window.location = redirect ); - } - - // Another intent has the equivalent successful status for the order. - if ( previousSuccessfulIntent ) { - return ( window.location = redirect ); - } - } - - return false; - } } diff --git a/includes/class-duplicate-payment-prevention-service.php b/includes/class-duplicate-payment-prevention-service.php index 71a0bfbcc10..35ac1896234 100644 --- a/includes/class-duplicate-payment-prevention-service.php +++ b/includes/class-duplicate-payment-prevention-service.php @@ -120,9 +120,8 @@ public function check_payment_intent_attached_to_order_succeeded( WC_Order $orde $return_url = $this->gateway->get_return_url( $order ); $return_url = add_query_arg( self::FLAG_PREVIOUS_SUCCESSFUL_INTENT, 'yes', $return_url ); return [ // nosemgrep: audit.php.wp.security.xss.query-arg -- https://woocommerce.github.io/code-reference/classes/WC-Payment-Gateway.html#method_get_return_url is passed in. - 'result' => 'success', - 'redirect' => $return_url, - 'wcpay_upe_previous_successful_intent' => 'yes', // This flag is needed for UPE flow. + 'result' => 'success', + 'redirect' => $return_url, ]; } @@ -178,9 +177,8 @@ public function check_against_session_processing_order( WC_Order $current_order $return_url = add_query_arg( self::FLAG_PREVIOUS_ORDER_PAID, 'yes', $return_url ); return [ // nosemgrep: audit.php.wp.security.xss.query-arg -- https://woocommerce.github.io/code-reference/classes/WC-Payment-Gateway.html#method_get_return_url is passed in. - 'result' => 'success', - 'redirect' => $return_url, - 'wcpay_upe_paid_for_previous_order' => 'yes', // This flag is needed for UPE flow. + 'result' => 'success', + 'redirect' => $return_url, ]; } diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index e96b468c8ae..8f7e09b09ac 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -899,59 +899,6 @@ public function output_payments_settings_screen() { endif; } - /** - * Log payment errors on Checkout. - * - * @throws Exception If nonce is not present or invalid or charge ID is empty or order not found. - */ - public function log_payment_error_ajax() { - try { - $is_nonce_valid = check_ajax_referer( 'wcpay_log_payment_error_nonce', false, false ); - if ( ! $is_nonce_valid ) { - throw new Exception( 'Invalid request.' ); - } - - $charge_id = isset( $_POST['charge_id'] ) ? wc_clean( wp_unslash( $_POST['charge_id'] ) ) : ''; - if ( empty( $charge_id ) ) { - throw new Exception( 'Charge ID cannot be empty.' ); - } - - // Get charge data from WCPay Server. - $request = Get_Charge::create( $charge_id ); - $request->set_hook_args( $charge_id ); - $charge_data = $request->send(); - $order_id = $charge_data['metadata']['order_id']; - - // Validate Order ID and proceed with logging errors and updating order status. - $order = wc_get_order( $order_id ); - if ( ! $order ) { - throw new Exception( 'Order not found. Unable to log error.' ); - } - - $intent_id = $charge_data['payment_intent'] ?? $order->get_meta( '_intent_id' ); - - $request = Get_Intention::create( $intent_id ); - $request->set_hook_args( $order ); - $intent = $request->send(); - - $intent_status = $intent->get_status(); - $error_message = esc_html( rtrim( $charge_data['failure_message'], '.' ) ); - - $this->order_service->mark_payment_failed( $order, $intent_id, $intent_status, $charge_id, $error_message ); - - wp_send_json_success(); - } catch ( Exception $e ) { - wp_send_json_error( - [ - 'error' => [ - 'message' => WC_Payments_Utils::get_filtered_error_message( $e ), - ], - ], - WC_Payments_Utils::get_filtered_error_status_code( $e ), - ); - } - } - /** * Displays the save to account checkbox. * diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index 5ba354b3810..f19f74bee7b 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -96,12 +96,10 @@ public function init_hooks() { add_action( 'wc_payments_set_gateway', [ $this, 'set_gateway' ] ); add_action( 'wc_payments_add_upe_payment_fields', [ $this, 'payment_fields' ] ); add_action( 'wp', [ $this->gateway, 'maybe_process_upe_redirect' ] ); - add_action( 'wc_ajax_wcpay_log_payment_error', [ $this->gateway, 'log_payment_error_ajax' ] ); add_action( 'wp_ajax_save_upe_appearance', [ $this->gateway, 'save_upe_appearance_ajax' ] ); add_action( 'wp_ajax_nopriv_save_upe_appearance', [ $this->gateway, 'save_upe_appearance_ajax' ] ); add_action( 'switch_theme', [ $this->gateway, 'clear_upe_appearance_transient' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ $this->gateway, 'clear_upe_appearance_transient' ] ); - add_action( 'wc_ajax_wcpay_log_payment_error', [ $this->gateway, 'log_payment_error_ajax' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts_for_zero_order_total' ], 11 ); @@ -179,7 +177,6 @@ public function get_payment_fields_js_config() { 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'createSetupIntentNonce' => wp_create_nonce( 'wcpay_create_setup_intent_nonce' ), - 'logPaymentErrorNonce' => wp_create_nonce( 'wcpay_log_payment_error_nonce' ), 'initWooPayNonce' => wp_create_nonce( 'wcpay_init_woopay_nonce' ), 'saveUPEAppearanceNonce' => wp_create_nonce( 'wcpay_save_upe_appearance_nonce' ), 'genericErrorMessage' => __( 'There was a problem processing the payment. Please check your email inbox and refresh the page to try again.', 'woocommerce-payments' ), diff --git a/tests/unit/test-class-duplicate-payment-prevention-service.php b/tests/unit/test-class-duplicate-payment-prevention-service.php index b0a3099b3b3..411f446cd40 100644 --- a/tests/unit/test-class-duplicate-payment-prevention-service.php +++ b/tests/unit/test-class-duplicate-payment-prevention-service.php @@ -77,7 +77,6 @@ public function test_check_session_order_redirect_to_previous_order() { $result = $this->service->check_against_session_processing_order( $current_order ); // Assert: the result of check_against_session_processing_order. - $this->assertSame( 'yes', $result['wcpay_upe_paid_for_previous_order'] ); $this->assertSame( 'success', $result['result'] ); $this->assertStringContainsString( $return_url, $result['redirect'] ); @@ -267,7 +266,6 @@ public function test_check_payment_intent_attached_to_order_succeeded_return_red $result = $this->service->check_payment_intent_attached_to_order_succeeded( $order ); // Assert: the result of check_intent_attached_to_order_succeeded. - $this->assertSame( 'yes', $result['wcpay_upe_previous_successful_intent'] ); $this->assertSame( 'success', $result['result'] ); $this->assertStringContainsString( $return_url, $result['redirect'] ); } From 6a6af30827f7531fe12bb627951f5ff747c15159 Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 25 Jan 2024 09:32:14 +0100 Subject: [PATCH 13/52] chore: remove deprecated functions since 5.0.0 (#8073) --- changelog/chore-remove-deprecated-functions | 4 ++ includes/class-wc-payment-gateway-wcpay.php | 56 --------------------- 2 files changed, 4 insertions(+), 56 deletions(-) create mode 100644 changelog/chore-remove-deprecated-functions diff --git a/changelog/chore-remove-deprecated-functions b/changelog/chore-remove-deprecated-functions new file mode 100644 index 00000000000..fc0ec5ed243 --- /dev/null +++ b/changelog/chore-remove-deprecated-functions @@ -0,0 +1,4 @@ +Significance: major +Type: dev + +chore: removed deprecated functions since 5.0.0 diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 8f7e09b09ac..704e499b32b 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -619,19 +619,6 @@ private function validate_order_id_received_vs_intent_meta_order_id( WC_Order $o } } - /** - * Gets payment method settings to pass to client scripts - * - * @deprecated 5.0.0 - * - * @return array - */ - private function get_enabled_payment_method_config() { - wc_deprecated_function( __FUNCTION__, '5.0.0', 'WC_Payments_Checkout::get_enabled_payment_method_config' ); - return WC_Payments::get_wc_payments_checkout()->get_enabled_payment_method_config(); - } - - /** * If we're in a WooPay preflight check, remove all the checkout order processed * actions to prevent a quantity reduction of the available resources. @@ -3837,7 +3824,6 @@ public function get_selected_stripe_payment_type_id() { return $this->stripe_id; } - /** * Returns the list of enabled payment method types that will function with the current checkout. * @@ -4169,7 +4155,6 @@ private function get_payment_method_type_from_payment_details( $payment_method_d return $payment_method_details['type'] ?? null; } - /** * This function wraps WC_Payments::get_payment_method_map, useful for unit testing. * @@ -4296,47 +4281,6 @@ public function is_in_test_mode() { return WC_Payments::mode()->is_test(); } - /** - * Whether the current page is the WooPayments settings page. - * - * @deprecated 5.0.0 - * - * @return bool - */ - public static function is_current_page_settings() { - wc_deprecated_function( __FUNCTION__, '5.0.0', 'WC_Payments_Admin_Settings::is_current_page_settings' ); - return WC_Payments_Admin_Settings::is_current_page_settings(); - } - - /** - * Generates the configuration values, needed for payment fields. - * - * Isolated as a separate method in order to be available both - * during the classic checkout, as well as the checkout block. - * - * @deprecated use WC_Payments_Checkout::get_payment_fields_js_config instead. - * - * @return array - */ - public function get_payment_fields_js_config() { - wc_deprecated_function( __FUNCTION__, '5.0.0', 'WC_Payments_Checkout::get_payment_fields_js_config' ); - return WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config(); - } - - /** - * Prepares customer data to be used on 'Pay for Order' or 'Add Payment Method' pages. - * Customer data is retrieved from order when on Pay for Order. - * Customer data is retrieved from customer when on 'Add Payment Method'. - * - * @deprecated use WC_Payments_Customer_Service::get_prepared_customer_data() instead. - * - * @return array|null An array with customer data or nothing. - */ - public function get_prepared_customer_data() { - wc_deprecated_function( __FUNCTION__, '5.0.0', 'WC_Payments_Customer_Service::get_prepared_customer_data' ); - return WC_Payments::get_customer_service()->get_prepared_customer_data(); - } - // End: Deprecated functions. /** From 3e13b1d52e9d0be9212681d334317ed564977bbd Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 25 Jan 2024 14:17:24 +0100 Subject: [PATCH 14/52] fix: pay-for-order compatibility with other gateways (#8089) --- .../fix-pay-for-order-compatibility-with-other-gateways | 4 ++++ client/checkout/classic/event-handlers.js | 8 ++++++++ 2 files changed, 12 insertions(+) create mode 100644 changelog/fix-pay-for-order-compatibility-with-other-gateways diff --git a/changelog/fix-pay-for-order-compatibility-with-other-gateways b/changelog/fix-pay-for-order-compatibility-with-other-gateways new file mode 100644 index 00000000000..94c79de9ce7 --- /dev/null +++ b/changelog/fix-pay-for-order-compatibility-with-other-gateways @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +fix: pay-for-order compatibility with other gateways diff --git a/client/checkout/classic/event-handlers.js b/client/checkout/classic/event-handlers.js index b6daa42875a..6e34fe52909 100644 --- a/client/checkout/classic/event-handlers.js +++ b/client/checkout/classic/event-handlers.js @@ -130,6 +130,14 @@ jQuery( function ( $ ) { } ); $payForOrderForm.on( 'submit', function () { + if ( + $payForOrderForm + .find( "input:checked[name='payment_method']" ) + .val() !== 'woocommerce_payments' + ) { + return; + } + return processPaymentIfNotUsingSavedMethod( $payForOrderForm ); } ); From e2a17324147a7be04eda8a8372f36c53906e2e94 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Fri, 26 Jan 2024 09:24:52 +1000 Subject: [PATCH 15/52] Update deposits REST API docs with decoupled deposit changes (#7848) Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> --- ...te-deposits-api-docs-estimated-deposits-rm | 4 + .../source/includes/wp-api-v3/deposits.md | 105 +++--------------- 2 files changed, 21 insertions(+), 88 deletions(-) create mode 100644 changelog/fix-7847-update-deposits-api-docs-estimated-deposits-rm diff --git a/changelog/fix-7847-update-deposits-api-docs-estimated-deposits-rm b/changelog/fix-7847-update-deposits-api-docs-estimated-deposits-rm new file mode 100644 index 00000000000..cd0f0e04d8d --- /dev/null +++ b/changelog/fix-7847-update-deposits-api-docs-estimated-deposits-rm @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update REST API documentation for deposits endpoints with changes to estimated and instant deposits diff --git a/docs/rest-api/source/includes/wp-api-v3/deposits.md b/docs/rest-api/source/includes/wp-api-v3/deposits.md index f075242ea5f..4d1d8a07607 100644 --- a/docs/rest-api/source/includes/wp-api-v3/deposits.md +++ b/docs/rest-api/source/includes/wp-api-v3/deposits.md @@ -26,7 +26,7 @@ The Deposits API endpoints provide access to an account's deposits data, includi - `date` _int_ - The arrival date of the deposit in unix timestamp milliseconds. - `type` _string_ - The type of deposit. `deposit` `withdrawal` - `amount` _int_ - The amount of the deposit. -- `status` _string_ - The status of the deposit. `paid` `pending` `in_transit` `canceled` `failed` `estimated` +- `status` _string_ - The status of the deposit. `paid` `pending` `in_transit` `canceled` `failed` - `bankAccount` _string_ - The bank account the deposit was/will be paid to. - `currency` _string_ - The currency of the deposit. E.g. `eur` - `automatic` _bool_ - Returns `true` if the payout is created by an automated schedule and `false` if it’s requested manually. @@ -51,14 +51,12 @@ Fetch an overview of account deposits for all deposit currencies. This includes - `deposit` _object_ - `last_paid` _array_ of [**Deposit**](#deposit-object) - The last deposit that has been paid for each deposit currency. - - `next_scheduled` _array_ of [**Deposit**](#deposit-object) - The next scheduled deposit for each deposit currency. - `last_manual_deposits` _array_ of [**Deposit**](#deposit-object) - Manual deposits that have been paid in the last 24 hours. - `balance` _object_ - `pending` _array_ - The pending balance for each deposit currency. - `amount` _int_ - The amount of the balance. - `currency` _string_ - The currency of the balance. E.g. `usd`. - `source_types` _object_ | _null_ - The amount of the balance from each source type, e.g. `{ "card": 12345 }` - - `deposits_count` _int_ - The number of deposits that make up the balance. - `available` _array_ - The available balance for each deposit currency. - `amount` _int_ - The amount of the balance. - `currency` _string_ - The currency of the balance. E.g. `usd`. @@ -69,7 +67,6 @@ Fetch an overview of account deposits for all deposit currencies. This includes - `fee` _int_ - The fee amount of the balance. - `fee_percentage` _int_ - The fee percentage of the balance. - `net` _int_ - The net amount of the balance. - - `transaction_ids` _array_ - The list of transaction IDs that make up the balance. - `account` _object_ - `deposits_enabled` _bool_ - Whether deposits are enabled for the account. - `deposits_blocked` _bool_ - Whether deposits are blocked for the account. @@ -118,34 +115,6 @@ curl -X GET https://example.com/wp-json/wc/v3/payments/deposits/overview-all \ "created": 1701302400 } ], - "next_scheduled": [ - { - "id": "wcpay_estimated_weekly_eur_1702598400", - "date": 1702598400000, - "type": "deposit", - "amount": 458784, - "status": "estimated", - "bankAccount": "STRIPE TEST BANK •••• 3000 (EUR)", - "currency": "eur", - "automatic": true, - "fee": 0, - "fee_percentage": 0, - "created": 1702598400 - }, - { - "id": "wcpay_estimated_weekly_usd_1701993600", - "date": 1701993600000, - "type": "deposit", - "amount": 823789, - "status": "estimated", - "bankAccount": "STRIPE TEST BANK •••• 6789 (USD)", - "currency": "usd", - "automatic": true, - "fee": 0, - "fee_percentage": 0, - "created": 1701993600 - } - ], "last_manual_deposits": [] }, "balance": { @@ -155,16 +124,14 @@ curl -X GET https://example.com/wp-json/wc/v3/payments/deposits/overview-all \ "currency": "eur", "source_types": { "card": -114696 - }, - "deposits_count": 1 + } }, { "amount": 707676, "currency": "usd", "source_types": { "card": 707676 - }, - "deposits_count": 2 + } } ], "available": [ @@ -189,11 +156,7 @@ curl -X GET https://example.com/wp-json/wc/v3/payments/deposits/overview-all \ "currency": "usd", "fee": 185, "fee_percentage": 1.5, - "net": 0, - "transaction_ids": [ - "txn_3OHyIxCIHGKp1UAi0aVyDQ5D", - "txn_3OJSuOCIHGKp1UAi1mRA2lL5" - ] + "net": 0 } ] }, @@ -226,13 +189,11 @@ Fetch an overview of account deposits for a single deposit currency. This includ ### Returns - `last_deposit` _object_ [**Deposit**](#deposit-object) | _null_- The last deposit that has been paid for the deposit currency. -- `next_deposit` _object_ [**Deposit**](#deposit-object) | _null_ - The next scheduled deposit for the deposit currency. - `balance` _object_ - `pending` _object_ - The pending balance for the deposit currency. - `amount` _int_ - The amount of the balance. - `currency` _string_ - The currency of the balance. E.g. `usd`. - `source_types` _object_ | _null_ - The amount of the balance from each source type, e.g. `{ "card": 12345 }` - - `deposits_count` _int_ - The number of deposits that make up the balance. - `available` _object_ - The available balance for the deposit currency. - `amount` _int_ - The amount of the balance. - `currency` _string_ - The currency of the balance. E.g. `usd`. @@ -243,7 +204,6 @@ Fetch an overview of account deposits for a single deposit currency. This includ - `fee` _int_ - The fee amount of the balance. - `fee_percentage` _int_ - The fee percentage of the balance. - `net` _int_ - The net amount of the balance. - - `transaction_ids` _array_ - The list of transaction IDs that make up the balance. - `account` _object_ - `deposits_disabled` _bool_ - Whether deposits are enabled for the account. - `deposits_blocked` _bool_ - Whether deposits are blocked for the account. @@ -276,19 +236,6 @@ curl -X GET https://example.com/wp-json/wc/v3/payments/deposits/overview \ "fee_percentage": 0, "created": 1701648000 }, - "next_deposit": { - "id": "wcpay_estimated_weekly_eur_1702598400", - "date": 1702598400000, - "type": "deposit", - "amount": 458784, - "status": "estimated", - "bankAccount": "STRIPE TEST BANK •••• 3000 (EUR)", - "currency": "eur", - "automatic": true, - "fee": 0, - "fee_percentage": 0, - "created": 1702598400 - }, "balance": { "available": { "amount": 573480, @@ -302,8 +249,7 @@ curl -X GET https://example.com/wp-json/wc/v3/payments/deposits/overview \ "currency": "eur", "source_types": { "card": -114696 - }, - "deposits_count": 1 + } } }, "instant_balance": { @@ -311,8 +257,7 @@ curl -X GET https://example.com/wp-json/wc/v3/payments/deposits/overview \ "currency": "usd", "fee": 0, "fee_percentage": 1.5, - "net": 0, - "transaction_ids": [] + "net": 0 }, "account": { "deposits_disabled": false, @@ -351,8 +296,8 @@ Fetch a list of deposits. - `date_before` _string_ - `date_after` _string_ - `date_between` _array_ -- `status_is` _string_ `paid` `pending` `in_transit` `canceled` `failed` `estimated` -- `status_is_not` _string_ `paid` `pending` `in_transit` `canceled` `failed` `estimated` +- `status_is` _string_ `paid` `pending` `in_transit` `canceled` `failed` +- `status_is_not` _string_ `paid` `pending` `in_transit` `canceled` `failed` - `direction` _string_ - `page` _integer_ - `pagesize` _integer_ @@ -372,19 +317,6 @@ curl -X GET https://example.com/wp-json/wc/v3/payments/deposits?sort=date \ ```json { "data": [ - { - "id": "wcpay_estimated_weekly_eur_1702598400", - "date": 1702598400000, - "type": "deposit", - "amount": 458784, - "status": "estimated", - "bankAccount": "STRIPE TEST BANK •••• 3000 (EUR)", - "currency": "eur", - "automatic": true, - "fee": 0, - "fee_percentage": 0, - "created": 1702598400 - }, { "id": "po_1OJ466CBu6Jj8nBr38JRxdNE", "date": 1701648000000, @@ -412,7 +344,7 @@ curl -X GET https://example.com/wp-json/wc/v3/payments/deposits?sort=date \ "created": 1701302400 } ], - "total_count": 3 + "total_count": 2 } ``` @@ -438,8 +370,8 @@ Useful in combination with the **List deposits** endpoint to get a summary of de - `date_before` _string_ - `date_after` _string_ - `date_between` _array_ -- `status_is` _string_ - `paid` `pending` `in_transit` `canceled` `failed` `estimated` -- `status_is_not` _string_ - `paid` `pending` `in_transit` `canceled` `failed` `estimated` +- `status_is` _string_ - `paid` `pending` `in_transit` `canceled` `failed` +- `status_is_not` _string_ - `paid` `pending` `in_transit` `canceled` `failed` ### Returns @@ -481,7 +413,7 @@ Fetches a deposit by ID. If a deposit is found for the provided ID, the response will return a [**Deposit**](#deposit-object) object. -If no deposit is found for the provided ID, the response will be an empty array. +If no deposit is found for the provided ID, the response will return a `500` status code. ```shell curl -X GET https://example.com/wp-json/wc/v3/payments/deposits/po_123abc \ @@ -522,17 +454,14 @@ Submit an instant deposit for a list of transactions. Only for eligible accounts ### Required body properties - `type`: _string_ - The type of deposit. `instant` -- `transaction_ids`: _array_ - The list of transaction IDs to deposit. +- `currency`: _string_ - The currency of the balance to deposit. E.g. `usd` ```shell curl -X POST 'https://example.com/wp-json/wc/v3/payments/deposits' \ -u consumer_key:consumer_secret --data '{ "type": "instant", - "transaction_ids": [ - "txn_3OHyIxCIHGKp1UAi0aVyDQ5D", - "txn_3OJSuOCIHGKp1UAi1mRA2lL5" - ] + "currency": "usd" }' ``` @@ -560,8 +489,8 @@ Request a CSV export of deposits matching the query. A link to the exported CSV - `date_before` _string_ - `date_after` _string_ - `date_between` _array_ -- `status_is` _string_ - `paid` `pending` `in_transit` `canceled` `failed` `estimated` -- `status_is_not` _string_ - `paid` `pending` `in_transit` `canceled` `failed` `estimated` +- `status_is` _string_ - `paid` `pending` `in_transit` `canceled` `failed` +- `status_is_not` _string_ - `paid` `pending` `in_transit` `canceled` `failed` ### Returns @@ -571,7 +500,7 @@ Request a CSV export of deposits matching the query. A link to the exported CSV curl -X POST 'https://example.com/wp-json/wc/v3/payments/deposits/download?status_is=paid' \ -u consumer_key:consumer_secret --data '{ - "user_email": "name@example.woo.com", + "user_email": "name@example.woo.com" }' ``` From 6f0f602e4b16cf1517d2cbd8aef6c81aac57207d Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Fri, 26 Jan 2024 09:28:32 +1000 Subject: [PATCH 16/52] Remove redundant estimated deposit code (#8057) Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> Co-authored-by: Rua Haszard --- ...0-remove-redundant-estimated-deposits-code | 5 ++++ .../account-balances/test/index.test.tsx | 1 - client/components/deposits-overview/hooks.ts | 1 - .../deposits-overview/test/index.tsx | 27 +++-------------- client/data/deposits/hooks.ts | 12 -------- client/data/deposits/selectors.js | 2 -- .../data/deposits/test/overviews.fixture.json | 29 ------------------- client/data/deposits/test/reducer.js | 1 - client/data/deposits/test/resolvers.js | 7 ++--- client/data/deposits/test/selectors.js | 3 -- client/payment-details/timeline/map-events.js | 4 +-- client/transactions/list/deposit.tsx | 6 +--- client/types/account-overview.d.ts | 2 -- .../captured-payments/foreign-card.json | 5 +--- .../captured-payments/fx-decimal.json | 5 +--- .../captured-payments/fx-foreign-card.json | 5 +--- .../captured-payments/fx-with-capped-fee.json | 5 +--- tests/fixtures/captured-payments/fx.json | 5 +--- .../captured-payments/only-base-fee.json | 5 +--- .../captured-payments/subscription.json | 5 +--- ...yments-reports-transactions-controller.php | 16 +++++----- 21 files changed, 30 insertions(+), 121 deletions(-) create mode 100644 changelog/fix-7850-remove-redundant-estimated-deposits-code diff --git a/changelog/fix-7850-remove-redundant-estimated-deposits-code b/changelog/fix-7850-remove-redundant-estimated-deposits-code new file mode 100644 index 00000000000..85a82c16321 --- /dev/null +++ b/changelog/fix-7850-remove-redundant-estimated-deposits-code @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Not user-facing: remove redundant code related to decoupling of transactions from deposits + + diff --git a/client/components/account-balances/test/index.test.tsx b/client/components/account-balances/test/index.test.tsx index a491b244747..a3bfa4ec9bd 100644 --- a/client/components/account-balances/test/index.test.tsx +++ b/client/components/account-balances/test/index.test.tsx @@ -138,7 +138,6 @@ const createMockOverview = ( fee_percentage: 0, status: 'paid', }, - nextScheduled: undefined, instant: { currency: currencyCode, amount: instantAmount, diff --git a/client/components/deposits-overview/hooks.ts b/client/components/deposits-overview/hooks.ts index 7dfb5a5a5ac..34c1ea75fee 100644 --- a/client/components/deposits-overview/hooks.ts +++ b/client/components/deposits-overview/hooks.ts @@ -11,7 +11,6 @@ interface RecentDeposits { const useRecentDeposits = ( currency?: string ): RecentDeposits => { const query = { - status_is_not: 'estimated', store_currency_is: currency, orderby: 'date', order: 'desc', diff --git a/client/components/deposits-overview/test/index.tsx b/client/components/deposits-overview/test/index.tsx index 493f01223c5..87cbf56552b 100644 --- a/client/components/deposits-overview/test/index.tsx +++ b/client/components/deposits-overview/test/index.tsx @@ -20,7 +20,7 @@ import { useDeposits, useAllDepositsOverviews, } from 'wcpay/data'; -import type { CachedDeposit, DepositStatus } from 'wcpay/types/deposits'; +import type { CachedDeposit } from 'wcpay/types/deposits'; import type * as AccountOverview from 'wcpay/types/account-overview'; jest.mock( 'wcpay/data', () => ( { @@ -87,10 +87,7 @@ const mockDeposits = [ // Creates a mock Overview object for the given currency code and balance amounts. const createMockOverview = ( - currencyCode: string, - depositAmount: number, - depositDate: number, - depositStatus: DepositStatus + currencyCode: string ): AccountOverview.Overview => { return { currency: currencyCode, @@ -117,19 +114,6 @@ const createMockOverview = ( fee_percentage: 0, status: 'paid', }, - nextScheduled: { - id: '456', - type: 'deposit', - amount: depositAmount, - automatic: true, - currency: currencyCode, - bankAccount: null, - created: Date.now(), - date: depositDate, - fee: 0, - fee_percentage: 0, - status: depositStatus, - }, instant: { currency: currencyCode, amount: 0, @@ -158,7 +142,6 @@ const createMockNewAccountOverview = ( source_types: [], }, lastPaid: undefined, - nextScheduled: undefined, instant: undefined, }; }; @@ -249,7 +232,7 @@ describe( 'Deposits Overview information', () => { } ); test( 'Component Renders', () => { - mockOverviews( [ createMockOverview( 'usd', 100, 0, 'pending' ) ] ); + mockOverviews( [ createMockOverview( 'usd' ) ] ); mockUseDeposits.mockReturnValue( { depositsCount: 0, deposits: mockDeposits, @@ -310,9 +293,7 @@ describe( 'Deposits Overview information', () => { test( 'Confirm notice renders if deposits blocked', () => { mockAccount.deposits_blocked = true; - mockOverviews( [ - createMockOverview( 'usd', 30000, 50000, 'pending' ), - ] ); + mockOverviews( [ createMockOverview( 'usd' ) ] ); mockUseDeposits.mockReturnValue( { depositsCount: 0, deposits: mockDeposits, diff --git a/client/data/deposits/hooks.ts b/client/data/deposits/hooks.ts index 7a6178050bf..3b6c25aa910 100644 --- a/client/data/deposits/hooks.ts +++ b/client/data/deposits/hooks.ts @@ -129,12 +129,6 @@ export const useDeposits = ( { status_is: statusIs, status_is_not: statusIsNot, }: Query ): CachedDeposits => { - // Temporarily default to excluding estimated deposits. - // Client components can (temporarily) opt-in by passing `status_is=estimated`. - // When we remove estimated deposits from server / APIs we can remove this default. - if ( ! statusIsNot && statusIs !== 'estimated' ) { - statusIsNot = 'estimated'; - } return useSelect( ( select ) => { const { @@ -197,12 +191,6 @@ export const useDepositsSummary = ( { status_is: statusIs, status_is_not: statusIsNot, }: Query ): DepositsSummaryCache => { - // Temporarily default to excluding estimated deposits. - // Client components can (temporarily) opt-in by passing `status_is=estimated`. - // When we remove estimated deposits from server / APIs we can remove this default. - if ( ! statusIsNot && statusIs !== 'estimated' ) { - statusIsNot = 'estimated'; - } return useSelect( ( select ) => { const { getDepositsSummary, isResolving } = select( STORE_NAME ); diff --git a/client/data/deposits/selectors.js b/client/data/deposits/selectors.js index 68b99935577..8c263237b19 100644 --- a/client/data/deposits/selectors.js +++ b/client/data/deposits/selectors.js @@ -67,7 +67,6 @@ export const getAllDepositsOverviews = ( state ) => { const groups = { lastPaid: deposit.last_paid, - nextScheduled: deposit.next_scheduled, pending: balance.pending, available: balance.available, instant: balance.instant, @@ -86,7 +85,6 @@ export const getAllDepositsOverviews = ( state ) => { currencies[ currency ] = { currency, lastPaid: undefined, - nextScheduled: undefined, pending: undefined, available: undefined, instant: undefined, diff --git a/client/data/deposits/test/overviews.fixture.json b/client/data/deposits/test/overviews.fixture.json index 775953f30cb..a9a8b25d387 100644 --- a/client/data/deposits/test/overviews.fixture.json +++ b/client/data/deposits/test/overviews.fixture.json @@ -27,34 +27,6 @@ "fee_percentage": 0, "created": 1619395200 } - ], - "next_scheduled": [ - { - "id": "wcpay_estimated_weekly_eur_1622678400", - "date": 1622678400000, - "type": "deposit", - "amount": 3343, - "status": "estimated", - "bankAccount": null, - "currency": "eur", - "automatic": true, - "fee": 0, - "fee_percentage": 0, - "created": 1622678400 - }, - { - "id": "wcpay_estimated_weekly_usd_1622678400", - "date": 1622678400000, - "type": "deposit", - "amount": 1656, - "status": "estimated", - "bankAccount": null, - "currency": "usd", - "automatic": true, - "fee": 0, - "fee_percentage": 0, - "created": 1622678400 - } ] }, "balance": { @@ -69,7 +41,6 @@ { "amount": 1656, "currency": "usd", - "deposits_count": 2, "source_types": { "card": 1656 } diff --git a/client/data/deposits/test/reducer.js b/client/data/deposits/test/reducer.js index ae7d5dec714..648843c71e4 100644 --- a/client/data/deposits/test/reducer.js +++ b/client/data/deposits/test/reducer.js @@ -26,7 +26,6 @@ describe( 'Deposits reducer tests', () => { }; const mockOverview = { last_deposit: mockDeposits[ 0 ], - next_deposit: mockDeposits[ 1 ], balance: { object: 'balance' }, deposits_schedule: { interval: 'daily' }, }; diff --git a/client/data/deposits/test/resolvers.js b/client/data/deposits/test/resolvers.js index 1bc2666428c..0d4dd1ea27a 100644 --- a/client/data/deposits/test/resolvers.js +++ b/client/data/deposits/test/resolvers.js @@ -59,14 +59,13 @@ const filterQuery = { dateAfter: '2020-04-29 23:59:59', dateBetween: [ '2020-04-28 00:00:00', '2020-04-29 23:59:59' ], statusIs: 'paid', - statusIsNot: 'estimated', + statusIsNot: 'failed', storeCurrencyIs: 'gbp', }; describe( 'getDepositsOverview resolver', () => { const successfulResponse = { last_deposit: depositsResponse.data[ 0 ], - next_deposit: depositsResponse.data[ 1 ], balance: { pending: { amount: 5500 }, available: { amount: 0 } }, deposits_schedule: { interval: 'daily' }, }; @@ -149,7 +148,7 @@ describe( 'getDeposits resolver', () => { const expectedQueryString = // eslint-disable-next-line max-len - 'page=1&pagesize=25&match=all&store_currency_is=gbp&date_before=2020-04-29%2003%3A59%3A59&date_after=2020-04-29%2004%3A00%3A00&date_between%5B0%5D=2020-04-28%2004%3A00%3A00&date_between%5B1%5D=2020-04-30%2003%3A59%3A59&status_is=paid&status_is_not=estimated'; + 'page=1&pagesize=25&match=all&store_currency_is=gbp&date_before=2020-04-29%2003%3A59%3A59&date_after=2020-04-29%2004%3A00%3A00&date_between%5B0%5D=2020-04-28%2004%3A00%3A00&date_between%5B1%5D=2020-04-30%2003%3A59%3A59&status_is=paid&status_is_not=failed'; beforeEach( () => { generator = getDeposits( query ); @@ -208,7 +207,7 @@ describe( 'getDepositsSummary resolver', () => { const query = filterQuery; const expectedQueryString = // eslint-disable-next-line max-len - 'match=all&store_currency_is=gbp&date_before=2020-04-29%2003%3A59%3A59&date_after=2020-04-29%2004%3A00%3A00&date_between%5B0%5D=2020-04-28%2004%3A00%3A00&date_between%5B1%5D=2020-04-30%2003%3A59%3A59&status_is=paid&status_is_not=estimated'; + 'match=all&store_currency_is=gbp&date_before=2020-04-29%2003%3A59%3A59&date_after=2020-04-29%2004%3A00%3A00&date_between%5B0%5D=2020-04-28%2004%3A00%3A00&date_between%5B1%5D=2020-04-30%2003%3A59%3A59&status_is=paid&status_is_not=failed'; let generator = null; beforeEach( () => { diff --git a/client/data/deposits/test/selectors.js b/client/data/deposits/test/selectors.js index cd8613a0758..f2d1646065d 100644 --- a/client/data/deposits/test/selectors.js +++ b/client/data/deposits/test/selectors.js @@ -126,7 +126,6 @@ describe( 'Deposits overview selectors', () => { overview: { data: { last_deposit: null, - next_deposit: null, balance: { object: 'balance' }, deposits_schedule: { interval: 'daily' }, }, @@ -221,13 +220,11 @@ describe( 'Deposits overviews selectors', () => { // Check the grouping checkResult( first.lastPaid, 'deposit.last_paid', first ); - checkResult( first.nextScheduled, 'deposit.next_scheduled', first ); checkResult( first.pending, 'balance.pending', first ); checkResult( first.available, 'balance.available', first ); checkResult( first.instant, 'balance.instant', first ); checkResult( second.lastPaid, 'deposit.last_paid', second ); - checkResult( second.nextScheduled, 'deposit.next_scheduled', second ); checkResult( second.pending, 'balance.pending', second ); checkResult( second.available, 'balance.available', second ); checkResult( second.instant, 'balance.instant', second ); diff --git a/client/payment-details/timeline/map-events.js b/client/payment-details/timeline/map-events.js index 49b74106f6d..dca250daff4 100644 --- a/client/payment-details/timeline/map-events.js +++ b/client/payment-details/timeline/map-events.js @@ -70,7 +70,7 @@ const getDepositTimelineItem = ( body = [] ) => { let headline = ''; - if ( event.deposit && ! event.deposit.id.includes( 'wcpay_estimated_' ) ) { + if ( event.deposit ) { headline = sprintf( isPositive ? // translators: %1$s - formatted amount, %2$s - deposit arrival date, - link to the deposit @@ -135,7 +135,7 @@ const getDepositTimelineItem = ( */ const getFinancingPaydownTimelineItem = ( event, formattedAmount, body ) => { let headline = ''; - if ( event.deposit && ! event.deposit.id.includes( 'wcpay_estimated_' ) ) { + if ( event.deposit ) { headline = sprintf( // translators: %1$s - formatted amount, %2$s - deposit arrival date, - link to the deposit __( diff --git a/client/transactions/list/deposit.tsx b/client/transactions/list/deposit.tsx index 09ceb108ca9..588f8f84142 100644 --- a/client/transactions/list/deposit.tsx +++ b/client/transactions/list/deposit.tsx @@ -24,11 +24,7 @@ interface DepositProps { } const Deposit: React.FC< DepositProps > = ( { depositId, dateAvailable } ) => { - if ( - depositId && - dateAvailable && - ! depositId.includes( 'wcpay_estimated_' ) - ) { + if ( depositId && dateAvailable ) { const depositUrl = getAdminUrl( { page: 'wc-admin', path: '/payments/deposits/details', diff --git a/client/types/account-overview.d.ts b/client/types/account-overview.d.ts index 7b02ef2a9c2..0deeab816a8 100644 --- a/client/types/account-overview.d.ts +++ b/client/types/account-overview.d.ts @@ -30,7 +30,6 @@ export interface Account { export interface Balance { amount: number; currency: string; - deposits_count?: number; source_types: Record< string, never >[]; } @@ -59,7 +58,6 @@ export interface InstantBalance { export interface Overview { currency: string; lastPaid: Deposit | undefined; - nextScheduled: Deposit | undefined; pending: Balance | undefined; available: Balance | undefined; instant: InstantBalance | undefined; diff --git a/tests/fixtures/captured-payments/foreign-card.json b/tests/fixtures/captured-payments/foreign-card.json index 9a121e3a7ed..50dc975b029 100644 --- a/tests/fixtures/captured-payments/foreign-card.json +++ b/tests/fixtures/captured-payments/foreign-card.json @@ -33,10 +33,7 @@ }, "currency": "CAD", "datetime": 1651997332, - "deposit": { - "id": "wcpay_estimated_daily_usd_1652572800", - "arrival_date": "1652572800" - }, + "deposit": null, "transaction_id": "txn_3Kx5Ae2EFxam75ai0P2BCbp0", "transaction_details": { "customer_currency": "CAD", diff --git a/tests/fixtures/captured-payments/fx-decimal.json b/tests/fixtures/captured-payments/fx-decimal.json index 77a136924f8..52d02537e81 100644 --- a/tests/fixtures/captured-payments/fx-decimal.json +++ b/tests/fixtures/captured-payments/fx-decimal.json @@ -26,10 +26,7 @@ }, "currency": "EUR", "datetime": 1651215495, - "deposit": { - "id": "wcpay_estimated_daily_usd_1651795200", - "arrival_date": "1651795200" - }, + "deposit": null, "transaction_id": "txn_3Ktnm22EFxam75ai0Sr9SR5A", "transaction_details": { "customer_currency": "EUR", diff --git a/tests/fixtures/captured-payments/fx-foreign-card.json b/tests/fixtures/captured-payments/fx-foreign-card.json index 9b26ad48fc1..846353f7e24 100644 --- a/tests/fixtures/captured-payments/fx-foreign-card.json +++ b/tests/fixtures/captured-payments/fx-foreign-card.json @@ -27,10 +27,7 @@ }, "currency": "USD", "datetime": 1651996460, - "deposit": { - "id": "wcpay_estimated_daily_usd_1652572800", - "arrival_date": "1652572800" - }, + "deposit": null, "transaction_id": "txn_3Kx4w12EFxam75ai0W5q4669", "transaction_details": { "customer_currency": "USD", diff --git a/tests/fixtures/captured-payments/fx-with-capped-fee.json b/tests/fixtures/captured-payments/fx-with-capped-fee.json index 23c3cbf1387..7d3c354ac95 100644 --- a/tests/fixtures/captured-payments/fx-with-capped-fee.json +++ b/tests/fixtures/captured-payments/fx-with-capped-fee.json @@ -35,10 +35,7 @@ }, "currency": "EUR", "datetime": 1651998676, - "deposit": { - "id": "wcpay_estimated_weekly_usd_1652659200", - "arrival_date": "1652659200" - }, + "deposit": null, "transaction_id": "txn_3Kx5WG2HDHuit9Eg0ZrUH90z", "transaction_details": { "customer_currency": "EUR", diff --git a/tests/fixtures/captured-payments/fx.json b/tests/fixtures/captured-payments/fx.json index 166deab4a86..7a4c1186d1e 100644 --- a/tests/fixtures/captured-payments/fx.json +++ b/tests/fixtures/captured-payments/fx.json @@ -27,10 +27,7 @@ }, "currency": "VND", "datetime": 1651215552, - "deposit": { - "id": "wcpay_estimated_daily_usd_1651795200", - "arrival_date": "1651795200" - }, + "deposit": null, "transaction_id": "txn_3KtnnI2EFxam75ai06EEZYXQ", "transaction_details": { "customer_currency": "VND", diff --git a/tests/fixtures/captured-payments/only-base-fee.json b/tests/fixtures/captured-payments/only-base-fee.json index 0385bd61c6f..a84920fd3b3 100644 --- a/tests/fixtures/captured-payments/only-base-fee.json +++ b/tests/fixtures/captured-payments/only-base-fee.json @@ -19,10 +19,7 @@ }, "currency": "USD", "datetime": 1651997740, - "deposit": { - "id": "wcpay_estimated_daily_usd_1652572800", - "arrival_date": "1652572800" - }, + "deposit": null, "transaction_id": "txn_3Kx5HD2EFxam75ai1FerGksO", "transaction_details": { "customer_currency": "USD", diff --git a/tests/fixtures/captured-payments/subscription.json b/tests/fixtures/captured-payments/subscription.json index a9a4d01eae4..bf58a36666f 100644 --- a/tests/fixtures/captured-payments/subscription.json +++ b/tests/fixtures/captured-payments/subscription.json @@ -34,10 +34,7 @@ }, "currency": "EUR", "datetime": 1651999249, - "deposit": { - "id": "wcpay_estimated_weekly_usd_1652659200", - "arrival_date": "1652659200" - }, + "deposit": null, "transaction_id": "txn_3Kx5fY2HDHuit9Eg0fQ402Ee", "transaction_details": { "customer_currency": "EUR", diff --git a/tests/unit/reports/test-class-wc-rest-payments-reports-transactions-controller.php b/tests/unit/reports/test-class-wc-rest-payments-reports-transactions-controller.php index 46c2b21452c..260f3c62057 100644 --- a/tests/unit/reports/test-class-wc-rest-payments-reports-transactions-controller.php +++ b/tests/unit/reports/test-class-wc-rest-payments-reports-transactions-controller.php @@ -208,7 +208,7 @@ private function get_transactions_list_from_server() { 'currency' => 'usd', 'risk_level' => 0, 'charge_id' => 'ch_3NVXQQR7Mcmd7SUg0eV2k74L', - 'deposit_id' => 'wcpay_estimated_daily_usd_1689897600', + 'deposit_id' => null, 'available_on' => '2023-07-21', 'exchange_rate' => 1.12284, 'customer_amount' => 2300, @@ -217,7 +217,7 @@ private function get_transactions_list_from_server() { 'amount_in_usd' => 2583, 'source_device' => null, 'channel' => null, - 'deposit_status' => 'estimated', + 'deposit_status' => null, 'order' => [ 'number' => '123', 'url' => 'https:\/\/wcpay.test\/wp-admin\/post.php?post=278&action=edit', @@ -241,7 +241,7 @@ private function get_transactions_list_from_server() { 'currency' => 'usd', 'risk_level' => 0, 'charge_id' => 'ch_3NVXQER7Mcmd7SUg1Mk9SsNy', - 'deposit_id' => 'wcpay_estimated_daily_usd_1689897600', + 'deposit_id' => null, 'available_on' => '2023-07-21', 'exchange_rate' => 1.12284, 'customer_amount' => 2300, @@ -250,7 +250,7 @@ private function get_transactions_list_from_server() { 'amount_in_usd' => 2583, 'source_device' => null, 'channel' => null, - 'deposit_status' => 'estimated', + 'deposit_status' => null, 'order' => [ 'number' => '275', 'url' => 'https:\/\/wcpay.test\/wp-admin\/post.php?post=275&action=edit', @@ -289,8 +289,8 @@ private function get_transactions_list() { 'order_id' => 123, 'risk_level' => 0, 'deposit_date' => '2023-07-21', - 'deposit_id' => 'wcpay_estimated_daily_usd_1689897600', - 'deposit_status' => 'estimated', + 'deposit_id' => null, + 'deposit_status' => null, ], [ 'transaction_id' => 'txn_345', @@ -315,8 +315,8 @@ private function get_transactions_list() { 'order_id' => 275, 'risk_level' => 0, 'deposit_date' => '2023-07-21', - 'deposit_id' => 'wcpay_estimated_daily_usd_1689897600', - 'deposit_status' => 'estimated', + 'deposit_id' => null, + 'deposit_status' => null, ], ]; } From 33cdb7d7dd87619a172ab4d37dbf2ea4e6bf90ce Mon Sep 17 00:00:00 2001 From: Mike Moore Date: Fri, 26 Jan 2024 10:01:33 -0500 Subject: [PATCH 17/52] Refactor: Consolidate Express Checkout Methods into Single Helper Class (#8055) Co-authored-by: Timur Karimov --- ...s-checkout-consolidate-duplicate-functions | 4 + dev/phpcs/ruleset.xml | 2 +- ...ayments-payment-request-button-handler.php | 180 +++------------- ...lass-wc-payments-woopay-button-handler.php | 193 ++++-------------- includes/class-wc-payments.php | 10 +- ...xpress-checkout-button-display-handler.php | 6 +- ...ayments-express-checkout-button-helper.php | 157 +++++++++++++- ...xpress-checkout-button-display-handler.php | 29 ++- ...ayments-express-checkout-button-helper.php | 152 ++++++++++++++ ...ayments-payment-request-button-handler.php | 59 +++--- ...lass-wc-payments-woopay-button-handler.php | 96 ++++++--- 11 files changed, 512 insertions(+), 376 deletions(-) create mode 100644 changelog/1545-express-checkout-consolidate-duplicate-functions rename includes/{ => express-checkout}/class-wc-payments-express-checkout-button-display-handler.php (96%) create mode 100644 tests/unit/test-class-wc-payments-express-checkout-button-helper.php diff --git a/changelog/1545-express-checkout-consolidate-duplicate-functions b/changelog/1545-express-checkout-consolidate-duplicate-functions new file mode 100644 index 00000000000..2c9592be3e7 --- /dev/null +++ b/changelog/1545-express-checkout-consolidate-duplicate-functions @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Merge duplicated Payment Request and WooPay button functionality . diff --git a/dev/phpcs/ruleset.xml b/dev/phpcs/ruleset.xml index 21b9b1fa970..27f2329cbb6 100644 --- a/dev/phpcs/ruleset.xml +++ b/dev/phpcs/ruleset.xml @@ -17,7 +17,7 @@ */includes/class-wc-payments-apple-pay-registration.php -*/includes/class-wc-payments-express-checkout-button-display-handler.php +*/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php */includes/class-wc-payments-customer-service.php diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index 135ea785bd4..8463e1aed89 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -23,6 +23,8 @@ * WC_Payments_Payment_Request_Button_Handler class. */ class WC_Payments_Payment_Request_Button_Handler { + const BUTTON_LOCATIONS = 'payment_request_button_locations'; + /** * WC_Payments_Account instance to get information about the account * @@ -154,7 +156,7 @@ public function set_session() { // Don't set session cookies on product pages to allow for caching when payment request // buttons are disabled. But keep cookies if there is already an active WC session in place. if ( - ! ( $this->is_product() && $this->should_show_payment_request_button() ) + ! ( $this->express_checkout_helper->is_product() && $this->should_show_payment_request_button() ) || ( isset( WC()->session ) && WC()->session->has_session() ) ) { return; @@ -182,22 +184,20 @@ public function handle_payment_request_redirect() { } /** - * Gets the button height. + * The settings for the `button` attribute - they depend on the "grouped settings" flag value. * - * @return string + * @return array */ - public function get_button_height() { - $height = $this->gateway->get_option( 'payment_request_button_size' ); - if ( 'medium' === $height ) { - return '48'; - } - - if ( 'large' === $height ) { - return '56'; - } + public function get_button_settings() { + $button_type = $this->gateway->get_option( 'payment_request_button_type' ); + $common_settings = $this->express_checkout_helper->get_common_button_settings(); + $payment_request_button_settings = [ + // Default format is en_US. + 'locale' => apply_filters( 'wcpay_payment_request_button_locale', substr( get_locale(), 0, 2 ) ), + 'branded_type' => 'default' === $button_type ? 'short' : 'long', + ]; - // for the "default"/"small" and "catch-all" scenarios. - return '40'; + return array_merge( $common_settings, $payment_request_button_settings ); } /** @@ -246,12 +246,12 @@ public function get_product_price( $product ) { * @return mixed Returns false if not on a product page, the product information otherwise. */ public function get_product_data() { - if ( ! $this->is_product() ) { + if ( ! $this->express_checkout_helper->is_product() ) { return false; } /** @var WC_Product_Variable $product */ // phpcs:ignore - $product = $this->get_product(); + $product = $this->express_checkout_helper->get_product(); $currency = get_woocommerce_currency(); if ( 'variable' === $product->get_type() || 'variable-subscription' === $product->get_type() ) { @@ -395,7 +395,7 @@ public function display_pay_for_order_page_html( $order ) { * @return mixed Returns false if on a product page, the product information otherwise. */ public function get_cart_data() { - if ( $this->is_product() ) { + if ( $this->express_checkout_helper->is_product() ) { return false; } @@ -504,47 +504,47 @@ public function should_show_payment_request_button() { } // Page not supported. - if ( ! $this->is_product() && ! $this->is_cart() && ! $this->is_checkout() ) { + if ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_checkout() ) { return false; } // Product page, but not available in settings. - if ( $this->is_product() && ! $this->is_available_at( 'product' ) ) { + if ( $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_available_at( 'product', self::BUTTON_LOCATIONS ) ) { return false; } // Checkout page, but not available in settings. - if ( $this->is_checkout() && ! $this->is_available_at( 'checkout' ) ) { + if ( $this->express_checkout_helper->is_checkout() && ! $this->express_checkout_helper->is_available_at( 'checkout', self::BUTTON_LOCATIONS ) ) { return false; } // Cart page, but not available in settings. - if ( $this->is_cart() && ! $this->is_available_at( 'cart' ) ) { + if ( $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_available_at( 'cart', self::BUTTON_LOCATIONS ) ) { return false; } // Product page, but has unsupported product type. - if ( $this->is_product() && ! $this->is_product_supported() ) { + if ( $this->express_checkout_helper->is_product() && ! $this->is_product_supported() ) { Logger::log( 'Product page has unsupported product type ( Payment Request button disabled )' ); return false; } // Cart has unsupported product type. - if ( ( $this->is_checkout() || $this->is_cart() ) && ! $this->has_allowed_items_in_cart() ) { + if ( ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) && ! $this->has_allowed_items_in_cart() ) { Logger::log( 'Items in the cart have unsupported product type ( Payment Request button disabled )' ); return false; } // Order total doesn't matter for Pay for Order page. Thus, this page should always display payment buttons. - if ( $this->is_pay_for_order_page() ) { + if ( $this->express_checkout_helper->is_pay_for_order_page() ) { return true; } // Cart total is 0 or is on product page and product price is 0. // Exclude pay-for-order pages from this check. if ( - ( ! $this->is_product() && ! $this->is_pay_for_order_page() && 0.0 === (float) WC()->cart->get_total( 'edit' ) ) || - ( $this->is_product() && 0.0 === (float) $this->get_product()->get_price() ) + ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_pay_for_order_page() && 0.0 === (float) WC()->cart->get_total( 'edit' ) ) || + ( $this->express_checkout_helper->is_product() && 0.0 === (float) $this->express_checkout_helper->get_product()->get_price() ) ) { Logger::log( 'Order price is 0 ( Payment Request button disabled )' ); @@ -632,12 +632,12 @@ public function has_subscription_product() { return false; } - if ( $this->is_product() ) { - $product = $this->get_product(); + if ( $this->express_checkout_helper->is_product() ) { + $product = $this->express_checkout_helper->get_product(); if ( WC_Subscriptions_Product::is_subscription( $product ) ) { return true; } - } elseif ( $this->is_checkout() || $this->is_cart() ) { + } elseif ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) { if ( WC_Subscriptions_Cart::cart_contains_subscription() ) { return true; } @@ -646,103 +646,6 @@ public function has_subscription_product() { return false; } - /** - * Checks if this is a product page or content contains a product_page shortcode. - * - * @return boolean - */ - public function is_product() { - return is_product() || wc_post_content_has_shortcode( 'product_page' ); - } - - /** - * Checks if this is the Pay for Order page. - * - * @return boolean - */ - public function is_pay_for_order_page() { - return is_checkout() && isset( $_GET['pay_for_order'] ); // phpcs:ignore WordPress.Security.NonceVerification - } - - /** - * Checks if this is the cart page or content contains a cart block. - * - * @return boolean - */ - public function is_cart() { - return is_cart() || has_block( 'woocommerce/cart' ); - } - - /** - * Checks if this is the checkout page or content contains a cart block. - * - * @return boolean - */ - public function is_checkout() { - return is_checkout() || has_block( 'woocommerce/checkout' ); - } - - /** - * Checks if payment request is available at a given location. - * - * @param string $location Location. - * @return boolean - */ - public function is_available_at( $location ) { - $available_locations = $this->gateway->get_option( 'payment_request_button_locations' ); - if ( $available_locations && is_array( $available_locations ) ) { - return in_array( $location, $available_locations, true ); - } - - return false; - } - - /** - * Gets the context for where the button is being displayed. - * - * @return string - */ - public function get_button_context() { - if ( $this->is_product() ) { - return 'product'; - } - - if ( $this->is_cart() ) { - return 'cart'; - } - - if ( $this->is_checkout() ) { - return 'checkout'; - } - - if ( $this->is_pay_for_order_page() ) { - return 'pay_for_order'; - } - - return ''; - } - - /** - * Get product from product page or product_page shortcode. - * - * @return WC_Product|false|null Product object. - */ - public function get_product() { - global $post; - - if ( is_product() ) { - return wc_get_product( $post->ID ); - } elseif ( wc_post_content_has_shortcode( 'product_page' ) ) { - // Get id from product_page shortcode. - preg_match( '/\[product_page id="(?\d+)"\]/', $post->post_content, $shortcode_match ); - if ( isset( $shortcode_match['id'] ) ) { - return wc_get_product( $shortcode_match['id'] ); - } - } - - return null; - } - /** * Returns the login redirect URL. * @@ -796,9 +699,9 @@ public function scripts() { ], 'button' => $this->get_button_settings(), 'login_confirmation' => $this->get_login_confirmation_settings(), - 'is_product_page' => $this->is_product(), - 'button_context' => $this->get_button_context(), - 'is_pay_for_order' => $this->is_pay_for_order_page(), + 'is_product_page' => $this->express_checkout_helper->is_product(), + 'button_context' => $this->express_checkout_helper->get_button_context(), + 'is_pay_for_order' => $this->express_checkout_helper->is_pay_for_order_page(), 'has_block' => has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' ), 'product' => $this->get_product_data(), 'total_label' => $this->express_checkout_helper->get_total_label(), @@ -847,7 +750,7 @@ public function display_payment_request_button_html() { * @return boolean */ private function is_product_supported() { - $product = $this->get_product(); + $product = $this->express_checkout_helper->get_product(); $is_supported = true; if ( is_null( $product ) @@ -1514,23 +1417,6 @@ public function get_option_is_apple_pay_enabled( $value ) { return $value; } - /** - * The settings for the `button` attribute - they depend on the "grouped settings" flag value. - * - * @return array - */ - public function get_button_settings() { - $button_type = $this->gateway->get_option( 'payment_request_button_type' ); - return [ - 'type' => $button_type, - 'theme' => $this->gateway->get_option( 'payment_request_button_theme' ), - 'height' => $this->get_button_height(), - // Default format is en_US. - 'locale' => apply_filters( 'wcpay_payment_request_button_locale', substr( get_locale(), 0, 2 ) ), - 'branded_type' => 'default' === $button_type ? 'short' : 'long', - ]; - } - /** * Settings array for the user authentication dialog and redirection. * diff --git a/includes/class-wc-payments-woopay-button-handler.php b/includes/class-wc-payments-woopay-button-handler.php index 5045174a7b3..4f19ffb3f57 100644 --- a/includes/class-wc-payments-woopay-button-handler.php +++ b/includes/class-wc-payments-woopay-button-handler.php @@ -22,6 +22,8 @@ * WC_Payments_WooPay_Button_Handler class. */ class WC_Payments_WooPay_Button_Handler { + const BUTTON_LOCATIONS = 'platform_checkout_button_locations'; + /** * WC_Payments_Account instance to get information about the account * @@ -43,17 +45,26 @@ class WC_Payments_WooPay_Button_Handler { */ private $woopay_utilities; + /** + * Express Checkout Helper instance. + * + * @var WC_Payments_Express_Checkout_Button_Helper + */ + private $express_checkout_helper; + /** * Initialize class actions. * - * @param WC_Payments_Account $account Account information. - * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. - * @param WooPay_Utilities $woopay_utilities WCPay gateway. + * @param WC_Payments_Account $account Account information. + * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. + * @param WooPay_Utilities $woopay_utilities WCPay gateway. + * @param WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper Express checkout helper. */ - public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway, WooPay_Utilities $woopay_utilities ) { - $this->account = $account; - $this->gateway = $gateway; - $this->woopay_utilities = $woopay_utilities; + public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway, WooPay_Utilities $woopay_utilities, WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper ) { + $this->account = $account; + $this->gateway = $gateway; + $this->woopay_utilities = $woopay_utilities; + $this->express_checkout_helper = $express_checkout_helper; } /** @@ -112,11 +123,11 @@ public function init() { } // Create WooPay button location option if it doesn't exist and enable all locations by default. - if ( ! array_key_exists( 'platform_checkout_button_locations', get_option( 'woocommerce_woocommerce_payments_settings' ) ) ) { + if ( ! array_key_exists( self::BUTTON_LOCATIONS, get_option( 'woocommerce_woocommerce_payments_settings' ) ) ) { - $all_locations = $this->gateway->form_fields['platform_checkout_button_locations']['options']; + $all_locations = $this->gateway->form_fields[ self::BUTTON_LOCATIONS ]['options']; - $this->gateway->update_option( 'platform_checkout_button_locations', array_keys( $all_locations ) ); + $this->gateway->update_option( self::BUTTON_LOCATIONS, array_keys( $all_locations ) ); WC_Payments::woopay_tracker()->woopay_locations_updated( $all_locations, array_keys( $all_locations ) ); } @@ -212,115 +223,19 @@ public function show_error_notice() { wp_die(); } - /** - * Checks if this is a product page or content contains a product_page shortcode. - * - * @return boolean - */ - public function is_product() { - return is_product() || wc_post_content_has_shortcode( 'product_page' ); - } - - /** - * Checks if this is the Pay for Order page. - * - * @return boolean - */ - public function is_pay_for_order_page() { - return is_checkout() && isset( $_GET['pay_for_order'] ); // phpcs:ignore WordPress.Security.NonceVerification - } - - /** - * Checks if this is the cart page or content contains a cart block. - * - * @return boolean - */ - public function is_cart() { - return is_cart() || has_block( 'woocommerce/cart' ); - } - - /** - * Checks if this is the checkout page or content contains a cart block. - * - * @return boolean - */ - public function is_checkout() { - return is_checkout() || has_block( 'woocommerce/checkout' ); - } - - /** - * Checks if payment request is available at a given location. - * - * @param string $location Location. - * @return boolean - */ - public function is_available_at( $location ) { - $available_locations = $this->gateway->get_option( 'platform_checkout_button_locations' ); - if ( $available_locations && is_array( $available_locations ) ) { - return in_array( $location, $available_locations, true ); - } - - return false; - } - - /** - * Gets the context for where the button is being displayed. - * - * @return string - */ - public function get_button_context() { - if ( $this->is_product() ) { - return 'product'; - } - - if ( $this->is_cart() ) { - return 'cart'; - } - - if ( $this->is_pay_for_order_page() ) { - return 'pay_for_order'; - } - - if ( $this->is_checkout() ) { - return 'checkout'; - } - - return ''; - } - /** * The settings for the `button` attribute - they depend on the "grouped settings" flag value. * * @return array */ public function get_button_settings() { - $button_type = $this->gateway->get_option( 'payment_request_button_type', 'default' ); - return [ - 'type' => $button_type, - 'theme' => $this->gateway->get_option( 'payment_request_button_theme', 'dark' ), - 'height' => $this->get_button_height(), + $common_settings = $this->express_checkout_helper->get_common_button_settings(); + $woopay_button_settings = [ 'size' => $this->gateway->get_option( 'payment_request_button_size' ), - 'context' => $this->get_button_context(), + 'context' => $this->express_checkout_helper->get_button_context(), ]; - } - - /** - * Gets the button height. - * - * @return string - */ - public function get_button_height() { - $height = $this->gateway->get_option( 'payment_request_button_size' ); - if ( 'medium' === $height ) { - return '48'; - } - if ( 'large' === $height ) { - return '56'; - } - - // for the "default" and "catch-all" scenarios. - return '40'; + return array_merge( $common_settings, $woopay_button_settings ); } /** @@ -341,7 +256,7 @@ public function should_show_woopay_button() { } // Page not supported. - if ( ! $this->is_product() && ! $this->is_cart() && ! $this->is_checkout() ) { + if ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_checkout() ) { return false; } @@ -351,44 +266,44 @@ public function should_show_woopay_button() { } // Product page, but not available in settings. - if ( $this->is_product() && ! $this->is_available_at( 'product' ) ) { + if ( $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_available_at( 'product', self::BUTTON_LOCATIONS ) ) { return false; } // Checkout page, but not available in settings. - if ( $this->is_checkout() && ! $this->is_available_at( 'checkout' ) ) { + if ( $this->express_checkout_helper->is_checkout() && ! $this->express_checkout_helper->is_available_at( 'checkout', self::BUTTON_LOCATIONS ) ) { return false; } // Cart page, but not available in settings. - if ( $this->is_cart() && ! $this->is_available_at( 'cart' ) ) { + if ( $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_available_at( 'cart', self::BUTTON_LOCATIONS ) ) { return false; } // Product page, but has unsupported product type. - if ( $this->is_product() && ! $this->is_product_supported() ) { + if ( $this->express_checkout_helper->is_product() && ! $this->is_product_supported() ) { Logger::log( 'Product page has unsupported product type ( WooPay Express button disabled )' ); return false; } // Cart has unsupported product type. - if ( ( $this->is_checkout() || $this->is_cart() ) && ! $this->has_allowed_items_in_cart() ) { + if ( ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) && ! $this->has_allowed_items_in_cart() ) { Logger::log( 'Items in the cart have unsupported product type ( WooPay Express button disabled )' ); return false; } if ( ! is_user_logged_in() ) { // On product page for a subscription product, but not logged in, making WooPay unavailable. - if ( $this->is_product() ) { + if ( $this->express_checkout_helper->is_product() ) { $current_product = wc_get_product(); - if ( $current_product && $this->is_product_subscription( $current_product ) ) { + if ( $current_product && $this->express_checkout_helper->is_product_subscription( $current_product ) ) { return false; } } // On cart or checkout page with a subscription product in cart, but not logged in, making WooPay unavailable. - if ( ( $this->is_checkout() || $this->is_cart() ) && class_exists( 'WC_Subscriptions_Cart' ) && WC_Subscriptions_Cart::cart_contains_subscription() ) { + if ( ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) && class_exists( 'WC_Subscriptions_Cart' ) && WC_Subscriptions_Cart::cart_contains_subscription() ) { // Check cart for subscription products. return false; } @@ -419,7 +334,7 @@ public function display_woopay_button_html() { $settings = $this->get_button_settings(); ?> - - Payment ID: - ch_38jdHA39KKA ++ + Payment ID: + + + pi_abc + +++ + Charge ID: + + + ch_38jdHA39KKA + +- Payment ID: - ch_38jdHA39KKA ++ + Payment ID: + + + pi_abc + +++ + Charge ID: + + + ch_38jdHA39KKA + +- Payment ID: - ch_38jdHA39KKA +++ + Payment ID: + + + pi_abc + +++ + Charge ID: + + + ch_38jdHA39KKA + ++++ + + +
+++ + + + + +`; + +exports[`PaymentDetailsSummary correctly renders when payment intent is missing 1`] = ` ++
+- +
++++ + Date + + + Sep 19, 2019, 5:24pm + ++- +
++++ + Channel + + + + Online + + ++- +
++++ + Customer + + + + Customer name + + ++- +
++++ + Order + + + + 45981 + + ++- +
++++ + Payment method + + + +++ + •••• + 4242 + + ++ ++- +
++++ + Risk evaluation + + + Normal + +++@@ -1925,8 +2388,34 @@ exports[`PaymentDetailsSummary renders fully refunded information for a charge 1+@@ -1600,8 +2037,34 @@ exports[`PaymentDetailsSummary renders a charge with subscriptions 1`] = `++@@ -1282,8 +1693,34 @@ exports[`PaymentDetailsSummary order missing notice renders notice if order miss++@@ -987,8 +1372,34 @@ exports[`PaymentDetailsSummary order missing notice does not render notice if or+++ $20.00 + + usd + + + Paid + +
++ +++ Fees: + -$0.70 +
+ ++ Net: + $19.30 +
++++ + Charge ID: + + + ch_38jdHA39KKA + +- Payment ID: - ch_38jdHA39KKA ++ + Payment ID: + + + pi_abc + +++ + Charge ID: + + + ch_38jdHA39KKA + +- Payment ID: - ch_38jdHA39KKA ++ + Payment ID: + + + pi_abc + +++ + Charge ID: + + + ch_38jdHA39KKA + +- Payment ID: - ch_38jdHA39KKA ++ + Payment ID: + + + pi_abc + +++ + Charge ID: + + + ch_38jdHA39KKA + +- Payment ID: - ch_38jdHA39KKA +@@ -2191,9 +2680,7 @@ exports[`PaymentDetailsSummary renders loading state 1`] = ` >+ + Payment ID: + + + pi_abc + +++ + Charge ID: + + + ch_38jdHA39KKA + +- Payment ID: -+ />- Payment ID: - ch_38jdHA39KKA +@@ -2734,8 +3247,34 @@ exports[`PaymentDetailsSummary renders the Tap to Pay channel from metadata 1`]+ + Payment ID: + + + pi_abc + +++ + Charge ID: + + + ch_38jdHA39KKA + +- Payment ID: - ch_38jdHA39KKA +@@ -3029,8 +3568,34 @@ exports[`PaymentDetailsSummary renders the information of a dispute-reversal cha+ + Payment ID: + + + pi_abc + +++ + Charge ID: + + + ch_38jdHA39KKA + +- Payment ID: - ch_38jdHA39KKA +diff --git a/client/payment-details/summary/test/index.test.tsx b/client/payment-details/summary/test/index.test.tsx index 9cef35c99d2..99c89bdabd0 100755 --- a/client/payment-details/summary/test/index.test.tsx +++ b/client/payment-details/summary/test/index.test.tsx @@ -73,6 +73,7 @@ const mockUseAuthorization = useAuthorization as jest.MockedFunction< const getBaseCharge = (): Charge => ( { id: 'ch_38jdHA39KKA', + payment_intent: 'pi_abc', /* Stripe data comes in seconds, instead of the default Date milliseconds */ created: Date.parse( 'Sep 19, 2019, 5:24 pm' ) / 1000, amount: 2000, @@ -225,6 +226,12 @@ describe( 'PaymentDetailsSummary', () => { ); } ); + test( 'correctly renders when payment intent is missing', () => { + const baseCharge = getBaseCharge(); + baseCharge.payment_intent = null; + expect( renderCharge( baseCharge ) ).toMatchSnapshot(); + } ); + test( 'renders partially refunded information for a charge', () => { const charge = getBaseCharge(); charge.refunded = false; diff --git a/client/payment-details/test/__snapshots__/index.test.tsx.snap b/client/payment-details/test/__snapshots__/index.test.tsx.snap index e09725a4576..0798e4b5d52 100644 --- a/client/payment-details/test/__snapshots__/index.test.tsx.snap +++ b/client/payment-details/test/__snapshots__/index.test.tsx.snap @@ -529,8 +529,34 @@ exports[`Payment details page should match the snapshot - Payment Intent query p+ + Payment ID: + + + pi_abc + +++ + Charge ID: + + + ch_38jdHA39KKA + +- Payment ID: - pi_mock +From 322d3d8d612f3316366fc75195960d8f0c35dec8 Mon Sep 17 00:00:00 2001 From: Timur Karimov+ + Payment ID: + + + pi_mock + +++ + Charge ID: + + + ch_mock + +Date: Mon, 5 Feb 2024 09:28:00 +0100 Subject: [PATCH 30/52] Add Stripe Link support for 3DS payments (#8119) Co-authored-by: Timur Karimov Co-authored-by: Francesco --- changelog/add-3ds-support-for-link | 4 ++++ client/checkout/api/index.js | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 changelog/add-3ds-support-for-link diff --git a/changelog/add-3ds-support-for-link b/changelog/add-3ds-support-for-link new file mode 100644 index 00000000000..dc3ccc23129 --- /dev/null +++ b/changelog/add-3ds-support-for-link @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Support Stripe Link payments with 3DS cards. diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js index 66b60454872..3ccaa9026a8 100644 --- a/client/checkout/api/index.js +++ b/client/checkout/api/index.js @@ -252,9 +252,9 @@ export default class WCPayAPI { // If this is a setup intent we're not processing a woopay payment so we can // use the regular getStripe function. if ( isSetupIntent ) { - return this.getStripe().confirmCardSetup( - decryptClientSecret( clientSecret ) - ); + return this.getStripe().handleNextAction( { + clientSecret: decryptClientSecret( clientSecret ), + } ); } // For woopay we need the capability to switch up the account ID specifically for @@ -274,9 +274,9 @@ export default class WCPayAPI { // When not dealing with a setup intent or woopay we need to force an account // specific request in Stripe. - return this.getStripe( true ).confirmCardPayment( - decryptClientSecret( clientSecret ) - ); + return this.getStripe( true ).handleNextAction( { + clientSecret: decryptClientSecret( clientSecret ), + } ); }; return ( From aa1e97d5d4d6fc42fc6ab51010a38107f8a8ea63 Mon Sep 17 00:00:00 2001 From: Vasily Belolapotkov Date: Mon, 5 Feb 2024 13:25:24 +0400 Subject: [PATCH 31/52] Remove Woo plugin header (#8050) --- changelog/update-translations-loading | 4 + includes/class-wc-payments.php | 2 + .../GenericServiceProvider.php | 6 + .../PluginManagement/TranslationsLoader.php | 173 ++++++++++++++++++ woocommerce-payments.php | 3 - 5 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 changelog/update-translations-loading create mode 100644 src/Internal/PluginManagement/TranslationsLoader.php diff --git a/changelog/update-translations-loading b/changelog/update-translations-loading new file mode 100644 index 00000000000..5f6f4434256 --- /dev/null +++ b/changelog/update-translations-loading @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Stop relying on Woo core for loading plugin translations. diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 444844cda1a..a52898cca96 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -656,6 +656,8 @@ public static function init() { add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_assets_script' ] ); self::$duplicate_payment_prevention_service->init( self::$card_gateway, self::$order_service ); + + wcpay_get_container()->get( \WCPay\Internal\PluginManagement\TranslationsLoader::class )->init_hooks(); } /** diff --git a/src/Internal/DependencyManagement/ServiceProvider/GenericServiceProvider.php b/src/Internal/DependencyManagement/ServiceProvider/GenericServiceProvider.php index 2bb57a967d1..9d11140d15a 100644 --- a/src/Internal/DependencyManagement/ServiceProvider/GenericServiceProvider.php +++ b/src/Internal/DependencyManagement/ServiceProvider/GenericServiceProvider.php @@ -17,6 +17,7 @@ use WCPay\Internal\Service\Level3Service; use WCPay\Internal\Service\OrderService; use WCPay\Internal\Service\SessionService; +use WCPay\Internal\PluginManagement\TranslationsLoader; /** * WCPay payments generic service provider. @@ -31,6 +32,7 @@ class GenericServiceProvider extends AbstractServiceProvider { Logger::class, OrderService::class, Level3Service::class, + TranslationsLoader::class, ]; /** @@ -58,5 +60,9 @@ public function register(): void { $container->addShared( SessionService::class ) ->addArgument( LegacyProxy::class ); + + $container->addShared( TranslationsLoader::class ) + ->addArgument( Logger::class ) + ->addArgument( HooksProxy::class ); } } diff --git a/src/Internal/PluginManagement/TranslationsLoader.php b/src/Internal/PluginManagement/TranslationsLoader.php new file mode 100644 index 00000000000..8232877caea --- /dev/null +++ b/src/Internal/PluginManagement/TranslationsLoader.php @@ -0,0 +1,173 @@ +logger = $logger; + $this->hooks_proxy = $hooks_proxy; + } + + + /** + * Hooks into WordPress plugin update process to load plugin translations from translate.wordpress.com. + */ + public function init_hooks() { + $this->hooks_proxy->add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'load_wcpay_translations' ] ); + } + + /** + * Hooks into auto-update process to load plugin translations from translate.wordpress.com. + * + * Runs in a cron thread, or in a visitor thread if triggered + * by _maybe_update_plugins(), or in an auto-update thread. + * + * @param object $transient The update_plugins transient object. + * + * @return object The same or a modified version of the transient. + */ + public function load_wcpay_translations( $transient ) { + try { + if ( is_object( $transient ) ) { + $translations = $this->get_translations_update_data(); + $merged_translations = array_merge( isset( $transient->translations ) ? $transient->translations : [], $translations ); + $transient->translations = $merged_translations; + } + } catch ( \Exception $ex ) { + $this->logger->error( 'Error with loading WooPayments translations from WordPress.com. Reason: ' . $ex->getMessage() ); + return $transient; + } + return $transient; + } + + /** + * Get translations updates information. + * + * @return array Update data {product_id => data} + * @throws \Exception If something goes wrong with fetching info about translation packages from WordPress.com. + */ + public function get_translations_update_data() { + $installed_translations = wp_get_installed_translations( 'plugins' ); + $locales = array_values( get_available_languages() ); + + if ( empty( $locales ) ) { + return []; + } + + // Use the same timeout values as Woo Core https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php#L257. + $timeout = wp_doing_cron() ? 30 : 3; + + if ( ! function_exists( 'get_plugin_data' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $plugin_data = get_plugin_data( WCPAY_PLUGIN_FILE ); + + /** + * Note: TextDomain could differ from the plugin slug, but WordPress uses TextDomain to load translations. + */ + $plugin_name = $plugin_data['TextDomain']; + + $request_body = [ + 'locales' => $locales, + 'plugins' => [], + ]; + + $request_body['plugins'][ $plugin_name ] = [ + 'version' => WCPAY_VERSION_NUMBER, + ]; + + $raw_response = wp_remote_post( + 'https://translate.wordpress.com/api/translations-updates/woocommerce', + [ + 'body' => wp_json_encode( $request_body ), + 'headers' => [ 'Content-Type: application/json' ], + 'timeout' => $timeout, + ] + ); + + $response_code = wp_remote_retrieve_response_code( $raw_response ); + if ( 200 !== $response_code ) { + $this->logger->debug( + sprintf( 'Raw response: %s', var_export( $raw_response, true ) ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- That's debug message which will only be logged when debuging is enabled. + ); + throw new \Exception( + sprintf( 'Request failed. HTTP response code: %s', $response_code ) + ); + } + + $response = json_decode( wp_remote_retrieve_body( $raw_response ), true ); + + if ( array_key_exists( 'success', $response ) && false === $response['success'] ) { + // The shape of response is not known, so more specific error message can't be provided in exception. Logging the response body for debuggin purposes. + $this->logger->debug( + sprintf( 'Unexpected response body: %s', var_export( $response, true ) ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- That's debug message which will only be logged when debuging is enabled. + ); + throw new \Exception( 'Unexpected response body.' ); + } + + $language_packs = $response['data'][ $plugin_name ]; + + $translations = []; + + foreach ( $language_packs as $language_pack ) { + // Maybe we have this language pack already installed so lets check revision date. + if ( array_key_exists( $plugin_name, $installed_translations ) && array_key_exists( $language_pack['wp_locale'], $installed_translations[ $plugin_name ] ) ) { + $installed_translation_revision_time = new DateTime( $installed_translations[ $plugin_name ][ $language_pack['wp_locale'] ]['PO-Revision-Date'] ); + $new_translation_revision_time = new DateTime( $language_pack['last_modified'] ); + // Skip if translation language pack is not newer than what is installed already. + if ( $new_translation_revision_time <= $installed_translation_revision_time ) { + continue; + } + } + $translations[] = [ + 'type' => 'plugin', + 'slug' => $plugin_name, + 'language' => $language_pack['wp_locale'], + 'version' => $language_pack['version'], + 'updated' => $language_pack['last_modified'], + 'package' => $language_pack['package'], + 'autoupdate' => true, + ]; + } + + return $translations; + } +} diff --git a/woocommerce-payments.php b/woocommerce-payments.php index d1db912b2e6..0d29cd17222 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -5,7 +5,6 @@ * Description: Accept payments via credit card. Manage transactions within WordPress. * Author: Automattic * Author URI: https://woo.com/ - * Woo: 5278104:bf3cf30871604e15eec560c962593c1f * Text Domain: woocommerce-payments * Domain Path: /languages * WC requires at least: 7.6 @@ -66,8 +65,6 @@ function wcpay_deactivated() { return; } -// Subscribe to automated translations. -add_filter( 'woocommerce_translations_updates_for_woocommerce-payments', '__return_true' ); /** * Initialize the Jetpack functionalities: connection, identity crisis, etc. From 7448731cb746d6e6832d389557ba1fad610f03e0 Mon Sep 17 00:00:00 2001 From: Guilherme Pressutto Date: Mon, 5 Feb 2024 13:28:05 -0300 Subject: [PATCH 32/52] Showing "started" event in transaction timeline (#8126) --- changelog/timeline-started-event | 4 ++++ client/payment-details/timeline/map-events.js | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 changelog/timeline-started-event diff --git a/changelog/timeline-started-event b/changelog/timeline-started-event new file mode 100644 index 00000000000..610f8513553 --- /dev/null +++ b/changelog/timeline-started-event @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Showing "started" event in transaction timeline diff --git a/client/payment-details/timeline/map-events.js b/client/payment-details/timeline/map-events.js index dca250daff4..890fb9601f7 100644 --- a/client/payment-details/timeline/map-events.js +++ b/client/payment-details/timeline/map-events.js @@ -605,6 +605,13 @@ const mapEventToTimelineItems = ( event ) => { ); switch ( type ) { + case 'started': + return [ + getStatusChangeTimelineItem( + event, + __( 'Started', 'woocommerce-payments' ) + ), + ]; case 'authorized': return [ getStatusChangeTimelineItem( From 02194d1ce4ad929f4a47782dce71da26da725560 Mon Sep 17 00:00:00 2001 From: Brian Borman <68524302+bborman22@users.noreply.github.com> Date: Mon, 5 Feb 2024 11:49:16 -0500 Subject: [PATCH 33/52] Fix E2E shopper tests around 3DS and UPE settings (#8123) Co-authored-by: Achyuth Ajoy --- changelog/fix-e2e-tests | 4 ++++ tests/e2e/config/default.json | 2 ++ .../shopper-wc-blocks-checkout-failures.spec.js | 2 +- .../shopper-wc-blocks-checkout-purchase.spec.js | 2 +- ...pper-subscriptions-purchase-free-trial.spec.js | 2 +- .../shopper/shopper-checkout-purchase.spec.js | 2 +- ...hopper-checkout-save-card-and-purchase.spec.js | 4 ++-- ...per-myaccount-payment-methods-add-fail.spec.js | 2 +- ...opper-myaccount-save-card-and-checkout.spec.js | 2 +- tests/e2e/utils/flows.js | 14 ++++++++++++-- tests/e2e/utils/payments.js | 15 ++------------- 11 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 changelog/fix-e2e-tests diff --git a/changelog/fix-e2e-tests b/changelog/fix-e2e-tests new file mode 100644 index 00000000000..94af6f93d7b --- /dev/null +++ b/changelog/fix-e2e-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix for E2E shopper tests around 3DS and UPE settings diff --git a/tests/e2e/config/default.json b/tests/e2e/config/default.json index a05ebfd4c2e..2f0277dcf7d 100644 --- a/tests/e2e/config/default.json +++ b/tests/e2e/config/default.json @@ -84,6 +84,7 @@ "addressfirstline": "Rue de l’Étuve, 1000", "addresssecondline": "billing-be", "city": "Bruxelles", + "postcode": "1000", "phone": "123456789", "email": "e2e-wcpay-customer@woo.com" }, @@ -95,6 +96,7 @@ "addressfirstline": "Petuelring 130", "addresssecondline": "billing-de", "city": "München", + "postcode": "80809", "state": "DE-BY", "phone": "123456789", "email": "e2e-wcpay-customer@woo.com" diff --git a/tests/e2e/specs/blocks/shopper/shopper-wc-blocks-checkout-failures.spec.js b/tests/e2e/specs/blocks/shopper/shopper-wc-blocks-checkout-failures.spec.js index 7ac506663e6..0c7cbba9adb 100644 --- a/tests/e2e/specs/blocks/shopper/shopper-wc-blocks-checkout-failures.spec.js +++ b/tests/e2e/specs/blocks/shopper/shopper-wc-blocks-checkout-failures.spec.js @@ -197,7 +197,7 @@ describeif( RUN_WC_BLOCKS_TESTS )( await expect( page ).toClick( 'button > span', { text: 'Place Order', } ); - await confirmCardAuthentication( page, '3DS' ); + await confirmCardAuthentication( page ); await page.waitForSelector( 'div.wc-block-components-notices' ); const declined3dsCardError = await page.$eval( 'div.wc-block-components-notices > div > div.components-notice__content', diff --git a/tests/e2e/specs/blocks/shopper/shopper-wc-blocks-checkout-purchase.spec.js b/tests/e2e/specs/blocks/shopper/shopper-wc-blocks-checkout-purchase.spec.js index 51022e6d92b..f3475df2805 100644 --- a/tests/e2e/specs/blocks/shopper/shopper-wc-blocks-checkout-purchase.spec.js +++ b/tests/e2e/specs/blocks/shopper/shopper-wc-blocks-checkout-purchase.spec.js @@ -77,7 +77,7 @@ describeif( RUN_WC_BLOCKS_TESTS )( '.wc-block-components-main button:not(:disabled)' ); await expect( page ).toClick( 'button', { text: 'Place Order' } ); - await confirmCardAuthentication( page, '3DS' ); + await confirmCardAuthentication( page ); await page.waitForNavigation( { waitUntil: 'networkidle0', } ); diff --git a/tests/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-free-trial.spec.js b/tests/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-free-trial.spec.js index 8a14c1d26a7..fa11e8b715d 100644 --- a/tests/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-free-trial.spec.js +++ b/tests/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-free-trial.spec.js @@ -116,7 +116,7 @@ describeif( RUN_SUBSCRIPTIONS_TESTS )( await expect( page ).toClick( testSelectors.checkoutPlaceOrderButton ); - await confirmCardAuthentication( page, '3DS', true ); + await confirmCardAuthentication( page, true ); await page.waitForNavigation( { waitUntil: 'networkidle0', } ); diff --git a/tests/e2e/specs/wcpay/shopper/shopper-checkout-purchase.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-checkout-purchase.spec.js index e1d871c9b07..ccb189a91ab 100644 --- a/tests/e2e/specs/wcpay/shopper/shopper-checkout-purchase.spec.js +++ b/tests/e2e/specs/wcpay/shopper/shopper-checkout-purchase.spec.js @@ -40,7 +40,7 @@ describe( 'Successful purchase', () => { const card = config.get( 'cards.3ds' ); await fillCardDetails( page, card ); await expect( page ).toClick( '#place_order' ); - await confirmCardAuthentication( page, '3DS' ); + await confirmCardAuthentication( page ); await page.waitForNavigation( { waitUntil: 'networkidle0', } ); diff --git a/tests/e2e/specs/wcpay/shopper/shopper-checkout-save-card-and-purchase.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-checkout-save-card-and-purchase.spec.js index 341b52cf516..cd4b4eeb13f 100644 --- a/tests/e2e/specs/wcpay/shopper/shopper-checkout-save-card-and-purchase.spec.js +++ b/tests/e2e/specs/wcpay/shopper/shopper-checkout-save-card-and-purchase.spec.js @@ -45,7 +45,7 @@ describe( 'Saved cards ', () => { await shopper.placeOrder(); } else { await expect( page ).toClick( '#place_order' ); - await confirmCardAuthentication( page, cardType ); + await confirmCardAuthentication( page ); await page.waitForNavigation( { waitUntil: 'networkidle0', } ); @@ -73,7 +73,7 @@ describe( 'Saved cards ', () => { await shopper.placeOrder(); } else { await expect( page ).toClick( '#place_order' ); - await confirmCardAuthentication( page, cardType ); + await confirmCardAuthentication( page ); await page.waitForNavigation( { waitUntil: 'networkidle0', } ); diff --git a/tests/e2e/specs/wcpay/shopper/shopper-myaccount-payment-methods-add-fail.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-myaccount-payment-methods-add-fail.spec.js index f3e3762f48d..e8fb0a0f79d 100644 --- a/tests/e2e/specs/wcpay/shopper/shopper-myaccount-payment-methods-add-fail.spec.js +++ b/tests/e2e/specs/wcpay/shopper/shopper-myaccount-payment-methods-add-fail.spec.js @@ -50,7 +50,7 @@ describe( 'Payment Methods', () => { text: 'Add payment method', } ); if ( cardType === 'declined-3ds' ) { - await confirmCardAuthentication( page, '3DS2' ); + await confirmCardAuthentication( page ); } await expect( page ).toMatchElement( '.woocommerce-error', { timeout: 30000, diff --git a/tests/e2e/specs/wcpay/shopper/shopper-myaccount-save-card-and-checkout.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-myaccount-save-card-and-checkout.spec.js index 8def9e54824..ddcd3c6586d 100644 --- a/tests/e2e/specs/wcpay/shopper/shopper-myaccount-save-card-and-checkout.spec.js +++ b/tests/e2e/specs/wcpay/shopper/shopper-myaccount-save-card-and-checkout.spec.js @@ -54,7 +54,7 @@ describe( 'Saved cards ', () => { await shopper.placeOrder(); } else { await expect( page ).toClick( '#place_order' ); - await confirmCardAuthentication( page, cardType ); + await confirmCardAuthentication( page ); await page.waitForNavigation( { waitUntil: 'networkidle0', } ); diff --git a/tests/e2e/utils/flows.js b/tests/e2e/utils/flows.js index af662b55fee..10499fe20c3 100644 --- a/tests/e2e/utils/flows.js +++ b/tests/e2e/utils/flows.js @@ -191,7 +191,7 @@ export const shopperWCP = { ! cardType.toLowerCase().includes( 'declined' ); if ( cardIs3DS ) { - await confirmCardAuthentication( page, cardType ); + await confirmCardAuthentication( page ); } await page.waitForNavigation( { @@ -426,7 +426,17 @@ export const merchantWCP = { button.click() ); } - await page.$eval( paymentMethod, ( method ) => method.click() ); + // Check if paymentMethod is an XPath + if ( paymentMethod.startsWith( '//' ) ) { + // Find the element using XPath and click it + const elements = await page.$x( paymentMethod ); + if ( elements.length > 0 ) { + await elements[ 0 ].click(); + } + } else { + // If it's a CSS selector, use $eval + await page.$eval( paymentMethod, ( method ) => method.click() ); + } await expect( page ).toClick( 'button', { text: 'Remove', } ); diff --git a/tests/e2e/utils/payments.js b/tests/e2e/utils/payments.js index 55067231bf5..b789068a9e9 100644 --- a/tests/e2e/utils/payments.js +++ b/tests/e2e/utils/payments.js @@ -177,11 +177,7 @@ export async function clearWCBCardDetails() { await page.keyboard.press( 'Backspace' ); } -export async function confirmCardAuthentication( - page, - cardType = '3DS', - authorize = true -) { +export async function confirmCardAuthentication( page, authorize = true ) { const target = authorize ? '#test-source-authorize-3ds' : '#test-source-fail-3ds'; @@ -195,14 +191,7 @@ export async function confirmCardAuthentication( const challengeFrameHandle = await stripeFrame.waitForSelector( 'iframe#challengeFrame' ); - let challengeFrame = await challengeFrameHandle.contentFrame(); - // 3DS 1 cards have another iframe enclosing the authorize form - if ( cardType.toUpperCase() === '3DS' ) { - const acsFrameHandle = await challengeFrame.waitForSelector( - 'iframe[name="acsFrame"]' - ); - challengeFrame = await acsFrameHandle.contentFrame(); - } + const challengeFrame = await challengeFrameHandle.contentFrame(); // Need to wait for the CSS animations to complete. await page.waitFor( 500 ); const button = await challengeFrame.waitForSelector( target ); From 37ef8921f11337be59fac8f5c24ceed6b99f82dd Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Tue, 6 Feb 2024 08:46:04 +1000 Subject: [PATCH 34/52] Render a notice when the available balance is below the minimum deposit threshold (#7710) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bruce Aldridge Co-authored-by: Rua Haszard Co-authored-by: Francesco Co-authored-by: Miguel Gasca Co-authored-by: Guilherme Pressutto Co-authored-by: Malith Senaweera <6216000+malithsen@users.noreply.github.com> Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> Co-authored-by: Allie Mims <60988591+allie500@users.noreply.github.com> Co-authored-by: Alefe Souza Co-authored-by: César Costa <10233985+cesarcosta99@users.noreply.github.com> Co-authored-by: Valery Sukhomlinov <683297+dmvrtx@users.noreply.github.com> Co-authored-by: Jesse Pearson Co-authored-by: Timur Karimov Co-authored-by: Timur Karimov Co-authored-by: Anurag Bhandari Co-authored-by: Cvetan Cvetanov Co-authored-by: Naman Malhotra Co-authored-by: Dan Paun <82826872+dpaun1985@users.noreply.github.com> Co-authored-by: Mike Moore Co-authored-by: Rua Haszard Co-authored-by: Matt Allan Co-authored-by: Dan Paun Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Co-authored-by: oaratovskyi Co-authored-by: Daniel Mallory Co-authored-by: Vlad Olaru Co-authored-by: Zvonimir Maglica Co-authored-by: Ismael Martín Alabarce Co-authored-by: Vlad Olaru Co-authored-by: Brent MacKinnon Co-authored-by: frosso Co-authored-by: Rafael Zaleski Co-authored-by: Brian Borman <68524302+bborman22@users.noreply.github.com> Co-authored-by: Achyuth Ajoy Co-authored-by: Hector Lovo Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: botwoo Co-authored-by: Ricardo Metring --- .../fix-6774-deposit-minimum-threshold-ui | 4 ++ .../deposits-overview/deposit-notices.tsx | 36 ++++++++++++ client/components/deposits-overview/index.tsx | 15 +++++ .../deposits-overview/test/index.tsx | 58 +++++++++++++++++++ client/components/inline-notice/styles.scss | 5 ++ client/globals.d.ts | 3 +- 6 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-6774-deposit-minimum-threshold-ui diff --git a/changelog/fix-6774-deposit-minimum-threshold-ui b/changelog/fix-6774-deposit-minimum-threshold-ui new file mode 100644 index 00000000000..16e922bc6b7 --- /dev/null +++ b/changelog/fix-6774-deposit-minimum-threshold-ui @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Show a notice to the merchant when the available balance is below the minimum deposit amount. diff --git a/client/components/deposits-overview/deposit-notices.tsx b/client/components/deposits-overview/deposit-notices.tsx index abf821275a5..1ec3d68baeb 100644 --- a/client/components/deposits-overview/deposit-notices.tsx +++ b/client/components/deposits-overview/deposit-notices.tsx @@ -151,6 +151,42 @@ export const NegativeBalanceDepositsPausedNotice: React.FC = () => ( ); +/** + * Renders a notice informing the user that their available balance is below the minimum deposit threshold. + */ +export const DepositMinimumBalanceNotice: React.FC< { + /** + * The minimum deposit amount formatted as a currency string (e.g. $5.00 USD). + */ + minimumDepositAmountFormatted: string; +} > = ( { minimumDepositAmountFormatted } ) => { + return ( + + { interpolateComponents( { + mixedString: sprintf( + /* translators: %s: a formatted currency amount, e.g. $5.00 USD */ + __( + 'Deposits are paused while your available funds balance remains below %s. {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'woocommerce-payments' + ), + minimumDepositAmountFormatted + ), + components: { + learnMoreLink: ( + // Link content is in the format string above. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + + ); +}; + /** * Renders a notice informing the user that deposits only occur when there are funds available. */ diff --git a/client/components/deposits-overview/index.tsx b/client/components/deposits-overview/index.tsx index 3906fd0f409..f87f1dc7a3e 100644 --- a/client/components/deposits-overview/index.tsx +++ b/client/components/deposits-overview/index.tsx @@ -15,12 +15,14 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies. */ import { getAdminUrl } from 'wcpay/utils'; +import { formatExplicitCurrency } from 'wcpay/utils/currency'; import { recordEvent, events } from 'tracks'; import Loadable from 'components/loadable'; import { useSelectedCurrencyOverview } from 'wcpay/overview/hooks'; import RecentDepositsList from './recent-deposits-list'; import DepositSchedule from './deposit-schedule'; import { + DepositMinimumBalanceNotice, DepositTransitDaysNotice, NegativeBalanceDepositsPausedNotice, NewAccountWaitingPeriodNotice, @@ -50,6 +52,10 @@ const DepositsOverview: React.FC = () => { const availableFunds = overview?.available?.amount ?? 0; const pendingFunds = overview?.pending?.amount ?? 0; + const minimumDepositAmount = + wcpaySettings.accountStatus.deposits + ?.minimum_scheduled_deposit_amounts?.[ selectedCurrency ] ?? 0; + const isAboveMinimumDepositAmount = availableFunds >= minimumDepositAmount; // If the available balance is negative, deposits may be paused. const isNegativeBalanceDepositsPaused = availableFunds < 0; // When there are funds pending but no available funds, deposits are paused. @@ -135,6 +141,15 @@ const DepositsOverview: React.FC = () => { { isNegativeBalanceDepositsPaused && () } + { availableFunds > 0 && + ! isAboveMinimumDepositAmount && ( + + ) } > ) } diff --git a/client/components/deposits-overview/test/index.tsx b/client/components/deposits-overview/test/index.tsx index 87cbf56552b..7b5c54af430 100644 --- a/client/components/deposits-overview/test/index.tsx +++ b/client/components/deposits-overview/test/index.tsx @@ -53,6 +53,9 @@ declare const global: { deposits: { restrictions: string; completed_waiting_period: boolean; + minimum_scheduled_deposit_amounts: { + [ currencyCode: string ]: number; + }; }; }; accountDefaultCurrency: string; @@ -195,6 +198,10 @@ describe( 'Deposits Overview information', () => { deposits: { restrictions: 'deposits_unrestricted', completed_waiting_period: true, + minimum_scheduled_deposit_amounts: { + eur: 500, + usd: 500, + }, }, }, accountDefaultCurrency: 'USD', @@ -548,3 +555,54 @@ describe( 'Paused Deposit notice Renders', () => { expect( queryByText( /Deposits may be interrupted/ ) ).toBeFalsy(); } ); } ); + +describe( 'Minimum Deposit Amount Notice', () => { + beforeAll( () => { + mockUseDeposits.mockReturnValue( { + depositsCount: 0, + deposits: [], + isLoading: false, + } ); + } ); + + afterAll( () => { + jest.clearAllMocks(); + } ); + + test( 'When available balance is below the minimum threshold', () => { + const accountOverview = createMockNewAccountOverview( 'eur', 100, 100 ); + mockOverviews( [ accountOverview ] ); + mockDepositOverviews( [ accountOverview ] ); + + mockUseSelectedCurrency.mockReturnValue( { + selectedCurrency: 'eur', + setSelectedCurrency: mockSetSelectedCurrency, + } ); + + const { getByText } = render( ); + getByText( + /Deposits are paused while your available funds balance remains below €5.00/, + { + ignore: '.a11y-speak-region', + } + ); + } ); + + test( 'When available balance is above the minimum threshold', () => { + const accountOverview = createMockNewAccountOverview( 'eur', 100, 500 ); + mockOverviews( [ accountOverview ] ); + mockDepositOverviews( [ accountOverview ] ); + + mockUseSelectedCurrency.mockReturnValue( { + selectedCurrency: 'eur', + setSelectedCurrency: mockSetSelectedCurrency, + } ); + + const { queryByText } = render( ); + expect( + queryByText( + /Deposits are paused while your available funds balance remains below/ + ) + ).toBeFalsy(); + } ); +} ); diff --git a/client/components/inline-notice/styles.scss b/client/components/inline-notice/styles.scss index 6c4059d7273..94c5b954d80 100644 --- a/client/components/inline-notice/styles.scss +++ b/client/components/inline-notice/styles.scss @@ -31,6 +31,11 @@ .components-notice__content { margin-top: 2px; margin-bottom: 2px; + + // Ensure links don't wrap across lines, e.g. "Learn (newline) more". + a { + white-space: nowrap; + } } .wcpay-inline-notice__content__actions { padding-top: 12px; diff --git a/client/globals.d.ts b/client/globals.d.ts index 71109c43227..e06eaf7a865 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -40,7 +40,8 @@ declare global { monthly_anchor: null | number; delay_days: null | number; completed_waiting_period: boolean; - minimum_deposit_amounts: Record< string, number >; + minimum_manual_deposit_amounts: Record< string, number >; + minimum_scheduled_deposit_amounts: Record< string, number >; }; depositsStatus?: string; currentDeadline?: bigint; From bcbb2eb5ff6531d03694b48fe337efef48209596 Mon Sep 17 00:00:00 2001 From: Samir Merchant Date: Tue, 6 Feb 2024 13:04:46 -0500 Subject: [PATCH 35/52] Fixes Pay for Order page for non-card payment methods (#8111) Co-authored-by: Francesco --- changelog/fix-order-pay-with-non-card-method | 4 ++++ client/checkout/classic/event-handlers.js | 6 +----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 changelog/fix-order-pay-with-non-card-method diff --git a/changelog/fix-order-pay-with-non-card-method b/changelog/fix-order-pay-with-non-card-method new file mode 100644 index 00000000000..d766ed918d4 --- /dev/null +++ b/changelog/fix-order-pay-with-non-card-method @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixes Pay for Order checkout using non-card payment methods. diff --git a/client/checkout/classic/event-handlers.js b/client/checkout/classic/event-handlers.js index 48a3f6cd0ba..6615b5859f3 100644 --- a/client/checkout/classic/event-handlers.js +++ b/client/checkout/classic/event-handlers.js @@ -130,11 +130,7 @@ jQuery( function ( $ ) { } ); $payForOrderForm.on( 'submit', function () { - if ( - $payForOrderForm - .find( "input:checked[name='payment_method']" ) - .val() !== 'woocommerce_payments' - ) { + if ( getSelectedUPEGatewayPaymentMethod() === null ) { return; } From f911d30e0c63bbd4b012d869fc56b59bec073d8c Mon Sep 17 00:00:00 2001 From: bruce aldridge Date: Wed, 7 Feb 2024 14:20:42 +1300 Subject: [PATCH 36/52] Negative balance deposit notice will only show when total balance is below 0 (#8117) --- .../fix-6941-warning-for-negative-balance | 4 ++++ client/components/deposits-overview/index.tsx | 5 +++-- .../deposits-overview/test/index.tsx | 18 +++++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 changelog/fix-6941-warning-for-negative-balance diff --git a/changelog/fix-6941-warning-for-negative-balance b/changelog/fix-6941-warning-for-negative-balance new file mode 100644 index 00000000000..e97bb609d62 --- /dev/null +++ b/changelog/fix-6941-warning-for-negative-balance @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixed a bug where the 'deposits paused while balance is negative' notice was erroneously shown after an instant deposit. diff --git a/client/components/deposits-overview/index.tsx b/client/components/deposits-overview/index.tsx index f87f1dc7a3e..99bf419f0ed 100644 --- a/client/components/deposits-overview/index.tsx +++ b/client/components/deposits-overview/index.tsx @@ -51,13 +51,14 @@ const DepositsOverview: React.FC = () => { const availableFunds = overview?.available?.amount ?? 0; const pendingFunds = overview?.pending?.amount ?? 0; + const totalFunds = availableFunds + pendingFunds; const minimumDepositAmount = wcpaySettings.accountStatus.deposits ?.minimum_scheduled_deposit_amounts?.[ selectedCurrency ] ?? 0; const isAboveMinimumDepositAmount = availableFunds >= minimumDepositAmount; - // If the available balance is negative, deposits may be paused. - const isNegativeBalanceDepositsPaused = availableFunds < 0; + // If the total balance is negative, deposits may be paused. + const isNegativeBalanceDepositsPaused = totalFunds < 0; // When there are funds pending but no available funds, deposits are paused. const isDepositAwaitingPendingFunds = availableFunds === 0 && pendingFunds > 0; diff --git a/client/components/deposits-overview/test/index.tsx b/client/components/deposits-overview/test/index.tsx index 7b5c54af430..27fd9d7e412 100644 --- a/client/components/deposits-overview/test/index.tsx +++ b/client/components/deposits-overview/test/index.tsx @@ -523,11 +523,11 @@ describe( 'Suspended Deposit Notice Renders', () => { } ); describe( 'Paused Deposit notice Renders', () => { - test( 'When available balance is negative', () => { + test( 'When total balance is negative', () => { const accountOverview = createMockNewAccountOverview( 'usd', - 100, - -100 // Negative 100 available balance + 50, // Pending and available balance total to -50 + -100 ); mockOverviews( [ accountOverview ] ); mockDepositOverviews( [ accountOverview ] ); @@ -551,6 +551,18 @@ describe( 'Paused Deposit notice Renders', () => { mockOverviews( [ accountOverview ] ); mockDepositOverviews( [ accountOverview ] ); + const { queryByText } = render( ); + expect( queryByText( /Deposits may be interrupted/ ) ).toBeFalsy(); + } ); + test( 'When available balance is negative', () => { + const accountOverview = createMockNewAccountOverview( + 'usd', + 100, + -100 // Negative 100 available balance + ); + mockOverviews( [ accountOverview ] ); + mockDepositOverviews( [ accountOverview ] ); + const { queryByText } = render( ); expect( queryByText( /Deposits may be interrupted/ ) ).toBeFalsy(); } ); From f5a43db8dbbcee469c8e65b5070f5404ab0683e9 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 7 Feb 2024 14:32:15 +0100 Subject: [PATCH 37/52] fix: help text alignemnt with Gutenberg plugin (#8147) --- changelog/fix-radio-button-help-text | 4 ++++ client/settings/card-body/styles.scss | 2 ++ client/settings/express-checkout-settings/index.scss | 8 -------- 3 files changed, 6 insertions(+), 8 deletions(-) create mode 100644 changelog/fix-radio-button-help-text diff --git a/changelog/fix-radio-button-help-text b/changelog/fix-radio-button-help-text new file mode 100644 index 00000000000..1d1e1d4e46b --- /dev/null +++ b/changelog/fix-radio-button-help-text @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +fix: help text alignment with Gutenberg plugin enabled diff --git a/client/settings/card-body/styles.scss b/client/settings/card-body/styles.scss index a53f9db9f7b..cf768d6db22 100644 --- a/client/settings/card-body/styles.scss +++ b/client/settings/card-body/styles.scss @@ -40,6 +40,8 @@ // spacing in the "Express checkouts" settings page .components-radio-control__option { margin-bottom: 18px; + align-items: center; + display: flex; } .components-base-control__help { diff --git a/client/settings/express-checkout-settings/index.scss b/client/settings/express-checkout-settings/index.scss index 89bb38daccf..3ef56165e73 100644 --- a/client/settings/express-checkout-settings/index.scss +++ b/client/settings/express-checkout-settings/index.scss @@ -336,14 +336,6 @@ } } -.components-radio-control { - .payment-method-settings { - &__option-help-text { - margin-left: 26px; - } - } -} - .components-notice { &__content { display: flex; From f31da3433b521caab56775fdf7ee83adce5c1b11 Mon Sep 17 00:00:00 2001 From: Allie Mims <60988591+allie500@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:14:17 -0500 Subject: [PATCH 38/52] Bump WC tested up to version to 8.5.2 (#8124) --- changelog/dev-bump-wc-version-8-5-2 | 4 ++++ woocommerce-payments.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog/dev-bump-wc-version-8-5-2 diff --git a/changelog/dev-bump-wc-version-8-5-2 b/changelog/dev-bump-wc-version-8-5-2 new file mode 100644 index 00000000000..5a21803fc4c --- /dev/null +++ b/changelog/dev-bump-wc-version-8-5-2 @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Bump WC tested up to version to 8.5.2. diff --git a/woocommerce-payments.php b/woocommerce-payments.php index 0d29cd17222..601c89c342f 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -8,7 +8,7 @@ * Text Domain: woocommerce-payments * Domain Path: /languages * WC requires at least: 7.6 - * WC tested up to: 8.4.0 + * WC tested up to: 8.5.2 * Requires at least: 6.0 * Requires PHP: 7.3 * Version: 7.1.0 From 204f395c655ccb17fb9bba53ede453f46a7d1b3f Mon Sep 17 00:00:00 2001 From: Rafael Zaleski Date: Wed, 7 Feb 2024 16:05:27 -0300 Subject: [PATCH 39/52] Refactor the Woopay Checkout Flow UX (#8133) --- changelog/refactor-2448-woopay-checkout-flow | 4 ++++ client/checkout/woopay/email-input-iframe.js | 9 +++++---- includes/class-wc-payments-checkout.php | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 changelog/refactor-2448-woopay-checkout-flow diff --git a/changelog/refactor-2448-woopay-checkout-flow b/changelog/refactor-2448-woopay-checkout-flow new file mode 100644 index 00000000000..7b8f3852210 --- /dev/null +++ b/changelog/refactor-2448-woopay-checkout-flow @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Refactor the WooPay checkout flow UX diff --git a/client/checkout/woopay/email-input-iframe.js b/client/checkout/woopay/email-input-iframe.js index 2af4f1a11aa..b3966aaaf2d 100644 --- a/client/checkout/woopay/email-input-iframe.js +++ b/client/checkout/woopay/email-input-iframe.js @@ -461,9 +461,7 @@ export const handleWooPayEmailInput = async ( woopayEmailInput.addEventListener( 'input', ( e ) => { if ( ! hasCheckedLoginSession && ! customerClickedBackButton ) { - if ( customerClickedBackButton ) { - openLoginSessionIframe( woopayEmailInput.value ); - } + openLoginSessionIframe( woopayEmailInput.value ); return; } @@ -611,7 +609,10 @@ export const handleWooPayEmailInput = async ( if ( ! customerClickedBackButton ) { // Check if user already has a WooPay login session. - if ( ! hasCheckedLoginSession ) { + if ( + ! hasCheckedLoginSession && + ! getConfig( 'isWooPayDirectCheckoutEnabled' ) + ) { openLoginSessionIframe( woopayEmailInput.value ); } } else { diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index f19f74bee7b..bfc0ea246e6 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -190,6 +190,7 @@ public function get_payment_fields_js_config() { 'isWoopayExpressCheckoutEnabled' => $this->woopay_util->is_woopay_express_checkout_enabled(), 'isWoopayFirstPartyAuthEnabled' => $this->woopay_util->is_woopay_first_party_auth_enabled(), 'isWooPayEmailInputEnabled' => $this->woopay_util->is_woopay_email_input_enabled(), + 'isWooPayDirectCheckoutEnabled' => WC_Payments_Features::is_woopay_direct_checkout_enabled(), 'isClientEncryptionEnabled' => WC_Payments_Features::is_client_secret_encryption_enabled(), 'woopayHost' => WooPay_Utilities::get_woopay_url(), 'platformTrackerNonce' => wp_create_nonce( 'platform_tracks_nonce' ), From 4a3011c22ab5b613a2fc7a1d24715bb5c7f0fa28 Mon Sep 17 00:00:00 2001 From: Malith Senaweera <6216000+malithsen@users.noreply.github.com> Date: Thu, 8 Feb 2024 10:56:40 +0530 Subject: [PATCH 40/52] Prevent WooPay webhook creation attempt when account is suspended (#8118) --- .../fix-woopay-webhook-on-suspended-accounts | 4 ++++ includes/class-wc-payments.php | 2 +- .../woopay/class-woopay-order-status-sync.php | 16 +++++++++++++++- .../test-class-woopay-order-status-sync.php | 18 +++++++++++++++++- 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-woopay-webhook-on-suspended-accounts diff --git a/changelog/fix-woopay-webhook-on-suspended-accounts b/changelog/fix-woopay-webhook-on-suspended-accounts new file mode 100644 index 00000000000..ff9ecffec00 --- /dev/null +++ b/changelog/fix-woopay-webhook-on-suspended-accounts @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Prevent WooPay webhook creation when account is suspended diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index a52898cca96..32dbd18f4c3 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -1459,7 +1459,7 @@ function ( $container ) { add_action( 'admin_init', [ $draft_orders, 'install' ] ); } - new WooPay_Order_Status_Sync( self::$api_client ); + new WooPay_Order_Status_Sync( self::$api_client, self::$account ); } } diff --git a/includes/woopay/class-woopay-order-status-sync.php b/includes/woopay/class-woopay-order-status-sync.php index 2cb9416ab73..98c7ceb79e3 100644 --- a/includes/woopay/class-woopay-order-status-sync.php +++ b/includes/woopay/class-woopay-order-status-sync.php @@ -7,6 +7,7 @@ namespace WCPay\WooPay; +use WC_Payments_Account; use WC_Payments_API_Client; use WCPay\Exceptions\API_Exception; @@ -21,6 +22,13 @@ class WooPay_Order_Status_Sync { const WCPAY_WEBHOOK_WOOPAY_ORDER_STATUS_CHANGED = 'wcpay_webhook_platform_checkout_order_status_changed'; + /** + * WC_Payments_Account instance to get information about the account + * + * @var WC_Payments_Account + */ + private $account; + /** * Client for making requests to the WooCommerce Payments API * @@ -32,10 +40,12 @@ class WooPay_Order_Status_Sync { * Setup webhook for the WooPay Order Status Sync. * * @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client. + * @param WC_Payments_Account $account - WooCommerce Payments account. */ - public function __construct( WC_Payments_API_Client $payments_api_client ) { + public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account ) { $this->payments_api_client = $payments_api_client; + $this->account = $account; add_filter( 'woocommerce_webhook_topic_hooks', [ __CLASS__, 'add_topics' ], 20, 2 ); add_filter( 'woocommerce_webhook_payload', [ __CLASS__, 'create_payload' ], 10, 4 ); @@ -63,6 +73,10 @@ public function maybe_create_woopay_order_webhook() { return; } + if ( $this->account->is_account_rejected() ) { + return; + } + $this->register_webhook(); } diff --git a/tests/unit/woopay/test-class-woopay-order-status-sync.php b/tests/unit/woopay/test-class-woopay-order-status-sync.php index 987680d4bc4..c99a39762b3 100644 --- a/tests/unit/woopay/test-class-woopay-order-status-sync.php +++ b/tests/unit/woopay/test-class-woopay-order-status-sync.php @@ -20,8 +20,9 @@ class WooPay_Order_Status_Sync_Test extends WP_UnitTestCase { public function set_up() { parent::set_up(); + $this->account_mock = $this->createMock( WC_Payments_Account::class ); $this->api_client_mock = $this->createMock( WC_Payments_API_Client::class ); - $this->webhook_sync_mock = new WCPay\WooPay\WooPay_Order_Status_Sync( $this->api_client_mock ); + $this->webhook_sync_mock = new WCPay\WooPay\WooPay_Order_Status_Sync( $this->api_client_mock, $this->account_mock ); // Mock the main class's cache service. $this->_cache = WC_Payments::get_database_cache(); @@ -45,6 +46,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { /** * Tests that WooPay-specific webhooks are modified as expected. + * @group webhook */ public function test_woopay_specific_webhook_payload_is_updated() { wp_set_current_user( self::$admin_user->ID ); @@ -119,6 +121,20 @@ public function test_webhook_is_created() { $this->assertNotEmpty( WooPay_Order_Status_Sync::get_webhook() ); } + /** + * Tests that the webhook is not created for rejected WCPay accounts. + */ + public function test_webhook_is_created_for_rejected() { + wp_set_current_user( self::$admin_user->ID ); + $this->account_mock->method( 'is_account_rejected' )->willReturn( true ); + + $this->assertEmpty( WooPay_Order_Status_Sync::get_webhook() ); + + $this->webhook_sync_mock->maybe_create_woopay_order_webhook(); + + $this->assertEmpty( WooPay_Order_Status_Sync::get_webhook() ); + } + /** * Tests that the webhook is deleted succesfuly. */ From dcff735a04e9874d791f9ebbbd1ad27ca01ee9aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Janisels?= Date: Thu, 8 Feb 2024 13:46:10 +0200 Subject: [PATCH 41/52] Format fee descriptions for iDeal payment method without percentage rate (#8149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kārlis Janisels Co-authored-by: Vladimir Reznichenko --- ...531-display-zero-percentage-rate-in-fee-pill | 5 +++++ client/utils/account-fees.tsx | 17 +---------------- 2 files changed, 6 insertions(+), 16 deletions(-) create mode 100644 changelog/fix-6531-display-zero-percentage-rate-in-fee-pill diff --git a/changelog/fix-6531-display-zero-percentage-rate-in-fee-pill b/changelog/fix-6531-display-zero-percentage-rate-in-fee-pill new file mode 100644 index 00000000000..e83ef108807 --- /dev/null +++ b/changelog/fix-6531-display-zero-percentage-rate-in-fee-pill @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Format fee descriptions for iDeal payment method without percentage rate + + diff --git a/client/utils/account-fees.tsx b/client/utils/account-fees.tsx index c2b3214c1b5..3893b13ab02 100644 --- a/client/utils/account-fees.tsx +++ b/client/utils/account-fees.tsx @@ -258,14 +258,7 @@ export const formatAccountFeesDescription = ( accountFees: FeeStructure, customFormats = {} ): string | JSX.Element => { - const defaultFee = { - fixed_rate: 0, - percentage_rate: 0, - currency: 'USD', - }; const baseFee = accountFees.base; - const additionalFee = accountFees.additional ?? defaultFee; - const fxFee = accountFees.fx ?? defaultFee; const currentBaseFee = getCurrentBaseFee( accountFees ); // Default formats will be used if no matching field was passed in the `formats` parameter. @@ -282,17 +275,9 @@ export const formatAccountFeesDescription = ( ...customFormats, }; - // Some payment methods doesn't have base percentage rate. In this case, the lowest rate will be shown as a start value - let displayFeePercentageRate = baseFee.percentage_rate; - if ( displayFeePercentageRate <= 0 ) { - displayFeePercentageRate = - additionalFee.percentage_rate < fxFee.percentage_rate - ? additionalFee.percentage_rate - : fxFee.percentage_rate; - } const feeDescription = sprintf( formats.fee, - formatFee( displayFeePercentageRate ), + formatFee( baseFee.percentage_rate ), formatCurrency( baseFee.fixed_rate, baseFee.currency ) ); const isFormattingWithDiscount = From ef934b7c77bdd8bdacc2e57665137d756ca73e45 Mon Sep 17 00:00:00 2001 From: Cvetan Cvetanov Date: Thu, 8 Feb 2024 14:27:56 +0200 Subject: [PATCH 42/52] Apply localization to CSV exports sent via email. (#7938) --- changelog/add-7843-localize-exports | 4 + client/components/csv-export-modal/index.tsx | 227 ++++++++++++++++ .../components/csv-export-modal/styles.scss | 51 ++++ .../test/__snapshots__/index.test.tsx.snap | 7 + .../csv-export-modal/test/index.test.tsx | 71 +++++ client/data/deposits/resolvers.js | 1 + client/data/disputes/resolvers.js | 1 + client/data/settings/actions.js | 6 + client/data/settings/hooks.js | 10 + client/data/settings/selectors.js | 4 + client/data/transactions/resolvers.js | 1 + client/deposits/index.tsx | 4 + client/deposits/list/index.tsx | 176 +++++++----- client/deposits/list/test/index.tsx | 21 +- client/disputes/index.tsx | 183 ++++++++----- client/disputes/test/index.tsx | 33 ++- client/globals.d.ts | 8 + .../components/export-language/index.tsx | 55 ++++ .../reporting-settings/components/index.ts | 6 + client/settings/reporting-settings/index.tsx | 35 +++ .../settings/reporting-settings/interfaces.ts | 4 + client/settings/reporting-settings/style.scss | 17 ++ .../test/__snapshots__/index.test.js.snap | 151 +++++++++++ .../reporting-settings/test/index.test.js | 55 ++++ client/settings/settings-manager/index.js | 28 ++ client/transactions/list/index.tsx | 250 +++++++++++------- client/transactions/list/test/index.tsx | 27 +- client/transactions/test/index.tsx | 8 + client/utils/index.js | 75 ++++++ includes/admin/class-wc-payments-admin.php | 4 + ...s-wc-rest-payments-deposits-controller.php | 3 +- ...s-wc-rest-payments-disputes-controller.php | 3 +- ...s-wc-rest-payments-settings-controller.php | 21 ++ ...-rest-payments-transactions-controller.php | 3 +- includes/class-wc-payments-utils.php | 27 ++ .../class-wc-payments-api-client.php | 18 +- tests/js/jest-test-file-setup.js | 10 + 37 files changed, 1363 insertions(+), 245 deletions(-) create mode 100644 changelog/add-7843-localize-exports create mode 100644 client/components/csv-export-modal/index.tsx create mode 100644 client/components/csv-export-modal/styles.scss create mode 100644 client/components/csv-export-modal/test/__snapshots__/index.test.tsx.snap create mode 100644 client/components/csv-export-modal/test/index.test.tsx create mode 100644 client/settings/reporting-settings/components/export-language/index.tsx create mode 100644 client/settings/reporting-settings/components/index.ts create mode 100644 client/settings/reporting-settings/index.tsx create mode 100644 client/settings/reporting-settings/interfaces.ts create mode 100644 client/settings/reporting-settings/style.scss create mode 100644 client/settings/reporting-settings/test/__snapshots__/index.test.js.snap create mode 100644 client/settings/reporting-settings/test/index.test.js diff --git a/changelog/add-7843-localize-exports b/changelog/add-7843-localize-exports new file mode 100644 index 00000000000..c80d6d639b1 --- /dev/null +++ b/changelog/add-7843-localize-exports @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Apply localization to CSV exports for transactions, deposits, and disputes sent via email. diff --git a/client/components/csv-export-modal/index.tsx b/client/components/csv-export-modal/index.tsx new file mode 100644 index 00000000000..2cddf0908ab --- /dev/null +++ b/client/components/csv-export-modal/index.tsx @@ -0,0 +1,227 @@ +/** @format */ +/** + * External dependencies + */ +import React, { useState } from 'react'; +import { __ } from '@wordpress/i18n'; +import { + Button, + SelectControl, + CheckboxControl, + ExternalLink, +} from '@wordpress/components'; +import interpolateComponents from '@automattic/interpolate-components'; +import { useDispatch } from '@wordpress/data'; +import DomainsIcon from 'gridicons/dist/domains'; + +/** + * Internal dependencies + */ +import { ReportingExportLanguageHook } from 'wcpay/settings/reporting-settings/interfaces'; +import { useReportingExportLanguage, useSettings } from 'wcpay/data'; +import ConfirmationModal from 'wcpay/components/confirmation-modal'; +import { getAdminUrl, getExportLanguageOptions } from 'wcpay/utils'; +import './styles.scss'; + +interface CSVExportModalProps { + totalItems: number; + exportType: 'transactions' | 'deposits' | 'disputes'; + onClose: () => void; + onSubmit: ( language: string ) => void; +} + +interface SettingsHook { + isSaving: boolean; + isLoading: boolean; + saveSettings: () => void; +} + +const CVSExportModal: React.FunctionComponent< CSVExportModalProps > = ( { + totalItems, + exportType, + onClose, + onSubmit, +} ) => { + const { updateOptions } = useDispatch( 'wc/admin/options' ); + const { saveSettings } = useSettings() as SettingsHook; + + const [ + exportLanguage, + updateExportLanguage, + ] = useReportingExportLanguage() as ReportingExportLanguageHook; + + const [ modalLanguage, setModalLanguage ] = useState( exportLanguage ); + const [ modalRemember, setModalRemember ] = useState( true ); + + const onDownload = async () => { + onSubmit( modalLanguage ); + + // If the Remember checkbox is checked, dismiss the modal. + if ( modalRemember ) { + await updateOptions( { + wcpay_reporting_export_modal_dismissed: modalRemember, + } ); + + updateExportLanguage( modalLanguage ); + saveSettings(); + + wcpaySettings.reporting.exportModalDismissed = true; + } + }; + + const buttonContent = ( + <> + + { __( 'Cancel', 'woocommerce-payments' ) } + ++ { __( 'Download', 'woocommerce-payments' ) } + + > + ); + + const getModalTitle = ( type: string ): string => { + switch ( type ) { + case 'transactions': + return __( + 'Export transactions report', + 'woocommerce-payments' + ); + case 'deposits': + return __( 'Export deposits report', 'woocommerce-payments' ); + case 'disputes': + return __( 'Export disputes report', 'woocommerce-payments' ); + default: + return __( 'Export report', 'woocommerce-payments' ); + } + }; + + const getExportNumberText = ( type: string ): string => { + switch ( type ) { + case 'transactions': + return __( + 'Exporting {{total/}} transactions…', + 'woocommerce-payments' + ); + case 'deposits': + return __( + 'Exporting {{total/}} deposits…', + 'woocommerce-payments' + ); + case 'disputes': + return __( + 'Exporting {{total/}} disputes…', + 'woocommerce-payments' + ); + default: + return __( + 'Exporting {{total/}} rows…', + 'woocommerce-payments' + ); + } + }; + + const getExportLabel = ( type: string ): string => { + switch ( type ) { + case 'transactions': + return __( + 'Export transactions report in', + 'woocommerce-payments' + ); + case 'deposits': + return __( + 'Export deposits report in', + 'woocommerce-payments' + ); + case 'disputes': + return __( + 'Export disputes report in', + 'woocommerce-payments' + ); + default: + return __( 'Export report in', 'woocommerce-payments' ); + } + }; + + const handleExportLanguageChange = ( language: string ) => { + setModalLanguage( language ); + }; + + const handleExportLanguageRememberChange = ( value: boolean ) => { + setModalRemember( value ); + }; + + return ( +{ + return false; + } } + > + + ); +}; + +export default CVSExportModal; diff --git a/client/components/csv-export-modal/styles.scss b/client/components/csv-export-modal/styles.scss new file mode 100644 index 00000000000..600a6d2a18a --- /dev/null +++ b/client/components/csv-export-modal/styles.scss @@ -0,0 +1,51 @@ +.reporting-export-modal { + .components-modal__header { + border-bottom: 1px solid #dcdcde !important; + } + + .wcpay-confirmation-modal__footer { + .is-secondary { + box-shadow: none; + } + } + + &__items-number { + border-bottom: 1px solid #dcdcde; + padding: 15px 0; + } + + &__settings { + @include breakpoint( '>660px' ) { + min-width: 500px; + } + + &--language { + display: flex; + flex-wrap: wrap; + } + + &--language-label { + flex: 1 1 200px; + display: flex; + + .domains-icon { + width: 16px; + margin: 7px 0; + } + + .export-label { + padding: 10px 0 0 8px; + } + } + + &--language-select { + flex: 1 1 200px; + } + + &--remember { + p { + padding-top: 0 !important; + } + } + } +} diff --git a/client/components/csv-export-modal/test/__snapshots__/index.test.tsx.snap b/client/components/csv-export-modal/test/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..251b5f8438f --- /dev/null +++ b/client/components/csv-export-modal/test/__snapshots__/index.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RefundModal it renders correctly 1`] = ` + +`; diff --git a/client/components/csv-export-modal/test/index.test.tsx b/client/components/csv-export-modal/test/index.test.tsx new file mode 100644 index 00000000000..3254e197156 --- /dev/null +++ b/client/components/csv-export-modal/test/index.test.tsx @@ -0,0 +1,71 @@ +/** @format */ + +/** + * External dependencies + */ +import React from 'react'; +import { render } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import CVSExportModal from '..'; +import { useReportingExportLanguage, useSettings } from 'wcpay/data'; + +declare const global: { + wcpaySettings: { + locale: { + code: string; + native_name: string; + }; + }; +}; + +jest.mock( '@wordpress/data', () => ( { + useDispatch: jest.fn( () => ( { updateOptions: jest.fn() } ) ), +} ) ); + +jest.mock( 'wcpay/data', () => ( { + useReportingExportLanguage: jest.fn( () => [ 'en', jest.fn() ] ), + useSettings: jest.fn(), +} ) ); + +const mockUseSettings = useSettings as jest.MockedFunction< + typeof useSettings +>; + +const mockUseReportingExportLanguage = useReportingExportLanguage as jest.MockedFunction< + typeof useReportingExportLanguage +>; + +describe( 'RefundModal', () => { + beforeEach( () => { + mockUseReportingExportLanguage.mockReturnValue( [ 'en', jest.fn() ] ); + + mockUseSettings.mockReturnValue( { + isLoading: false, + isSaving: false, + saveSettings: ( a ) => a, + } ); + + global.wcpaySettings = { + locale: { + code: 'es_ES', + native_name: 'Spanish', + }, + }; + } ); + + test( 'it renders correctly', () => { + const { container: modal } = render( ++ { interpolateComponents( { + mixedString: getExportNumberText( exportType ), + components: { + total: { totalItems }, + }, + } ) } ++ +++Settings
+ +++ ++++ + { getExportLabel( exportType ) } + + +++ +++ ), + }, + } ) } + checked={ modalRemember } + onChange={ handleExportLanguageRememberChange } + data-testid="export-modal-remember" + /> + + ); + + expect( modal ).toMatchSnapshot(); + } ); +} ); diff --git a/client/data/deposits/resolvers.js b/client/data/deposits/resolvers.js index 4625f1374ab..a1420a9d3ed 100644 --- a/client/data/deposits/resolvers.js +++ b/client/data/deposits/resolvers.js @@ -99,6 +99,7 @@ const formatQueryFilters = ( query ) => ( { ], status_is: query.statusIs, status_is_not: query.statusIsNot, + locale: query.locale, } ); export function getDepositsCSV( query ) { diff --git a/client/data/disputes/resolvers.js b/client/data/disputes/resolvers.js index ee82f790c51..bf45770537c 100644 --- a/client/data/disputes/resolvers.js +++ b/client/data/disputes/resolvers.js @@ -34,6 +34,7 @@ const formatQueryFilters = ( query ) => ( { search: query.search, status_is: query.statusIs, status_is_not: query.statusIsNot, + locale: query.locale, } ); export function getDisputesCSV( query ) { diff --git a/client/data/settings/actions.js b/client/data/settings/actions.js index 137a01b31fc..d2d4728997d 100644 --- a/client/data/settings/actions.js +++ b/client/data/settings/actions.js @@ -204,6 +204,12 @@ export function updateDepositScheduleMonthlyAnchor( } ); } +export function updateExportLanguage( language ) { + return updateSettingsValues( { + reporting_export_language: language, + } ); +} + export function* saveSettings() { let error = null; try { diff --git a/client/data/settings/hooks.js b/client/data/settings/hooks.js index 37fb0a79b40..47bf57f07ef 100644 --- a/client/data/settings/hooks.js +++ b/client/data/settings/hooks.js @@ -272,6 +272,16 @@ export const useDepositScheduleMonthlyAnchor = () => { return [ depositScheduleMonthlyAnchor, updateDepositScheduleMonthlyAnchor ]; }; +export const useReportingExportLanguage = () => { + const { updateExportLanguage } = useDispatch( STORE_NAME ); + + const exportLanguage = useSelect( ( select ) => + select( STORE_NAME ).getExportLanguage() + ); + + return [ exportLanguage, updateExportLanguage ]; +}; + export const useDepositDelayDays = () => useSelect( ( select ) => select( STORE_NAME ).getDepositDelayDays(), [] ); diff --git a/client/data/settings/selectors.js b/client/data/settings/selectors.js index 41facc3ebb0..f2cf12f9b0f 100644 --- a/client/data/settings/selectors.js +++ b/client/data/settings/selectors.js @@ -116,6 +116,10 @@ export const getDepositScheduleInterval = ( state ) => { return getSettings( state ).deposit_schedule_interval || ''; }; +export const getExportLanguage = ( state ) => { + return getSettings( state ).reporting_export_language || ''; +}; + export const getDepositScheduleWeeklyAnchor = ( state ) => { return getSettings( state ).deposit_schedule_weekly_anchor || ''; }; diff --git a/client/data/transactions/resolvers.js b/client/data/transactions/resolvers.js index 8f5c3ea3a96..d2d6cab6cfb 100644 --- a/client/data/transactions/resolvers.js +++ b/client/data/transactions/resolvers.js @@ -55,6 +55,7 @@ export const formatQueryFilters = ( query ) => ( { customer_currency_is_not: query.customerCurrencyIsNot, search: query.search, user_timezone: getUserTimeZone(), + locale: query.locale, } ); /** diff --git a/client/deposits/index.tsx b/client/deposits/index.tsx index 7f6e5c30f14..34a74dade6b 100644 --- a/client/deposits/index.tsx +++ b/client/deposits/index.tsx @@ -14,6 +14,7 @@ import { TestModeNotice } from 'components/test-mode-notice'; import BannerNotice from 'components/banner-notice'; import DepositSchedule from 'components/deposits-overview/deposit-schedule'; import { useAllDepositsOverviews } from 'data'; +import { useSettings } from 'wcpay/data'; import DepositsList from './list'; const useNextDepositNoticeState = () => { @@ -73,6 +74,9 @@ const NextDepositNotice: React.FC = () => { }; const DepositsPage: React.FC = () => { + // pre-fetching the settings. + useSettings(); + return ( ); }; diff --git a/client/transactions/list/test/index.tsx b/client/transactions/list/test/index.tsx index 63fbbc01427..3d93aee514e 100644 --- a/client/transactions/list/test/index.tsx +++ b/client/transactions/list/test/index.tsx @@ -18,7 +18,11 @@ import os from 'os'; * Internal dependencies */ import { TransactionsList } from '../'; -import { useTransactions, useTransactionsSummary } from 'data/index'; +import { + useTransactions, + useTransactionsSummary, + useReportingExportLanguage, +} from 'data/index'; import type { Transaction } from 'data/transactions/hooks'; jest.mock( '@woocommerce/csv-export', () => { @@ -50,6 +54,7 @@ jest.mock( '@wordpress/data', () => ( { jest.mock( 'data/index', () => ( { useTransactions: jest.fn(), useTransactionsSummary: jest.fn(), + useReportingExportLanguage: jest.fn( () => [ 'en', jest.fn() ] ), } ) ); const mockDownloadCSVFile = downloadCSVFile as jest.MockedFunction< @@ -66,6 +71,10 @@ const mockUseTransactionsSummary = useTransactionsSummary as jest.MockedFunction typeof useTransactionsSummary >; +const mockUseReportingExportLanguage = useReportingExportLanguage as jest.MockedFunction< + typeof useReportingExportLanguage +>; + declare const global: { wcpaySettings: { isSubscriptionsActive: boolean; @@ -87,6 +96,9 @@ declare const global: { precision: number; }; }; + reporting?: { + exportModalDismissed: boolean; + }; }; }; @@ -205,6 +217,8 @@ describe( 'Transactions list', () => { // the query string is preserved across tests, so we need to reset it updateQueryString( {}, '/', {} ); + mockUseReportingExportLanguage.mockReturnValue( [ 'en', jest.fn() ] ); + global.wcpaySettings = { featureFlags: { customSearch: true, @@ -225,6 +239,9 @@ describe( 'Transactions list', () => { precision: 2, }, }, + reporting: { + exportModalDismissed: true, + }, }; } ); @@ -537,12 +554,6 @@ describe( 'Transactions list', () => { await waitFor( () => { expect( mockApiFetch ).toHaveBeenCalledTimes( 1 ); - expect( mockApiFetch ).toHaveBeenCalledWith( { - method: 'POST', - path: `/wc/v3/payments/transactions/download?user_email=mock%40example.com&user_timezone=${ encodeURIComponent( - getUserTimeZone() - ) }`, - } ); } ); } ); @@ -597,7 +608,7 @@ describe( 'Transactions list', () => { method: 'POST', path: `/wc/v3/payments/transactions/download?user_email=mock%40example.com&deposit_id=po_mock&user_timezone=${ encodeURIComponent( getUserTimeZone() - ) }`, + ) }&locale=en`, } ); } ); } ); diff --git a/client/transactions/test/index.tsx b/client/transactions/test/index.tsx index e8347332656..f5611c38c54 100644 --- a/client/transactions/test/index.tsx +++ b/client/transactions/test/index.tsx @@ -18,6 +18,7 @@ import { useSettings, useTransactions, useTransactionsSummary, + useReportingExportLanguage, } from 'data/index'; jest.mock( '@wordpress/api-fetch', () => jest.fn() ); @@ -44,6 +45,7 @@ jest.mock( 'data/index', () => ( { useManualCapture: jest.fn(), useSettings: jest.fn(), useAuthorizationsSummary: jest.fn(), + useReportingExportLanguage: jest.fn( () => [ 'en', jest.fn() ] ), } ) ); const mockUseTransactions = useTransactions as jest.MockedFunction< @@ -70,6 +72,10 @@ const mockUseFraudOutcomeTransactionsSummary = useFraudOutcomeTransactionsSummar typeof useFraudOutcomeTransactionsSummary >; +const mockUseReportingExportLanguage = useReportingExportLanguage as jest.MockedFunction< + typeof useReportingExportLanguage +>; + declare const global: { wcpaySettings: { featureFlags: { @@ -90,6 +96,8 @@ describe( 'TransactionsPage', () => { beforeEach( () => { jest.clearAllMocks(); + mockUseReportingExportLanguage.mockReturnValue( [ 'en', jest.fn() ] ); + // the query string is preserved across tests, so we need to reset it updateQueryString( {}, '/', {} ); diff --git a/client/utils/index.js b/client/utils/index.js index f10cb308251..2d4390bccdd 100644 --- a/client/utils/index.js +++ b/client/utils/index.js @@ -7,6 +7,7 @@ import moment from 'moment'; import { dateI18n } from '@wordpress/date'; import { NAMESPACE } from 'wcpay/data/constants'; import { numberFormat } from '@woocommerce/number'; +import { __ } from '@wordpress/i18n'; /** * Returns true if WooPayments is in test mode, false otherwise. @@ -158,3 +159,77 @@ export const applyThousandSeparator = ( trxCount ) => { const formattedNumber = partial( numberFormat, siteNumberOptions ); return formattedNumber( trxCount ); }; + +/** + * Returns true if Export Modal is dismissed, false otherwise. + * + * @return {boolean} True if dismissed, false otherwise. + */ +export const isExportModalDismissed = () => { + if ( typeof wcpaySettings === 'undefined' ) { + return true; + } + + return wcpaySettings?.reporting?.exportModalDismissed ?? false; +}; + +/** + * Returns true if Export Modal is dismissed, false otherwise. + * + * @return {boolean} True if dismissed, false otherwise. + */ + +export const isDefaultSiteLanguage = () => { + if ( typeof wcpaySettings === 'undefined' ) { + return true; + } + + return wcpaySettings.locale?.code === 'en_US'; +}; + +/** + * Returns the language code for CSV exports. + * + * @param {string} language Selected language code. + * @param {string} storedLanguage Stored language code. + * + * @return {string} Language code. + */ +export const getExportLanguage = ( language, storedLanguage ) => { + let siteLanguage = 'en_US'; + + // If the default site language is en_US, skip + if ( isDefaultSiteLanguage() ) { + return siteLanguage; + } + + if ( typeof wcpaySettings !== 'undefined' ) { + siteLanguage = wcpaySettings?.locale?.code ?? siteLanguage; + } + + // In case the default export setting is not present, use the site locale. + const defaultLanguage = storedLanguage ?? siteLanguage; + + // When modal is dismissed use the default language locale. + return language !== '' ? language : defaultLanguage; +}; + +/** + * Returns the language options for CSV exports language selector. + * + * @return {Array} Language options. + */ +export const getExportLanguageOptions = () => { + return [ + { + label: __( 'English (United States)', 'woocommerce-payments' ), + value: 'en_US', + }, + { + label: + __( 'Site Language - ', 'woocommerce-payments' ) + + wcpaySettings.locale.native_name, + value: wcpaySettings.locale.code, + }, + ]; +}; diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 179c1b2ddc6..be2f2631440 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -899,6 +899,10 @@ private function get_js_settings(): array { 'capabilityRequestNotices' => get_option( 'wcpay_capability_request_dismissed_notices ', [] ), 'storeName' => get_bloginfo( 'name' ), 'isNextDepositNoticeDismissed' => WC_Payments_Features::is_next_deposit_notice_dismissed(), + 'reporting' => [ + 'exportModalDismissed' => get_option( 'wcpay_reporting_export_modal_dismissed', false ), + ], + 'locale' => WC_Payments_Utils::get_language_data( get_locale() ), ]; return apply_filters( 'wcpay_js_settings', $this->wcpay_js_settings ); diff --git a/includes/admin/class-wc-rest-payments-deposits-controller.php b/includes/admin/class-wc-rest-payments-deposits-controller.php index f8a4fd7ec1c..54ccf81590b 100644 --- a/includes/admin/class-wc-rest-payments-deposits-controller.php +++ b/includes/admin/class-wc-rest-payments-deposits-controller.php @@ -151,9 +151,10 @@ public function get_deposit( $request ) { */ public function get_deposits_export( $request ) { $user_email = $request->get_param( 'user_email' ); + $locale = $request->get_param( 'locale' ); $filters = $this->get_deposits_filters( $request ); - return $this->forward_request( 'get_deposits_export', [ $filters, $user_email ] ); + return $this->forward_request( 'get_deposits_export', [ $filters, $user_email, $locale ] ); } /** diff --git a/includes/admin/class-wc-rest-payments-disputes-controller.php b/includes/admin/class-wc-rest-payments-disputes-controller.php index e2efc774a83..64e1e478a21 100644 --- a/includes/admin/class-wc-rest-payments-disputes-controller.php +++ b/includes/admin/class-wc-rest-payments-disputes-controller.php @@ -148,9 +148,10 @@ public function close_dispute( $request ) { */ public function get_disputes_export( $request ) { $user_email = $request->get_param( 'user_email' ); + $locale = $request->get_param( 'locale' ); $filters = $this->get_disputes_filters( $request ); - return $this->forward_request( 'get_disputes_export', [ $filters, $user_email ] ); + return $this->forward_request( 'get_disputes_export', [ $filters, $user_email, $locale ] ); } /** diff --git a/includes/admin/class-wc-rest-payments-settings-controller.php b/includes/admin/class-wc-rest-payments-settings-controller.php index 97c5087e46d..55e56214142 100644 --- a/includes/admin/class-wc-rest-payments-settings-controller.php +++ b/includes/admin/class-wc-rest-payments-settings-controller.php @@ -201,6 +201,10 @@ public function register_routes() { 'description' => __( 'Monthly anchor for deposit scheduling when interval is set to monthly', 'woocommerce-payments' ), 'type' => [ 'integer', 'null' ], ], + 'reporting_export_language' => [ + 'description' => __( 'The language for an exported report for transactions, deposits, or disputes.', 'woocommerce-payments' ), + 'type' => 'string', + ], 'is_payment_request_enabled' => [ 'description' => sprintf( /* translators: %s: WooPayments */ @@ -514,6 +518,7 @@ public function get_settings(): WP_REST_Response { 'deposit_status' => $this->wcpay_gateway->get_option( 'deposit_status' ), 'deposit_restrictions' => $this->wcpay_gateway->get_option( 'deposit_restrictions' ), 'deposit_completed_waiting_period' => $this->wcpay_gateway->get_option( 'deposit_completed_waiting_period' ), + 'reporting_export_language' => $this->wcpay_gateway->get_option( 'reporting_export_language' ), 'current_protection_level' => $this->wcpay_gateway->get_option( 'current_protection_level' ), 'advanced_fraud_protection_settings' => $this->wcpay_gateway->get_option( 'advanced_fraud_protection_settings' ), 'is_migrating_stripe_billing' => $is_migrating_stripe_billing ?? false, @@ -542,6 +547,7 @@ public function update_settings( WP_REST_Request $request ) { $this->update_payment_request_appearance( $request ); $this->update_is_saved_cards_enabled( $request ); $this->update_is_woopay_enabled( $request ); + $this->update_reporting_export_language( $request ); $this->update_woopay_store_logo( $request ); $this->update_woopay_custom_message( $request ); $this->update_woopay_enabled_locations( $request ); @@ -1059,4 +1065,19 @@ private function get_avs_check_enabled( array $ruleset_config ) { return $avs_check_enabled; } + + /** + * Updates the "reporting_export_language" setting. + * + * @param WP_REST_Request $request Request object. + */ + private function update_reporting_export_language( WP_REST_Request $request ) { + if ( ! $request->has_param( 'reporting_export_language' ) ) { + return; + } + + $reporting_export_language = $request->get_param( 'reporting_export_language' ); + + $this->wcpay_gateway->update_option( 'reporting_export_language', $reporting_export_language ); + } } diff --git a/includes/admin/class-wc-rest-payments-transactions-controller.php b/includes/admin/class-wc-rest-payments-transactions-controller.php index 68044ac904c..c3a783943b8 100644 --- a/includes/admin/class-wc-rest-payments-transactions-controller.php +++ b/includes/admin/class-wc-rest-payments-transactions-controller.php @@ -173,9 +173,10 @@ public function get_fraud_outcome_transactions_export( $request ) { public function get_transactions_export( $request ) { $user_email = $request->get_param( 'user_email' ); $deposit_id = $request->get_param( 'deposit_id' ); + $locale = $request->get_param( 'locale' ); $filters = $this->get_transactions_filters( $request ); - return $this->forward_request( 'get_transactions_export', [ $filters, $user_email, $deposit_id ] ); + return $this->forward_request( 'get_transactions_export', [ $filters, $user_email, $deposit_id, $locale ] ); } /** diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index 664c0475a8a..8734677608d 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -993,4 +993,31 @@ public static function enqueue_style( $handle, $path = '', $deps = [], $version } wp_enqueue_style( $handle ); } + + /** + * Returns language data: english name and native name + * + * @param string $language Language code. + * + * @return array + */ + public static function get_language_data( $language ) { + require_once ABSPATH . 'wp-admin/includes/translation-install.php'; + + $translations = wp_get_available_translations(); + + if ( isset( $translations[ $language ] ) ) { + return [ + 'code' => $language, + 'english_name' => $translations[ $language ]['english_name'] ?? $language, + 'native_name' => $translations[ $language ]['native_name'] ?? $language, + ]; + } + + return [ + 'code' => 'en_US', + 'english_name' => 'English (United States)', + 'native_name' => 'English (United States)', + ]; + } } diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php index 18fb4a6a9c2..826b1434be7 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -416,12 +416,13 @@ public function get_fraud_outcome_transactions_export( $request ) { * @param array $filters The filters to be used in the query. * @param string $user_email The email to search for. * @param string $deposit_id The deposit to filter on. + * @param string $locale Site locale. * * @return array Export summary * * @throws API_Exception - Exception thrown on request failure. */ - public function get_transactions_export( $filters = [], $user_email = '', $deposit_id = null ) { + public function get_transactions_export( $filters = [], $user_email = '', $deposit_id = null, $locale = null ) { // Map Order # terms to the actual charge id to be used in the server. if ( ! empty( $filters['search'] ) ) { $filters['search'] = WC_Payments_Utils::map_search_orders_to_charge_ids( $filters['search'] ); @@ -432,6 +433,9 @@ public function get_transactions_export( $filters = [], $user_email = '', $depos if ( ! empty( $deposit_id ) ) { $filters['deposit_id'] = $deposit_id; } + if ( ! empty( $locale ) ) { + $filters['locale'] = $locale; + } return $this->request( $filters, self::TRANSACTIONS_API . '/download', self::POST ); } @@ -578,15 +582,19 @@ public function close_dispute( $dispute_id ) { * * @param array $filters The filters to be used in the query. * @param string $user_email The email to search for. + * @param string $locale Site locale. * * @return array Export summary * * @throws API_Exception - Exception thrown on request failure. */ - public function get_disputes_export( $filters = [], $user_email = '' ) { + public function get_disputes_export( $filters = [], $user_email = '', $locale = null ) { if ( ! empty( $user_email ) ) { $filters['user_email'] = $user_email; } + if ( ! empty( $locale ) ) { + $filters['locale'] = $locale; + } return $this->request( $filters, self::DISPUTES_API . '/download', self::POST ); } @@ -596,15 +604,19 @@ public function get_disputes_export( $filters = [], $user_email = '' ) { * * @param array $filters The filters to be used in the query. * @param string $user_email The email to send export to. + * @param string $locale Site locale. * * @return array Export summary * * @throws API_Exception - Exception thrown on request failure. */ - public function get_deposits_export( $filters = [], $user_email = '' ) { + public function get_deposits_export( $filters = [], $user_email = '', $locale = null ) { if ( ! empty( $user_email ) ) { $filters['user_email'] = $user_email; } + if ( ! empty( $locale ) ) { + $filters['locale'] = $locale; + } return $this->request( $filters, self::DEPOSITS_API . '/download', self::POST ); } diff --git a/tests/js/jest-test-file-setup.js b/tests/js/jest-test-file-setup.js index c4d9395a9a1..7776536fcdc 100644 --- a/tests/js/jest-test-file-setup.js +++ b/tests/js/jest-test-file-setup.js @@ -101,6 +101,16 @@ global.wpApiSettings = { nonce: 'random_wp_rest_nonce', }; +global.wcpaySettings = { + locale: { + code: 'es_ES', + native_name: 'Spanish', + }, + accountLoans: { + loans: [ 'flxln_123456|active' ], + }, +}; + // const config = require( '../../config/development.json' ); // window.wcAdminFeatures = config && config.features ? config.features : {}; From fa5925a7afe0b8b0d262e14013722a3979d2ffe9 Mon Sep 17 00:00:00 2001 From: Guilherme Pressutto diff --git a/client/deposits/list/index.tsx b/client/deposits/list/index.tsx index 51d9e111751..815e5f16658 100644 --- a/client/deposits/list/index.tsx +++ b/client/deposits/list/index.tsx @@ -24,6 +24,7 @@ import { useDispatch } from '@wordpress/data'; * Internal dependencies. */ import { useDeposits, useDepositsSummary } from 'wcpay/data'; +import { useReportingExportLanguage } from 'data/index'; import { displayType, displayStatus } from '../strings'; import { formatExplicitCurrency, formatExportAmount } from 'utils/currency'; import DetailsLink, { getDetailsURL } from 'components/details-link'; @@ -34,6 +35,13 @@ import DownloadButton from 'components/download-button'; import { getDepositsCSV } from 'wcpay/data/deposits/resolvers'; import { applyThousandSeparator } from '../../utils/index.js'; import DepositStatusChip from 'components/deposit-status-chip'; +import { + isExportModalDismissed, + getExportLanguage, + isDefaultSiteLanguage, +} from 'utils'; +import CSVExportModal from 'components/csv-export-modal'; +import { ReportingExportLanguageHook } from 'wcpay/settings/reporting-settings/interfaces'; import './style.scss'; import { parseInt } from 'lodash'; @@ -89,6 +97,10 @@ const getColumns = ( sortByDate?: boolean ): DepositsTableHeader[] => [ ]; export const DepositsList = (): JSX.Element => { + const [ + exportLanguage, + ] = useReportingExportLanguage() as ReportingExportLanguageHook; + const [ isDownloading, setIsDownloading ] = useState( false ); const { createNotice } = useDispatch( 'core/notices' ); const { deposits, isLoading } = useDeposits( getQuery() ); @@ -96,6 +108,8 @@ export const DepositsList = (): JSX.Element => { getQuery() ); + const [ isCSVExportModalOpen, setCSVExportModalOpen ] = useState( false ); + const sortByDate = ! getQuery().orderby || 'date' === getQuery().orderby; const columns = useMemo( () => getColumns( sortByDate ), [ sortByDate ] ); const totalRows = depositsSummary.count || 0; @@ -201,50 +215,51 @@ export const DepositsList = (): JSX.Element => { const downloadable = !! rows.length; - const onDownload = async () => { - setIsDownloading( true ); - const downloadType = totalRows > rows.length ? 'endpoint' : 'browser'; + const endpointExport = async ( language: string ) => { + // We destructure page and path to get the right params. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { page, path, ...params } = getQuery(); const userEmail = wcpaySettings.currentUserEmail; + const locale = getExportLanguage( language, exportLanguage ); - if ( 'endpoint' === downloadType ) { - const { - date_before: dateBefore, - date_after: dateAfter, - date_between: dateBetween, - match, - status_is: statusIs, - status_is_not: statusIsNot, - store_currency_is: storeCurrencyIs, - } = getQuery(); - - const isFiltered = - !! dateBefore || - !! dateAfter || - !! dateBetween || - !! statusIs || - !! statusIsNot || - !! storeCurrencyIs; - - const confirmThreshold = 1000; - const confirmMessage = sprintf( - __( - "You are about to export %d deposits. If you'd like to reduce the size of your export, you can use one or more filters. Would you like to continue?", - 'woocommerce-payments' - ), - totalRows - ); + const { + date_before: dateBefore, + date_after: dateAfter, + date_between: dateBetween, + match, + status_is: statusIs, + status_is_not: statusIsNot, + store_currency_is: storeCurrencyIs, + } = getQuery(); - if ( - isFiltered || - totalRows < confirmThreshold || - window.confirm( confirmMessage ) - ) { - try { - const { - exported_deposits: exportedDeposits, - } = await apiFetch( { + const isFiltered = + !! dateBefore || + !! dateAfter || + !! dateBetween || + !! statusIs || + !! statusIsNot || + !! storeCurrencyIs; + + const confirmThreshold = 1000; + const confirmMessage = sprintf( + __( + "You are about to export %d deposits. If you'd like to reduce the size of your export, you can use one or more filters. Would you like to continue?", + 'woocommerce-payments' + ), + totalRows + ); + + if ( + isFiltered || + totalRows < confirmThreshold || + window.confirm( confirmMessage ) + ) { + try { + const { exported_deposits: exportedDeposits } = await apiFetch( + { path: getDepositsCSV( { userEmail, + locale, dateAfter, dateBefore, dateBetween, @@ -254,33 +269,46 @@ export const DepositsList = (): JSX.Element => { storeCurrencyIs, } ), method: 'POST', - } ); - - createNotice( - 'success', - sprintf( - __( - 'Your export will be emailed to %s', - 'woocommerce-payments' - ), - userEmail - ) - ); - - recordEvent( events.DEPOSITS_DOWNLOAD_CSV_CLICK, { - exported_deposits: exportedDeposits, - total_deposits: exportedDeposits, - download_type: 'endpoint', - } ); - } catch { - createNotice( - 'error', + } + ); + + createNotice( + 'success', + sprintf( __( - 'There was a problem generating your export.', + 'Your export will be emailed to %s', 'woocommerce-payments' - ) - ); - } + ), + userEmail + ) + ); + + recordEvent( events.DEPOSITS_DOWNLOAD_CSV_CLICK, { + exported_deposits: exportedDeposits, + total_deposits: exportedDeposits, + download_type: 'endpoint', + } ); + } catch { + createNotice( + 'error', + __( + 'There was a problem generating your export.', + 'woocommerce-payments' + ) + ); + } + } + }; + + const onDownload = async () => { + setIsDownloading( true ); + const downloadType = totalRows > rows.length ? 'endpoint' : 'browser'; + + if ( 'endpoint' === downloadType ) { + if ( ! isDefaultSiteLanguage() && ! isExportModalDismissed() ) { + setCSVExportModalOpen( true ); + } else { + endpointExport( '' ); } } else { const params = getQuery(); @@ -321,6 +349,16 @@ export const DepositsList = (): JSX.Element => { setIsDownloading( false ); }; + const closeModal = () => { + setCSVExportModalOpen( false ); + }; + + const exportDeposits = ( language: string ) => { + endpointExport( language ); + + closeModal(); + }; + return ( ); }; diff --git a/client/deposits/list/test/index.tsx b/client/deposits/list/test/index.tsx index 078675d5914..c8686254ba6 100644 --- a/client/deposits/list/test/index.tsx +++ b/client/deposits/list/test/index.tsx @@ -14,7 +14,11 @@ import os from 'os'; * Internal dependencies */ import { DepositsList } from '../'; -import { useDeposits, useDepositsSummary } from 'wcpay/data'; +import { + useDeposits, + useDepositsSummary, + useReportingExportLanguage, +} from 'wcpay/data'; import { formatDate, getUnformattedAmount } from 'wcpay/utils/test-utils'; import { CachedDeposit, @@ -26,6 +30,7 @@ import React from 'react'; jest.mock( 'wcpay/data', () => ( { useDeposits: jest.fn(), useDepositsSummary: jest.fn(), + useReportingExportLanguage: jest.fn( () => [ 'en', jest.fn() ] ), } ) ); jest.mock( '@woocommerce/csv-export', () => { @@ -68,6 +73,9 @@ declare const global: { connect: { country: string; }; + reporting?: { + exportModalDismissed: boolean; + }; }; }; @@ -100,6 +108,10 @@ const mockDownloadCSVFile = downloadCSVFile as jest.MockedFunction< typeof downloadCSVFile >; +const mockUseReportingExportLanguage = useReportingExportLanguage as jest.MockedFunction< + typeof useReportingExportLanguage +>; + describe( 'Deposits list', () => { beforeEach( () => { jest.clearAllMocks(); @@ -107,6 +119,8 @@ describe( 'Deposits list', () => { // the query string is preserved across tests, so we need to reset it updateQueryString( {}, '/', {} ); + mockUseReportingExportLanguage.mockReturnValue( [ 'en', jest.fn() ] ); + global.wcpaySettings = { zeroDecimalCurrencies: [], connect: { @@ -123,6 +137,9 @@ describe( 'Deposits list', () => { precision: 2, }, }, + reporting: { + exportModalDismissed: true, + }, }; } ); @@ -329,7 +346,7 @@ describe( 'Deposits list', () => { expect( mockApiFetch ).toHaveBeenCalledWith( { method: 'POST', path: - '/wc/v3/payments/deposits/download?user_email=mock%40example.com', + '/wc/v3/payments/deposits/download?user_email=mock%40example.com&locale=en', } ); } ); } ); diff --git a/client/disputes/index.tsx b/client/disputes/index.tsx index c63cfa49955..f0876f7906a 100644 --- a/client/disputes/index.tsx +++ b/client/disputes/index.tsx @@ -24,7 +24,11 @@ import NoticeOutlineIcon from 'gridicons/dist/notice-outline'; /** * Internal dependencies. */ -import { useDisputes, useDisputesSummary } from 'data/index'; +import { + useDisputes, + useDisputesSummary, + useReportingExportLanguage, +} from 'data/index'; import OrderLink from 'components/order-link'; import DisputeStatusChip from 'components/dispute-status-chip'; import ClickableCell from 'components/clickable-cell'; @@ -39,8 +43,16 @@ import DownloadButton from 'components/download-button'; import disputeStatusMapping from 'components/dispute-status-chip/mappings'; import { CachedDispute, DisputesTableHeader } from 'wcpay/types/disputes'; import { getDisputesCSV } from 'wcpay/data/disputes/resolvers'; -import { applyThousandSeparator } from 'wcpay/utils'; +import { + applyThousandSeparator, + isExportModalDismissed, + getExportLanguage, + isDefaultSiteLanguage, +} from 'wcpay/utils'; +import { useSettings } from 'wcpay/data'; import { isAwaitingResponse } from 'wcpay/disputes/utils'; +import CSVExportModal from 'components/csv-export-modal'; +import { ReportingExportLanguageHook } from 'wcpay/settings/reporting-settings/interfaces'; import './style.scss'; @@ -193,6 +205,9 @@ const smartDueDate = ( dispute: CachedDispute ) => { }; export const DisputesList = (): JSX.Element => { + // pre-fetching the settings. + useSettings(); + const [ isDownloading, setIsDownloading ] = useState( false ); const { createNotice } = useDispatch( 'core/notices' ); const { disputes, isLoading } = useDisputes( getQuery() ); @@ -201,6 +216,12 @@ export const DisputesList = (): JSX.Element => { getQuery() ); + const [ isCSVExportModalOpen, setCSVExportModalOpen ] = useState( false ); + + const [ + exportLanguage, + ] = useReportingExportLanguage() as ReportingExportLanguageHook; + const headers = getHeaders( getQuery().orderby ); const totalRows = disputesSummary.count || 0; @@ -339,49 +360,49 @@ export const DisputesList = (): JSX.Element => { const downloadable = !! rows.length; - const onDownload = async () => { - setIsDownloading( true ); - const title = __( 'Disputes', 'woocommerce-payments' ); - const downloadType = totalRows > rows.length ? 'endpoint' : 'browser'; + const endpointExport = async ( language: string ) => { + // We destructure page and path to get the right params. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { page, path, ...params } = getQuery(); const userEmail = wcpaySettings.currentUserEmail; - if ( 'endpoint' === downloadType ) { - const { - date_before: dateBefore, - date_after: dateAfter, - date_between: dateBetween, - match, - status_is: statusIs, - status_is_not: statusIsNot, - } = getQuery(); - - const isFiltered = - !! dateBefore || - !! dateAfter || - !! dateBetween || - !! statusIs || - !! statusIsNot; - - const confirmThreshold = 1000; - const confirmMessage = sprintf( - __( - "You are about to export %d disputes. If you'd like to reduce the size of your export, you can use one or more filters. Would you like to continue?", - 'woocommerce-payments' - ), - totalRows - ); + const locale = getExportLanguage( language, exportLanguage ); + const { + date_before: dateBefore, + date_after: dateAfter, + date_between: dateBetween, + match, + status_is: statusIs, + status_is_not: statusIsNot, + } = getQuery(); + + const isFiltered = + !! dateBefore || + !! dateAfter || + !! dateBetween || + !! statusIs || + !! statusIsNot; + + const confirmThreshold = 1000; + const confirmMessage = sprintf( + __( + "You are about to export %d disputes. If you'd like to reduce the size of your export, you can use one or more filters. Would you like to continue?", + 'woocommerce-payments' + ), + totalRows + ); - if ( - isFiltered || - totalRows < confirmThreshold || - window.confirm( confirmMessage ) - ) { - try { - const { - exported_disputes: exportedDisputes, - } = await apiFetch( { + if ( + isFiltered || + totalRows < confirmThreshold || + window.confirm( confirmMessage ) + ) { + try { + const { exported_disputes: exportedDisputes } = await apiFetch( + { path: getDisputesCSV( { userEmail, + locale, dateAfter, dateBefore, dateBetween, @@ -390,33 +411,47 @@ export const DisputesList = (): JSX.Element => { statusIsNot, } ), method: 'POST', - } ); + } + ); - createNotice( - 'success', - sprintf( - __( - 'Your export will be emailed to %s', - 'woocommerce-payments' - ), - userEmail - ) - ); - - recordEvent( events.DISPUTE_DOWNLOAD_CSV_CLICK, { - exported_disputes: exportedDisputes, - total_disputes: exportedDisputes, - download_type: 'endpoint', - } ); - } catch { - createNotice( - 'error', + createNotice( + 'success', + sprintf( __( - 'There was a problem generating your export.', + 'Your export will be emailed to %s', 'woocommerce-payments' - ) - ); - } + ), + userEmail + ) + ); + + recordEvent( events.DISPUTE_DOWNLOAD_CSV_CLICK, { + exported_disputes: exportedDisputes, + total_disputes: exportedDisputes, + download_type: 'endpoint', + } ); + } catch { + createNotice( + 'error', + __( + 'There was a problem generating your export.', + 'woocommerce-payments' + ) + ); + } + } + }; + + const onDownload = async () => { + setIsDownloading( true ); + const title = __( 'Disputes', 'woocommerce-payments' ); + const downloadType = totalRows > rows.length ? 'endpoint' : 'browser'; + + if ( 'endpoint' === downloadType ) { + if ( ! isDefaultSiteLanguage() && ! isExportModalDismissed() ) { + setCSVExportModalOpen( true ); + } else { + endpointExport( '' ); } } else { const csvColumns = [ @@ -504,6 +539,16 @@ export const DisputesList = (): JSX.Element => { disputesSummary.currencies || ( isCurrencyFiltered ? [ getQuery().store_currency_is ?? '' ] : [] ); + const closeModal = () => { + setCSVExportModalOpen( false ); + }; + + const exportDisputes = ( language: string ) => { + endpointExport( language ); + + closeModal(); + }; + return ( @@ -345,6 +383,16 @@ export const DepositsList = (): JSX.Element => { ), ] } /> + { ! isDefaultSiteLanguage() && + ! isExportModalDismissed() && + isCSVExportModalOpen && ( + + ) } ); }; diff --git a/client/disputes/test/index.tsx b/client/disputes/test/index.tsx index f206c96ae02..9233f46817f 100644 --- a/client/disputes/test/index.tsx +++ b/client/disputes/test/index.tsx @@ -11,7 +11,12 @@ import os from 'os'; * Internal dependencies */ import DisputesList from '..'; -import { useDisputes, useDisputesSummary } from 'data/index'; +import { + useDisputes, + useDisputesSummary, + useReportingExportLanguage, + useSettings, +} from 'data/index'; import { formatDate, getUnformattedAmount } from 'wcpay/utils/test-utils'; import React from 'react'; import { @@ -49,6 +54,8 @@ jest.mock( '@wordpress/data', () => ( { jest.mock( 'data/index', () => ( { useDisputes: jest.fn(), useDisputesSummary: jest.fn(), + useReportingExportLanguage: jest.fn( () => [ 'en', jest.fn() ] ), + useSettings: jest.fn(), } ) ); const mockDownloadCSVFile = downloadCSVFile as jest.MockedFunction< @@ -65,6 +72,14 @@ const mockUseDisputesSummary = useDisputesSummary as jest.MockedFunction< typeof useDisputesSummary >; +const mockUseSettings = useSettings as jest.MockedFunction< + typeof useSettings +>; + +const mockUseReportingExportLanguage = useReportingExportLanguage as jest.MockedFunction< + typeof useReportingExportLanguage +>; + declare const global: { wcpaySettings: { zeroDecimalCurrencies: string[]; @@ -82,6 +97,9 @@ declare const global: { precision: number; }; }; + reporting?: { + exportModalDismissed: boolean; + }; }; }; @@ -152,6 +170,14 @@ describe( 'Disputes list', () => { new Date( '2019-11-07T12:33:37.000Z' ).getTime() ); + mockUseReportingExportLanguage.mockReturnValue( [ 'en', jest.fn() ] ); + + mockUseSettings.mockReturnValue( { + isLoading: false, + isSaving: false, + saveSettings: ( a ) => a, + } ); + global.wcpaySettings = { zeroDecimalCurrencies: [], connect: { @@ -168,6 +194,9 @@ describe( 'Disputes list', () => { precision: 2, }, }, + reporting: { + exportModalDismissed: true, + }, }; } ); @@ -258,7 +287,7 @@ describe( 'Disputes list', () => { expect( mockApiFetch ).toHaveBeenCalledWith( { method: 'POST', path: - '/wc/v3/payments/disputes/download?user_email=mock%40example.com', + '/wc/v3/payments/disputes/download?user_email=mock%40example.com&locale=en', } ); } ); } ); diff --git a/client/globals.d.ts b/client/globals.d.ts index e06eaf7a865..377f985ab5d 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -122,6 +122,14 @@ declare global { capabilityRequestNotices: Record< string, boolean >; storeName: string; isNextDepositNoticeDismissed: boolean; + reporting: { + exportModalDismissed?: boolean; + }; + locale: { + code: string; + english_name: string; + native_name: string; + }; }; const wc: { diff --git a/client/settings/reporting-settings/components/export-language/index.tsx b/client/settings/reporting-settings/components/export-language/index.tsx new file mode 100644 index 00000000000..950d43f5584 --- /dev/null +++ b/client/settings/reporting-settings/components/export-language/index.tsx @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import React from 'react'; +import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; +import { SelectControl, ExternalLink } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import interpolateComponents from '@automattic/interpolate-components'; +import { ReportingExportLanguageHook } from '../../interfaces'; +import { useReportingExportLanguage } from 'wcpay/data'; +import { getExportLanguageOptions } from 'wcpay/utils'; + +const ExportLanguage: React.FC = () => { + const [ + exportLanguage, + updateExportLanguage, + ] = useReportingExportLanguage() as ReportingExportLanguageHook; + + const handleExportLanguageChange = ( language: string ) => { + updateExportLanguage( language ); + }; + + return ( + @@ -529,6 +574,16 @@ export const DisputesList = (): JSX.Element => { ), ] } /> + { ! isDefaultSiteLanguage() && + ! isExportModalDismissed() && + isCSVExportModalOpen && ( + + ) } ++ ); +}; + +export default ExportLanguage; diff --git a/client/settings/reporting-settings/components/index.ts b/client/settings/reporting-settings/components/index.ts new file mode 100644 index 00000000000..3c3360bd22a --- /dev/null +++ b/client/settings/reporting-settings/components/index.ts @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import ExportLanguage from './export-language'; + +export { ExportLanguage }; diff --git a/client/settings/reporting-settings/index.tsx b/client/settings/reporting-settings/index.tsx new file mode 100644 index 00000000000..9e2ee4a0178 --- /dev/null +++ b/client/settings/reporting-settings/index.tsx @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import React from 'react'; +import { __ } from '@wordpress/i18n'; +import { Card } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import CardBody from '../card-body'; +import { ExportLanguage } from './components'; +import './style.scss'; + +const Reporting: React.FC = () => { + return ( + <> ++ + { interpolateComponents( { + mixedString: __( + 'You can change your global site language preferences in {{learnMoreLink}}General Settings{{/learnMoreLink}}.', + 'woocommerce-payments' + ), + components: { + learnMoreLink: ( + // eslint-disable-next-line max-len +
++ ), + }, + } ) } + + + > + ); +}; + +export default Reporting; diff --git a/client/settings/reporting-settings/interfaces.ts b/client/settings/reporting-settings/interfaces.ts new file mode 100644 index 00000000000..22f37cb1e7b --- /dev/null +++ b/client/settings/reporting-settings/interfaces.ts @@ -0,0 +1,4 @@ +export type ReportingExportLanguageHook = [ + string, + ( language: string ) => void +]; diff --git a/client/settings/reporting-settings/style.scss b/client/settings/reporting-settings/style.scss new file mode 100644 index 00000000000..354c2530e61 --- /dev/null +++ b/client/settings/reporting-settings/style.scss @@ -0,0 +1,17 @@ +.reporting-settings { + &__text--help-text { + font-size: 12px; + color: #757575; + margin: 0; + display: inline-block; + } + + .reporting-export-language { + flex-wrap: wrap; + margin-bottom: 0; + .components-select-control { + padding-right: 16px; + margin-bottom: 0; + } + } +} diff --git a/client/settings/reporting-settings/test/__snapshots__/index.test.js.snap b/client/settings/reporting-settings/test/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..aef73119cb9 --- /dev/null +++ b/client/settings/reporting-settings/test/__snapshots__/index.test.js.snap @@ -0,0 +1,151 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Reporting Settings should render correctly 1`] = ` ++ ++ + { __( + 'Report exporting default language', + 'woocommerce-payments' + ) } + +
++ ++`; diff --git a/client/settings/reporting-settings/test/index.test.js b/client/settings/reporting-settings/test/index.test.js new file mode 100644 index 00000000000..ef0a2437145 --- /dev/null +++ b/client/settings/reporting-settings/test/index.test.js @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import { render, screen, within } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import Reporting from '..'; +import { useReportingExportLanguage } from 'wcpay/data'; + +jest.mock( '@wordpress/data' ); + +jest.mock( 'wcpay/data', () => ( { + useReportingExportLanguage: jest.fn(), +} ) ); + +describe( 'Reporting Settings', () => { + beforeEach( () => { + useReportingExportLanguage.mockReturnValue( [ 'en_US', jest.fn() ] ); + + global.wcpaySettings = { + locale: { + code: 'es_ES', + native_name: 'Spanish', + }, + }; + } ); + + it( 'should render correctly', () => { + const { container } = render(++++ + ++++ + Report exporting default language + +
++++++++++ +++ + +++ ++ + ++ You can change your global site language preferences in + + General Settings + + . + + +
+); + expect( container ).toMatchSnapshot(); + + expect( + screen.getByText( /Report exporting default language/ ) + ).toBeInTheDocument(); + expect( + screen.getByText( + /You can change your global site language preferences/ + ) + ).toBeInTheDocument(); + } ); + + it( 'renders the language select', () => { + render( ); + + const languageSelect = screen.getByLabelText( /Language/ ); + expect( languageSelect ).toHaveValue( 'en_US' ); + + within( languageSelect ).getByRole( 'option', { name: /English/ } ); + within( languageSelect ).getByRole( 'option', { + name: /Site Language - Spanish/, + } ); + } ); +} ); diff --git a/client/settings/settings-manager/index.js b/client/settings/settings-manager/index.js index 8204bad0ac9..858d9346404 100644 --- a/client/settings/settings-manager/index.js +++ b/client/settings/settings-manager/index.js @@ -15,6 +15,7 @@ import PaymentMethods from '../../payment-methods'; import ExpressCheckout from '../express-checkout'; import SettingsSection from '../settings-section'; import GeneralSettings from '../general-settings'; +import ReportingSettings from '../reporting-settings'; import SettingsLayout from '../settings-layout'; import SaveSettingsSection from '../save-settings-section'; import Transactions from '../transactions'; @@ -23,6 +24,7 @@ import LoadableSettingsSection from '../loadable-settings-section'; import ErrorBoundary from '../../components/error-boundary'; import { useDepositDelayDays, useSettings } from '../../data'; import FraudProtection from '../fraud-protection'; +import { isDefaultSiteLanguage } from 'wcpay/utils'; const PaymentMethodsDescription = () => ( <> @@ -132,6 +134,20 @@ const FraudProtectionDescription = () => { ); }; +const ReportingDescription = () => { + return ( + <> + { __( 'Reporting', 'woocommerce-payments' ) }
++ { __( + 'Adjust your report exporting language preferences.', + 'woocommerce-payments' + ) } +
+ > + ); +}; + const AdvancedDescription = () => { return ( <> @@ -249,6 +265,18 @@ const SettingsManager = () => { + { ! isDefaultSiteLanguage() && ( ++ + ) }+ ++ ++ getColumns( @@ -586,6 +603,113 @@ export const TransactionsList = ( const downloadable = !! rows.length; + const endpointExport = async ( language: string ) => { + // We destructure page and path to get the right params. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { page, path, ...params } = getQuery(); + const userEmail = wcpaySettings.currentUserEmail; + + const locale = getExportLanguage( language, exportLanguage ); + const { + date_after: dateAfter, + date_before: dateBefore, + date_between: dateBetween, + match, + search, + type_is: typeIs, + type_is_not: typeIsNot, + source_device_is: sourceDeviceIs, + source_device_is_not: sourceDeviceIsNot, + channel_is: channelIs, + channel_is_not: channelIsNot, + customer_country_is: customerCountryIs, + customer_country_is_not: customerCountryIsNot, + risk_level_is: riskLevelIs, + risk_level_is_not: riskLevelIsNot, + customer_currency_is: customerCurrencyIs, + customer_currency_is_not: customerCurrencyIsNot, + } = params; + const depositId = props.depositId; + + const isFiltered = + !! dateAfter || + !! dateBefore || + !! dateBetween || + !! search || + !! typeIs || + !! typeIsNot || + !! channelIs || + !! channelIsNot || + !! customerCountryIs || + !! customerCountryIsNot || + !! riskLevelIs || + !! riskLevelIsNot || + !! sourceDeviceIs || + !! sourceDeviceIsNot; + + const confirmThreshold = 10000; + const confirmMessage = sprintf( + __( + "You are about to export %d transactions. If you'd like to reduce the size of your export, you can use one or more filters. Would you like to continue?", + 'woocommerce-payments' + ), + totalRows + ); + + if ( + isFiltered || + totalRows < confirmThreshold || + window.confirm( confirmMessage ) + ) { + try { + await apiFetch( { + path: getTransactionsCSV( { + userEmail, + locale, + dateAfter, + dateBefore, + dateBetween, + match, + search, + typeIs, + typeIsNot, + sourceDeviceIs, + sourceDeviceIsNot, + customerCurrencyIs, + customerCurrencyIsNot, + channelIs, + channelIsNot, + customerCountryIs, + customerCountryIsNot, + riskLevelIs, + riskLevelIsNot, + depositId, + } ), + method: 'POST', + } ); + + createNotice( + 'success', + sprintf( + __( + 'Your export will be emailed to %s', + 'woocommerce-payments' + ), + userEmail + ) + ); + } catch { + createNotice( + 'error', + __( + 'There was a problem generating your export.', + 'woocommerce-payments' + ) + ); + } + } + }; + const onDownload = async () => { setIsDownloading( true ); @@ -593,7 +717,6 @@ export const TransactionsList = ( // eslint-disable-next-line @typescript-eslint/no-unused-vars const { page, path, ...params } = getQuery(); const downloadType = totalRows > rows.length ? 'endpoint' : 'browser'; - const userEmail = wcpaySettings.currentUserEmail; recordEvent( events.TRANSACTIONS_DOWNLOAD_CSV_CLICK, { location: props.depositId ? 'deposit_details' : 'transactions', @@ -603,102 +726,10 @@ export const TransactionsList = ( } ); if ( 'endpoint' === downloadType ) { - const { - date_after: dateAfter, - date_before: dateBefore, - date_between: dateBetween, - match, - search, - type_is: typeIs, - type_is_not: typeIsNot, - source_device_is: sourceDeviceIs, - source_device_is_not: sourceDeviceIsNot, - channel_is: channelIs, - channel_is_not: channelIsNot, - customer_country_is: customerCountryIs, - customer_country_is_not: customerCountryIsNot, - risk_level_is: riskLevelIs, - risk_level_is_not: riskLevelIsNot, - customer_currency_is: customerCurrencyIs, - customer_currency_is_not: customerCurrencyIsNot, - } = params; - const depositId = props.depositId; - - const isFiltered = - !! dateAfter || - !! dateBefore || - !! dateBetween || - !! search || - !! typeIs || - !! typeIsNot || - !! channelIs || - !! channelIsNot || - !! customerCountryIs || - !! customerCountryIsNot || - !! riskLevelIs || - !! riskLevelIsNot || - !! sourceDeviceIs || - !! sourceDeviceIsNot; - - const confirmThreshold = 10000; - const confirmMessage = sprintf( - __( - "You are about to export %d transactions. If you'd like to reduce the size of your export, you can use one or more filters. Would you like to continue?", - 'woocommerce-payments' - ), - totalRows - ); - - if ( - isFiltered || - totalRows < confirmThreshold || - window.confirm( confirmMessage ) - ) { - try { - await apiFetch( { - path: getTransactionsCSV( { - userEmail, - dateAfter, - dateBefore, - dateBetween, - match, - search, - typeIs, - typeIsNot, - sourceDeviceIs, - sourceDeviceIsNot, - customerCurrencyIs, - customerCurrencyIsNot, - channelIs, - channelIsNot, - customerCountryIs, - customerCountryIsNot, - riskLevelIs, - riskLevelIsNot, - depositId, - } ), - method: 'POST', - } ); - - createNotice( - 'success', - sprintf( - __( - 'Your export will be emailed to %s', - 'woocommerce-payments' - ), - userEmail - ) - ); - } catch { - createNotice( - 'error', - __( - 'There was a problem generating your export.', - 'woocommerce-payments' - ) - ); - } + if ( ! isDefaultSiteLanguage() && ! isExportModalDismissed() ) { + setCSVExportModalOpen( true ); + } else { + endpointExport( '' ); } } else { downloadCSVFile( @@ -777,6 +808,16 @@ export const TransactionsList = ( } } + const closeModal = () => { + setCSVExportModalOpen( false ); + }; + + const exportTransactions = ( language: string ) => { + endpointExport( language ); + + closeModal(); + }; + const showFilters = ! props.depositId; const storeCurrencies = transactionsSummary.store_currencies || @@ -827,6 +868,17 @@ export const TransactionsList = ( ), ] } /> + + { ! isDefaultSiteLanguage() && + ! isExportModalDismissed() && + isCSVExportModalOpen && ( + + ) } Date: Thu, 8 Feb 2024 13:52:16 -0300 Subject: [PATCH 43/52] Displaying the correct method name in Order Edit page for HPOS (#8150) --- changelog/fix-payment-method-name-hpos | 4 +++ ...ayments-payment-request-button-handler.php | 28 ++++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 changelog/fix-payment-method-name-hpos diff --git a/changelog/fix-payment-method-name-hpos b/changelog/fix-payment-method-name-hpos new file mode 100644 index 00000000000..d78496ec4be --- /dev/null +++ b/changelog/fix-payment-method-name-hpos @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Displaying the correct method name in Order Edit page for HPOS diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index 8463e1aed89..536973e46a3 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -409,13 +409,7 @@ public function get_cart_data() { * @param string $id Gateway ID. */ public function filter_gateway_title( $title, $id ) { - global $post; - - if ( ! is_object( $post ) ) { - return $title; - } - - $order = wc_get_order( $post->ID ); + $order = $this->get_current_order(); $method_title = is_object( $order ) ? $order->get_payment_method_title() : ''; if ( 'woocommerce_payments' === $id && ! empty( $method_title ) ) { @@ -431,6 +425,26 @@ public function filter_gateway_title( $title, $id ) { return $title; } + /** + * Used to get the order in admin edit page. + * + * @return WC_Order|WC_Order_Refund|bool + */ + private function get_current_order() { + global $theorder; + global $post; + + if ( is_object( $theorder ) ) { + return $theorder; + } + + if ( is_object( $post ) ) { + return wc_get_order( $post->ID ); + } + + return false; + } + /** * Normalizes postal code in case of redacted data from Apple Pay. * From 4707df67dd2784a5d49f77817b224bdb0bba33ab Mon Sep 17 00:00:00 2001 From: James Allan Date: Fri, 9 Feb 2024 12:12:53 +1000 Subject: [PATCH 44/52] Skip the log file life extension unit test on WC after v8.6.0 (#8159) --- changelog/fix-issue-8131 | 5 +++++ ...ass-wc-payments-subscription-migration-log-handler.php | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 changelog/fix-issue-8131 diff --git a/changelog/fix-issue-8131 b/changelog/fix-issue-8131 new file mode 100644 index 00000000000..e3a59a93770 --- /dev/null +++ b/changelog/fix-issue-8131 @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Fixes a php unit test and so no customer facing + + diff --git a/tests/unit/subscriptions/test-class-wc-payments-subscription-migration-log-handler.php b/tests/unit/subscriptions/test-class-wc-payments-subscription-migration-log-handler.php index 7a0a7153e7d..d105707da38 100644 --- a/tests/unit/subscriptions/test-class-wc-payments-subscription-migration-log-handler.php +++ b/tests/unit/subscriptions/test-class-wc-payments-subscription-migration-log-handler.php @@ -72,6 +72,14 @@ public function test_log() { * Confirms that log files are not deleted by WC's log cleanup and that mock log files are deleted. */ public function test_extend_life_of_migration_file_logs() { + + // WC 8.6 changed the way log files are cleaned up, this test which uses the `touch()` method is no longer valid. + if ( version_compare( WC_VERSION, '8.6.0', '>=' ) ) { + $this->markTestSkipped( + 'This test only applies on WC versions prior to 8.6.0.' + ); + } + $message = 'Test message 1234567890'; // Log messages - Log to the migration file and a dummy log. From 9bfa935f9c4f10343ce1955477d6ef1101f539a6 Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Fri, 9 Feb 2024 07:39:05 +0000 Subject: [PATCH 45/52] New Tracking events and passing more tracking parameters (#8127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ismael Martín Alabarce --- changelog/dev-client-tracking-events | 4 + client/connect-account-page/index.tsx | 10 -- client/globals.d.ts | 26 ++++- client/onboarding/steps/test/mode-choice.tsx | 4 + client/onboarding/tracking.ts | 8 +- .../overview/task-list/tasks/dispute-task.tsx | 2 +- .../payment-gateways-confirmation.js | 6 + client/settings/deposits/index.js | 2 +- client/settings/general-settings/index.js | 12 +- client/tracks/events.ts | 1 + client/tracks/index.ts | 17 ++- includes/admin/class-wc-payments-admin.php | 24 +--- includes/class-wc-payments-account.php | 110 ++++++++++++++++-- .../multi-currency/SettingsOnboardCta.php | 6 +- tests/js/jest-test-file-setup.js | 7 ++ .../admin/test-class-wc-payments-admin.php | 77 ------------ tests/unit/test-class-wc-payments-account.php | 88 ++++++++++++++ 17 files changed, 267 insertions(+), 137 deletions(-) create mode 100644 changelog/dev-client-tracking-events diff --git a/changelog/dev-client-tracking-events b/changelog/dev-client-tracking-events new file mode 100644 index 00000000000..b76830c2dfc --- /dev/null +++ b/changelog/dev-client-tracking-events @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Some minor update to tracking parameters to pass additional data like Woo store ID. diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx index 48b3dc85360..4f76afeab0f 100644 --- a/client/connect-account-page/index.tsx +++ b/client/connect-account-page/index.tsx @@ -55,11 +55,6 @@ const ConnectAccountPage: React.FC = () => { ...( incentive && { incentive_id: incentive.id, } ), - woo_country_code: - wcSettings?.preloadSettings?.general - ?.woocommerce_default_country || - wcSettings?.admin?.preloadSettings?.general - ?.woocommerce_default_country, } ); // We only want to run this once. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -104,11 +99,6 @@ const ConnectAccountPage: React.FC = () => { ...( incentive && { incentive_id: incentive.id, } ), - woo_country_code: - wcSettings?.preloadSettings?.general - ?.woocommerce_default_country || - wcSettings?.admin?.preloadSettings?.general - ?.woocommerce_default_country, } ); // If there is an incentive available, request promo activation before redirecting. diff --git a/client/globals.d.ts b/client/globals.d.ts index 377f985ab5d..c1ceae3212f 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -9,6 +9,7 @@ import type { declare global { const wcpaySettings: { + version: string; connectUrl: string; isSubscriptionsActive: boolean; featureFlags: { @@ -149,5 +150,28 @@ declare global { ) => void; }; - const wcSettings: Record< string, any >; + const wcSettings: { + admin: { + onboarding: { + profile: { + wccom_connected: boolean; + }; + }; + currentUserData: { + first_name: string; + }; + preloadSettings: { + general: { + woocommerce_allowed_countries: string; + woocommerce_all_except_countries: string[]; + woocommerce_specific_allowed_countries: string[]; + woocommerce_default_country: string; + }; + }; + }; + adminUrl: string; + countries: Record< string, string >; + homeUrl: string; + siteTitle: string; + }; } diff --git a/client/onboarding/steps/test/mode-choice.tsx b/client/onboarding/steps/test/mode-choice.tsx index d6400d98936..575b03b397c 100644 --- a/client/onboarding/steps/test/mode-choice.tsx +++ b/client/onboarding/steps/test/mode-choice.tsx @@ -45,6 +45,10 @@ describe( 'ModeChoice', () => { } ); it( 'calls nextStep by clicking continue when `live` is selected', () => { + global.wcpaySettings = { + connectUrl: 'https://wcpay.test/connect', + devMode: true, + }; nextStep = jest.fn(); render( ); diff --git a/client/onboarding/tracking.ts b/client/onboarding/tracking.ts index 94ebed5a7cc..f2840d5d930 100644 --- a/client/onboarding/tracking.ts +++ b/client/onboarding/tracking.ts @@ -26,7 +26,7 @@ const stepElapsed = () => { export const trackStarted = (): void => { startTime = stepStartTime = Date.now(); - recordEvent( events.ONBOARDING_FLOW_STARTED, {} ); + recordEvent( events.ONBOARDING_FLOW_STARTED ); }; export const trackModeSelected = ( mode: string ): void => { @@ -55,12 +55,14 @@ export const trackRedirected = ( isEligible: boolean ): void => { }; export const trackAccountReset = (): void => - recordEvent( events.ONBOARDING_FLOW_RESET, {} ); + recordEvent( events.ONBOARDING_FLOW_RESET ); export const trackEligibilityModalClosed = ( action: 'dismiss' | 'setup_deposits' | 'enable_payments_only' ): void => - recordEvent( events.ONBOARDING_FLOW_ELIGIBILITY_MODAL_CLOSED, { action } ); + recordEvent( events.ONBOARDING_FLOW_ELIGIBILITY_MODAL_CLOSED, { + action, + } ); export const useTrackAbandoned = (): { trackAbandoned: ( method: 'hide' | 'exit' ) => void; diff --git a/client/overview/task-list/tasks/dispute-task.tsx b/client/overview/task-list/tasks/dispute-task.tsx index da789e7d733..029204cc044 100644 --- a/client/overview/task-list/tasks/dispute-task.tsx +++ b/client/overview/task-list/tasks/dispute-task.tsx @@ -13,7 +13,7 @@ import type { TaskItemProps } from '../types'; import type { CachedDispute } from 'wcpay/types/disputes'; import { formatCurrency } from 'wcpay/utils/currency'; import { getAdminUrl } from 'wcpay/utils'; -import { recordEvent, events } from 'wcpay/tracks'; +import { recordEvent, events } from 'tracks'; import { isDueWithin } from 'wcpay/disputes/utils'; /** diff --git a/client/payment-gateways/payment-gateways-confirmation.js b/client/payment-gateways/payment-gateways-confirmation.js index eea42b86f3f..275c2c1046e 100644 --- a/client/payment-gateways/payment-gateways-confirmation.js +++ b/client/payment-gateways/payment-gateways-confirmation.js @@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; */ import DisableConfirmationModal from './disable-confirmation-modal'; import { useSettings } from 'wcpay/data'; +import { recordEvent, events } from 'tracks'; const PaymentGatewaysConfirmation = () => { // pre-fetching the settings (and available payment methods) _before_ the modal is displayed, @@ -27,6 +28,11 @@ const PaymentGatewaysConfirmation = () => { jQuery( 'tr[data-gateway_id="woocommerce_payments"] .wc-payment-gateway-method-toggle-enabled' ).trigger( 'click' ); + + recordEvent( events.GATEWAY_TOGGLE, { + action: 'disable', + context: 'wc-payments-settings', + } ); }, [ setIsConfirmationModalVisible ] ); const handleDialogDismissal = useCallback( () => { diff --git a/client/settings/deposits/index.js b/client/settings/deposits/index.js index d7989da49d6..a6cf3b0a195 100644 --- a/client/settings/deposits/index.js +++ b/client/settings/deposits/index.js @@ -23,7 +23,7 @@ import { useDepositRestrictions, } from '../../data'; import './style.scss'; -import { recordEvent, events } from 'wcpay/tracks'; +import { recordEvent, events } from 'tracks'; import InlineNotice from 'components/inline-notice'; const daysOfWeek = [ diff --git a/client/settings/general-settings/index.js b/client/settings/general-settings/index.js index cdfbd054211..2635dcead11 100644 --- a/client/settings/general-settings/index.js +++ b/client/settings/general-settings/index.js @@ -14,6 +14,7 @@ import CardBody from '../card-body'; import InlineNotice from 'wcpay/components/inline-notice'; import SetupLivePaymentsModal from 'wcpay/overview/modal/setup-live-payments'; import TestModeConfirmationModal from './test-mode-confirm-modal'; +import { recordEvent, events } from 'tracks'; const GeneralSettings = () => { const [ isWCPayEnabled, setIsWCPayEnabled ] = useIsWCPayEnabled(); @@ -22,13 +23,22 @@ const GeneralSettings = () => { const isDevModeEnabled = useDevMode(); const [ testModeModalVisible, setTestModeModalVisible ] = useState( false ); + const handleWcpayEnabledChange = ( enableWCPay ) => { + setIsWCPayEnabled( enableWCPay ); + + recordEvent( events.GATEWAY_TOGGLE, { + action: enableWCPay ? 'enable' : 'disable', + context: 'wcpay-settings', + } ); + }; + return ( <> wcTracks.isEnabled; /** * Records site event. * + * By default Woo adds `url`, `blog_lang`, `blog_id`, `store_id`, `products_count`, and `wc_version` + * properties to every event. + * * @param {string} eventName Name of the event. * @param {Object} [eventProperties] Event properties (optional). */ @@ -27,9 +30,17 @@ export const recordEvent = ( eventName: string, eventProperties: Record< string, unknown > = {} ): void => { - // Add `is_test_mode` property to every event. - eventProperties.is_test_mode = wcpaySettings?.testMode; - + // TODO: Load these properties in a new script to ensure it's available everywhere. + // wcpaySettings is not available outside of WCPay pages. + if ( window.wcpaySettings ) { + // Add default properties to every event. + Object.assign( eventProperties, { + is_test_mode: wcpaySettings.testMode, + jetpack_connected: wcpaySettings.isJetpackConnected, + wcpay_version: wcpaySettings.version, + woo_country_code: wcpaySettings.connect.country, + } ); + } // Wc-admin track script is enqueued after ours, wrap in domReady // to make sure we're not too early. domReady( () => { diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index be2f2631440..6989ba85b6e 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -205,7 +205,6 @@ public function init_hooks() { add_action( 'admin_menu', [ $this, 'add_payments_menu' ], 0 ); add_action( 'admin_init', [ $this, 'maybe_redirect_to_onboarding' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic. add_action( 'admin_enqueue_scripts', [ $this, 'maybe_redirect_overview_to_connect' ], 1 ); // Run this late (after `admin_init`) but before any scripts are actually enqueued. - add_action( 'admin_enqueue_scripts', [ $this, 'maybe_redirect_onboarding_flow_to_connect' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'register_payments_scripts' ], 9 ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_payments_scripts' ], 9 ); add_action( 'woocommerce_admin_field_payment_gateways', [ $this, 'payment_gateways_container' ] ); @@ -840,6 +839,7 @@ private function get_js_settings(): array { } $this->wcpay_js_settings = [ + 'version' => WCPAY_VERSION_NUMBER, 'connectUrl' => $connect_url, 'connect' => [ 'country' => WC()->countries->get_base_country(), @@ -1149,28 +1149,6 @@ public function maybe_redirect_overview_to_connect() { $this->account->redirect_to_onboarding_welcome_page(); } - /** - * Prevent access to onboarding flow if the server is not connected. - * Redirect back to the connect page with an error message. - */ - public function maybe_redirect_onboarding_flow_to_connect(): void { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return; - } - $url_params = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification - if ( isset( $url_params['page'] ) && 'wc-admin' === $url_params['page'] - && isset( $url_params['path'] ) && '/payments/onboarding' === $url_params['path'] && ! $this->payments_api_client->is_server_connected() ) { - $this->account->redirect_to_onboarding_welcome_page( - sprintf( - /* translators: %s: WooPayments */ - __( 'Please connect to WordPress.com to start using %s.', 'woocommerce-payments' ), - 'WooPayments' - ) - ); - return; - } - } - /** * Add woopay as a payment method to the edit order on admin. * diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 75a8cc3479d..c5c19a7d815 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -34,7 +34,9 @@ class WC_Payments_Account { const ERROR_MESSAGE_TRANSIENT = 'wcpay_error_message'; const INSTANT_DEPOSITS_REMINDER_ACTION = 'wcpay_instant_deposit_reminder'; const TRACKS_EVENT_ACCOUNT_CONNECT_START = 'wcpay_account_connect_start'; + const TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_START = 'wcpay_account_connect_wpcom_connection_start'; const TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_SUCCESS = 'wcpay_account_connect_wpcom_connection_success'; + const TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_FAILURE = 'wcpay_account_connect_wpcom_connection_failure'; const TRACKS_EVENT_ACCOUNT_CONNECT_FINISHED = 'wcpay_account_connect_finished'; const TRACKS_EVENT_KYC_REMINDER_MERCHANT_RETURNED = 'wcpay_kyc_reminder_merchant_returned'; @@ -100,6 +102,7 @@ public function init_hooks() { add_action( 'admin_init', [ $this, 'maybe_redirect_to_server_link' ] ); add_action( 'admin_init', [ $this, 'maybe_redirect_settings_to_connect_or_overview' ] ); add_action( 'admin_init', [ $this, 'maybe_redirect_onboarding_flow_to_overview' ] ); + add_action( 'admin_init', [ $this, 'maybe_redirect_onboarding_flow_to_connect' ] ); add_action( 'admin_init', [ $this, 'maybe_activate_woopay' ] ); // Add handlers for inbox notes and reminders. @@ -934,6 +937,51 @@ public function maybe_redirect_onboarding_flow_to_overview(): bool { return true; } + /** + * Prevent access to onboarding flow if the server is not connected. + * Redirect back to the connect page with an error message. + * + * @return void + */ + public function maybe_redirect_onboarding_flow_to_connect(): void { + if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { + return; + } + + $params = [ + 'page' => 'wc-admin', + 'path' => '/payments/onboarding', + ]; + + // We're not in the onboarding flow page, don't redirect. + if ( count( $params ) !== count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended + return; + } + + // Server is connected, don't redirect. + if ( $this->payments_api_client->is_server_connected() ) { + return; + } + + $referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ); + + // Track unsuccessful Jetpack connection. + if ( strpos( $referer, 'wordpress.com' ) ) { + $this->tracks_event( + self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_FAILURE, + [ 'mode' => WC_Payments::mode()->is_test() ? 'test' : 'live' ] + ); + } + + $this->redirect_to_onboarding_welcome_page( + sprintf( + /* translators: %s: WooPayments */ + __( 'Please connect to WordPress.com to start using %s.', 'woocommerce-payments' ), + 'WooPayments' + ) + ); + } + /** * Filter function to add Stripe to the list of allowed redirect hosts * @@ -984,6 +1032,9 @@ public function maybe_handle_onboarding() { } if ( isset( $_GET['wcpay-reconnect-wpcom'] ) && check_admin_referer( 'wcpay-reconnect-wpcom' ) ) { + // Track the Jetpack connection start. + $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_START ); + $this->payments_api_client->start_server_connection( WC_Payments_Admin_Settings::get_settings_url() ); return; } @@ -997,10 +1048,8 @@ public function maybe_handle_onboarding() { $test_mode = isset( $_GET['test_mode'] ) ? boolval( wc_clean( wp_unslash( $_GET['test_mode'] ) ) ) : false; $event_properties = [ - 'incentive' => $incentive, - 'is_new_onboarding_flow' => $progressive, - 'woo_country_code' => WC()->countries->get_base_country(), - 'mode' => $test_mode || WC_Payments::mode()->is_test() ? 'test' : 'live', + 'incentive' => $incentive, + 'mode' => $test_mode || WC_Payments::mode()->is_test() ? 'test' : 'live', ]; $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_START, @@ -1052,6 +1101,12 @@ public function maybe_handle_onboarding() { if ( isset( $_GET['wcpay-connect-jetpack-success'] ) ) { if ( ! $this->payments_api_client->is_server_connected() ) { + // Track unsuccessful Jetpack connection. + $this->tracks_event( + self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_FAILURE, + $event_properties + ); + $this->redirect_to_onboarding_welcome_page( sprintf( /* translators: %s: WooPayments */ @@ -1066,10 +1121,8 @@ public function maybe_handle_onboarding() { // Track successful Jetpack connection. $test_mode = isset( $_GET['test_mode'] ) ? boolval( wc_clean( wp_unslash( $_GET['test_mode'] ) ) ) : false; $event_properties = [ - 'incentive' => $incentive, - 'is_new_onboarding_flow' => $progressive, - 'woo_country_code' => WC()->countries->get_base_country(), - 'mode' => $test_mode || WC_Payments::mode()->is_test() ? 'test' : 'live', + 'incentive' => $incentive, + 'mode' => $test_mode || WC_Payments::mode()->is_test() ? 'test' : 'live', ]; $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_SUCCESS, @@ -1163,6 +1216,21 @@ public static function get_payments_task_page_url() { ); } + /** + * Get Connect page url. + * + * @return string + */ + public static function get_connect_page_url(): string { + return add_query_arg( + [ + 'page' => 'wc-admin', + 'path' => '/payments/connect', + ], + admin_url( 'admin.php' ) + ); + } + /** * Get overview page url * @@ -1242,6 +1310,9 @@ private function maybe_init_jetpack_connection( $wcpay_connect_from, $additional return; } + // Track the Jetpack connection start. + $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_START ); + $redirect = add_query_arg( array_merge( [ @@ -1479,10 +1550,8 @@ private function finalize_connection( $state, $mode ) { $incentive = ! empty( $_GET['promo'] ) ? sanitize_text_field( wp_unslash( $_GET['promo'] ) ) : ''; $progressive = ! empty( $_GET['progressive'] ) && 'true' === $_GET['progressive']; $event_properties = [ - 'incentive' => $incentive, - 'is_new_onboarding_flow' => $progressive, - 'woo_country_code' => WC()->countries->get_base_country(), - 'mode' => 'test' === $mode ? 'test' : 'live', + 'incentive' => $incentive, + 'mode' => 'test' === $mode ? 'test' : 'live', ]; $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_FINISHED, @@ -1918,6 +1987,9 @@ private function redirect_to_onboarding_flow_page() { return; } + // Track the Jetpack connection start. + $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_START ); + $onboarding_url = admin_url( 'admin.php?page=wc-admin&path=/payments/onboarding' ); if ( ! $this->payments_api_client->is_server_connected() ) { @@ -1930,12 +2002,26 @@ private function redirect_to_onboarding_flow_page() { /** * Send a Tracks event. * + * By default Woo adds `url`, `blog_lang`, `blog_id`, `store_id`, `products_count`, and `wc_version` + * properties to every event. + * * @param string $name The event name. * @param array $properties Optional. The event custom properties. * * @return void */ private function tracks_event( string $name, array $properties = [] ) { + // Add default properties to every event. + $properties = array_merge( + $properties, + [ + 'is_test_mode' => WC_Payments::mode()->is_test(), + 'jetpack_connected' => $this->payments_api_client->is_server_connected(), + 'wcpay_version' => WCPAY_VERSION_NUMBER, + 'woo_country_code' => WC()->countries->get_base_country(), + ] + ); + if ( ! function_exists( 'wc_admin_record_tracks_event' ) ) { return; } diff --git a/includes/multi-currency/SettingsOnboardCta.php b/includes/multi-currency/SettingsOnboardCta.php index 82d64c27f8e..2dce770fd0b 100644 --- a/includes/multi-currency/SettingsOnboardCta.php +++ b/includes/multi-currency/SettingsOnboardCta.php @@ -53,11 +53,7 @@ public function init_hooks() { * Output the call to action button if needing to onboard. */ public function currencies_settings_onboarding_cta() { - $params = [ - 'page' => 'wc-admin', - 'path' => '/payments/connect', - ]; - $href = admin_url( add_query_arg( $params, 'admin.php' ) ); + $href = \WC_Payments_Account::get_connect_page_url(); ?> diff --git a/tests/js/jest-test-file-setup.js b/tests/js/jest-test-file-setup.js index 7776536fcdc..09400abae27 100644 --- a/tests/js/jest-test-file-setup.js +++ b/tests/js/jest-test-file-setup.js @@ -126,3 +126,10 @@ global.ResizeObserver = jest.fn().mockImplementation( () => ( { unobserve: jest.fn(), disconnect: jest.fn(), } ) ); + +// Mock the tracks module to avoid the need to mock wcpaySettings in every test. +jest.mock( 'tracks', () => ( { + recordEvent: jest.fn(), + isEnabled: jest.fn(), + events: {}, +} ) ); diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php index 1f828d877b8..ca7917968cb 100644 --- a/tests/unit/admin/test-class-wc-payments-admin.php +++ b/tests/unit/admin/test-class-wc-payments-admin.php @@ -400,83 +400,6 @@ public function data_maybe_redirect_overview_to_connect() { ]; } - /** - * @dataProvider data_maybe_redirect_onboarding_flow_to_connect - */ - public function test_maybe_redirect_onboarding_flow_to_connect( $expected_times_redirect_called, $is_server_connected, $get_params ) { - $this->mock_current_user_is_admin(); - $_GET = $get_params; - - $this->mock_api_client - ->method( 'is_server_connected' ) - ->willReturn( $is_server_connected ); - - $this->mock_account - ->expects( $this->exactly( $expected_times_redirect_called ) ) - ->method( 'redirect_to_onboarding_welcome_page' ); - - $this->payments_admin->maybe_redirect_onboarding_flow_to_connect(); - } - - /** - * Data provider for test_maybe_redirect_onboarding_flow_to_connect - */ - public function data_maybe_redirect_onboarding_flow_to_connect() { - return [ - 'no_get_params' => [ - 0, - false, - [], - ], - 'empty_page_param' => [ - 0, - false, - [ - 'path' => '/payments/onboarding', - ], - ], - 'incorrect_page_param' => [ - 0, - false, - [ - 'page' => 'wc-settings', - 'path' => '/payments/onboarding', - ], - ], - 'empty_path_param' => [ - 0, - false, - [ - 'page' => 'wc-admin', - ], - ], - 'incorrect_path_param' => [ - 0, - false, - [ - 'page' => 'wc-admin', - 'path' => '/payments/does-not-exist', - ], - ], - 'server_connected' => [ - 0, - true, - [ - 'page' => 'wc-admin', - 'path' => '/payments/onboarding', - ], - ], - 'happy_path' => [ - 1, - false, - [ - 'page' => 'wc-admin', - 'path' => '/payments/onboarding', - ], - ], - ]; - } - /** * Tests WC_Payments_Admin::add_disputes_notification_badge() */ diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php index a872511ba2d..fac43dacb2c 100644 --- a/tests/unit/test-class-wc-payments-account.php +++ b/tests/unit/test-class-wc-payments-account.php @@ -96,6 +96,7 @@ public function test_filters_registered_properly() { $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_to_server_link' ] ), 'maybe_redirect_to_server_link action does not exist.' ); $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_settings_to_connect_or_overview' ] ), 'maybe_redirect_settings_to_connect_or_overview action does not exist.' ); $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_onboarding_flow_to_overview' ] ), 'maybe_redirect_onboarding_flow_to_overview action does not exist.' ); + $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_redirect_onboarding_flow_to_connect' ] ), 'maybe_redirect_onboarding_flow_to_connect action does not exist.' ); $this->assertNotFalse( has_action( 'admin_init', [ $this->wcpay_account, 'maybe_activate_woopay' ] ), 'maybe_activate_woopay action does not exist.' ); $this->assertNotFalse( has_action( 'woocommerce_payments_account_refreshed', [ $this->wcpay_account, 'handle_instant_deposits_inbox_note' ] ), 'handle_instant_deposits_inbox_note action does not exist.' ); $this->assertNotFalse( has_action( 'woocommerce_payments_account_refreshed', [ $this->wcpay_account, 'handle_loan_approved_inbox_note' ] ), 'handle_loan_approved_inbox_note action does not exist.' ); @@ -433,6 +434,93 @@ public function data_maybe_redirect_onboarding_flow_to_overview() { ]; } + /** + * @dataProvider data_maybe_redirect_onboarding_flow_to_connect + */ + public function test_maybe_redirect_onboarding_flow_to_connect( $expected_times_redirect_called, $is_server_connected, $get_params ) { + wp_set_current_user( 1 ); + $_GET = $get_params; + + $this->mock_api_client = $this->getMockBuilder( 'WC_Payments_API_Client' ) + ->disableOriginalConstructor() + ->getMock(); + + $this->mock_api_client + ->method( 'is_server_connected' ) + ->willReturn( $is_server_connected ); + + // Mock WC_Payments_Account without redirect_to_onboarding_welcome_page to prevent headers already sent error. + $this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class ) + ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service ] ) + ->onlyMethods( [ 'redirect_to_onboarding_welcome_page' ] ) + ->getMock(); + + $this->wcpay_account + ->expects( $this->exactly( $expected_times_redirect_called ) ) + ->method( 'redirect_to_onboarding_welcome_page' ); + + $this->wcpay_account->maybe_redirect_onboarding_flow_to_connect(); + } + + /** + * Data provider for test_maybe_redirect_onboarding_flow_to_connect + */ + public function data_maybe_redirect_onboarding_flow_to_connect() { + return [ + 'no_get_params' => [ + 0, + false, + [], + ], + 'empty_page_param' => [ + 0, + false, + [ + 'path' => '/payments/onboarding', + ], + ], + 'incorrect_page_param' => [ + 0, + false, + [ + 'page' => 'wc-settings', + 'path' => '/payments/onboarding', + ], + ], + 'empty_path_param' => [ + 0, + false, + [ + 'page' => 'wc-admin', + ], + ], + 'incorrect_path_param' => [ + 0, + false, + [ + 'page' => 'wc-admin', + 'path' => '/payments/does-not-exist', + ], + ], + 'server_connected' => [ + 0, + true, + [ + 'page' => 'wc-admin', + 'path' => '/payments/onboarding', + ], + ], + 'happy_path' => [ + 1, + false, + [ + 'page' => 'wc-admin', + 'path' => '/payments/onboarding', + ], + ], + ]; + } + /** * @dataProvider data_maybe_redirect_settings_to_connect_or_overview */ From 2d16b990b9e2d0c78483c830f7ae4d5e044946bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismael=20Mart=C3=ADn=20Alabarce?=
Date: Fri, 9 Feb 2024 12:08:20 +0100 Subject: [PATCH 46/52] Fix WooPayments overriding a WooCommerce analytics style (#8153) --- .../fix-5622-wcpay-overriding-analytics-style | 5 +++ client/deposits/details/style.scss | 36 +++++++++++++++++++ client/style.scss | 36 ------------------- 3 files changed, 41 insertions(+), 36 deletions(-) create mode 100644 changelog/fix-5622-wcpay-overriding-analytics-style diff --git a/changelog/fix-5622-wcpay-overriding-analytics-style b/changelog/fix-5622-wcpay-overriding-analytics-style new file mode 100644 index 00000000000..ea40c6687a6 --- /dev/null +++ b/changelog/fix-5622-wcpay-overriding-analytics-style @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Prevent WooPayments overriding a WooCommerce analytics style + + diff --git a/client/deposits/details/style.scss b/client/deposits/details/style.scss index 01e91c29955..b05e381fde0 100644 --- a/client/deposits/details/style.scss +++ b/client/deposits/details/style.scss @@ -69,4 +69,40 @@ padding: 24px; } } + + /* WC SummaryNumber modifications: some margins, colors and hide of unused fields */ + .woocommerce-summary { + .woocommerce-summary__item-label { + margin-bottom: 12px; + } + .woocommerce-summary__item-data { + margin-top: 0; + margin-bottom: 8px; + } + .wcpay-summary__item-detail { + color: $dark-gray-500; + } + /* Hide unused SummaryNumber fields */ + .woocommerce-summary__item-prev-value, + .woocommerce-summary__item-delta { + display: none; + } + .woocommerce-summary__item { + background: $studio-white; + &:hover .woocommerce-summary__item-label { + color: $gray-700; + } + } + a.woocommerce-summary__item { + &:hover { + background-color: $studio-gray-0; + &:hover .woocommerce-summary__item-label { + color: var( --wp-admin-theme-color ); + } + } + &:active { + background-color: $studio-gray-5; + } + } + } } diff --git a/client/style.scss b/client/style.scss index 4c3f147fc86..2dcdbb2a7dd 100644 --- a/client/style.scss +++ b/client/style.scss @@ -58,42 +58,6 @@ } } -/* SummaryNumber modifications: some margins, colors and hide of unused fields */ -.woocommerce-summary { - .woocommerce-summary__item-label { - margin-bottom: 12px; - } - .woocommerce-summary__item-data { - margin-top: 0; - margin-bottom: 8px; - } - .wcpay-summary__item-detail { - color: $dark-gray-500; - } - /* Hide unused SummaryNumber fields */ - .woocommerce-summary__item-prev-value, - .woocommerce-summary__item-delta { - display: none; - } - .woocommerce-summary__item { - background: $studio-white; - &:hover .woocommerce-summary__item-label { - color: $gray-700; - } - } - a.woocommerce-summary__item { - &:hover { - background-color: $studio-gray-0; - &:hover .woocommerce-summary__item-label { - color: var( --wp-admin-theme-color ); - } - } - &:active { - background-color: $studio-gray-5; - } - } -} - /** * This styling changes the appearance of warning notices to match our designs. * In particular it removes margins that aren't supposed to be present, and From 7b46387e26e19ce18e4fb753ebddf89c3236273d Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Fri, 9 Feb 2024 14:52:36 +0000 Subject: [PATCH 47/52] Using Event Type and string literals for events (#8166) --- changelog/dev-tracking-refactor | 4 + client/card-readers/settings/file-upload.tsx | 8 +- client/checkout/blocks/index.js | 4 +- client/checkout/classic/event-handlers.js | 4 +- client/checkout/woopay/email-input-iframe.js | 10 +- .../woopay-express-checkout-button.js | 8 +- client/components/account-balances/index.tsx | 4 +- client/components/deposits-overview/index.tsx | 6 +- .../components/disputed-order-notice/index.js | 13 +- .../components/banner-actions/index.tsx | 6 +- .../fraud-risk-tools-banner/index.tsx | 4 +- .../components/woopay/save-user/agreement.js | 6 +- .../save-user/checkout-page-save-user.js | 12 +- client/connect-account-page/index.tsx | 6 +- .../info-notice-modal.tsx | 4 +- client/data/multi-currency/actions.js | 4 +- client/deposits/list/index.tsx | 10 +- client/disputes/evidence/index.js | 22 +-- client/disputes/index.tsx | 8 +- client/onboarding/tracking.ts | 18 +-- client/overview/inbox-notifications/index.js | 8 +- .../overview/task-list/tasks/dispute-task.tsx | 4 +- .../dispute-awaiting-response-details.tsx | 12 +- .../dispute-resolution-footer.tsx | 12 +- client/payment-details/index.tsx | 4 +- client/payment-details/summary/index.tsx | 18 +-- .../summary/refund-modal/index.tsx | 4 +- .../payment-gateways-confirmation.js | 4 +- .../blocks/payment-request-express.js | 10 +- client/payment-request/index.js | 10 +- client/settings/deposits/index.js | 5 +- .../express-checkout-settings/file-upload.tsx | 8 +- .../advanced-settings/index.tsx | 6 +- .../components/protection-levels/index.tsx | 6 +- .../settings/fraud-protection/tour/index.tsx | 6 +- client/settings/general-settings/index.js | 4 +- .../settings/save-settings-section/index.js | 4 +- .../subscription-product-onboarding/modal.js | 8 +- client/subscriptions-empty-state/index.js | 8 +- client/tos/request.js | 4 +- client/tracks/event.d.ts | 101 +++++++++++++ client/tracks/events.ts | 133 ------------------ client/tracks/index.ts | 8 +- client/transactions/blocked/index.tsx | 6 +- client/transactions/filters/index.tsx | 4 +- client/transactions/list/index.tsx | 4 +- client/transactions/risk-review/columns.tsx | 11 +- client/transactions/risk-review/index.tsx | 6 +- client/transactions/uncaptured/index.tsx | 6 +- 49 files changed, 281 insertions(+), 304 deletions(-) create mode 100644 changelog/dev-tracking-refactor create mode 100644 client/tracks/event.d.ts delete mode 100644 client/tracks/events.ts diff --git a/changelog/dev-tracking-refactor b/changelog/dev-tracking-refactor new file mode 100644 index 00000000000..bdbd63abfc0 --- /dev/null +++ b/changelog/dev-tracking-refactor @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Refactor to how tracking events are defined for better readability. diff --git a/client/card-readers/settings/file-upload.tsx b/client/card-readers/settings/file-upload.tsx index 7cca71daca2..63fcf597e96 100644 --- a/client/card-readers/settings/file-upload.tsx +++ b/client/card-readers/settings/file-upload.tsx @@ -3,7 +3,7 @@ * External dependencies */ import React from 'react'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import apiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; @@ -71,7 +71,7 @@ const BrandingFileUpload: React.FunctionComponent< CardReaderFileUploadProps > = setLoading( true ); - recordEvent( events.SETTINGS_FILE_UPLOAD_STARTED, { + recordEvent( 'wcpay_merchant_settings_file_upload_started', { type: key, } ); @@ -96,11 +96,11 @@ const BrandingFileUpload: React.FunctionComponent< CardReaderFileUploadProps > = setLoading( false ); setUploadError( false ); - recordEvent( events.SETTINGS_FILE_UPLOAD_SUCCESS, { + recordEvent( 'wcpay_merchant_settings_file_upload_success', { type: key, } ); } catch ( { err } ) { - recordEvent( events.SETTINGS_FILE_UPLOAD_FAILED, { + recordEvent( 'wcpay_merchant_settings_file_upload_success', { message: ( err as Error ).message, } ); diff --git a/client/checkout/blocks/index.js b/client/checkout/blocks/index.js index 6693613e839..f501c169f5c 100644 --- a/client/checkout/blocks/index.js +++ b/client/checkout/blocks/index.js @@ -34,7 +34,7 @@ import { } from '../constants.js'; import { getDeferredIntentCreationUPEFields } from './payment-elements'; import { handleWooPayEmailInput } from '../woopay/email-input-iframe'; -import { recordUserEvent, events } from 'tracks'; +import { recordUserEvent } from 'tracks'; import wooPayExpressCheckoutPaymentMethod from '../woopay/express-button/woopay-express-checkout-payment-method'; import { isPreviewing } from '../preview'; @@ -127,7 +127,7 @@ const addCheckoutTracking = () => { return; } - recordUserEvent( events.PLACE_ORDER_CLICK ); + recordUserEvent( 'checkout_place_order_button_click' ); } ); } }; diff --git a/client/checkout/classic/event-handlers.js b/client/checkout/classic/event-handlers.js index 6615b5859f3..4bbacc9beeb 100644 --- a/client/checkout/classic/event-handlers.js +++ b/client/checkout/classic/event-handlers.js @@ -28,7 +28,7 @@ import WCPayAPI from 'wcpay/checkout/api'; import apiRequest from '../utils/request'; import { handleWooPayEmailInput } from 'wcpay/checkout/woopay/email-input-iframe'; import { isPreviewing } from 'wcpay/checkout/preview'; -import { recordUserEvent, events } from 'tracks'; +import { recordUserEvent } from 'tracks'; jQuery( function ( $ ) { enqueueFraudScripts( getUPEConfig( 'fraudServices' ) ); @@ -83,7 +83,7 @@ jQuery( function ( $ ) { return; } - recordUserEvent( events.PLACE_ORDER_CLICK ); + recordUserEvent( 'checkout_place_order_button_click' ); } ); window.addEventListener( 'hashchange', () => { diff --git a/client/checkout/woopay/email-input-iframe.js b/client/checkout/woopay/email-input-iframe.js index b3966aaaf2d..19f42e006a1 100644 --- a/client/checkout/woopay/email-input-iframe.js +++ b/client/checkout/woopay/email-input-iframe.js @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { getConfig } from 'wcpay/utils/checkout'; -import { recordUserEvent, events } from 'tracks'; +import { recordUserEvent } from 'tracks'; import request from '../utils/request'; import { buildAjaxURL } from '../../payment-request/utils'; import { @@ -340,7 +340,7 @@ export const handleWooPayEmailInput = async ( parentDiv.removeChild( errorMessage ); } - recordUserEvent( events.WOOPAY_EMAIL_CHECK ); + recordUserEvent( 'checkout_email_address_woopay_check' ); request( buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'get_woopay_signature' ), @@ -402,7 +402,7 @@ export const handleWooPayEmailInput = async ( if ( data[ 'user-exists' ] ) { openIframe( email ); } else if ( data.code !== 'rest_invalid_param' ) { - recordUserEvent( events.WOOPAY_OFFERED ); + recordUserEvent( 'checkout_woopay_save_my_info_offered' ); } } ) .catch( ( err ) => { @@ -497,7 +497,7 @@ export const handleWooPayEmailInput = async ( 'woopay-login-session-iframe-wrapper' ); loginSessionIframe.classList.add( 'open' ); - recordUserEvent( events.WOOPAY_AUTO_REDIRECT ); + recordUserEvent( 'checkout_woopay_auto_redirect' ); spinner.remove(); // Do nothing if the iframe has been closed. if ( @@ -621,7 +621,7 @@ export const handleWooPayEmailInput = async ( dispatchUserExistEvent( true ); }, 2000 ); - recordUserEvent( events.WOOPAY_SKIPPED, {}, true ); + recordUserEvent( 'woopay_skipped', {}, true ); searchParams.delete( 'skip_woopay' ); diff --git a/client/checkout/woopay/express-button/woopay-express-checkout-button.js b/client/checkout/woopay/express-button/woopay-express-checkout-button.js index 9431cb1fd70..67d5bb222f8 100644 --- a/client/checkout/woopay/express-button/woopay-express-checkout-button.js +++ b/client/checkout/woopay/express-button/woopay-express-checkout-button.js @@ -12,7 +12,7 @@ import WoopayIcon from './woopay-icon'; import WoopayIconLight from './woopay-icon-light'; import { expressCheckoutIframe } from './express-checkout-iframe'; import useExpressCheckoutProductHandler from './use-express-checkout-product-handler'; -import { recordUserEvent, events } from 'tracks'; +import { recordUserEvent } from 'tracks'; import { getConfig } from 'wcpay/utils/checkout'; import request from 'wcpay/checkout/utils/request'; import { showErrorMessage } from 'wcpay/checkout/woopay/express-button/utils'; @@ -77,7 +77,7 @@ export const WoopayExpressCheckoutButton = ( { useEffect( () => { if ( ! isPreview ) { - recordUserEvent( events.WOOPAY_BUTTON_LOAD, { + recordUserEvent( 'woopay_button_load', { source: context, } ); } @@ -151,7 +151,7 @@ export const WoopayExpressCheckoutButton = ( { return; // eslint-disable-line no-useless-return } - recordUserEvent( events.WOOPAY_BUTTON_CLICK, { + recordUserEvent( 'woopay_button_click', { source: context, } ); @@ -234,7 +234,7 @@ export const WoopayExpressCheckoutButton = ( { return; } - recordUserEvent( events.WOOPAY_BUTTON_CLICK, { + recordUserEvent( 'woopay_button_click', { source: context, } ); diff --git a/client/components/account-balances/index.tsx b/client/components/account-balances/index.tsx index 666e121c6c4..4b222a72c6a 100644 --- a/client/components/account-balances/index.tsx +++ b/client/components/account-balances/index.tsx @@ -16,7 +16,7 @@ import BalanceBlock from './balance-block'; import BalanceTooltip from './balance-tooltip'; import { documentationUrls, fundLabelStrings } from './strings'; import InstantDepositButton from 'deposits/instant-deposits'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import type * as AccountOverview from 'wcpay/types/account-overview'; import './style.scss'; @@ -57,7 +57,7 @@ const AccountBalances: React.FC = () => { const onTabSelect = ( tabName: BalanceTabProps[ 'name' ] ) => { setSelectedCurrency( tabName ); - recordEvent( events.OVERVIEW_BALANCES_CURRENCY_CLICK, { + recordEvent( 'wcpay_overview_balances_currency_tab_click', { selected_currency: tabName, } ); }; diff --git a/client/components/deposits-overview/index.tsx b/client/components/deposits-overview/index.tsx index 99bf419f0ed..d0cc48da3bd 100644 --- a/client/components/deposits-overview/index.tsx +++ b/client/components/deposits-overview/index.tsx @@ -16,7 +16,7 @@ import { __ } from '@wordpress/i18n'; */ import { getAdminUrl } from 'wcpay/utils'; import { formatExplicitCurrency } from 'wcpay/utils/currency'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import Loadable from 'components/loadable'; import { useSelectedCurrencyOverview } from 'wcpay/overview/hooks'; import RecentDepositsList from './recent-deposits-list'; @@ -177,7 +177,7 @@ const DepositsOverview: React.FC = () => { } ) } onClick={ () => recordEvent( - events.OVERVIEW_DEPOSITS_VIEW_HISTORY_CLICK + 'wcpay_overview_deposits_view_history_click' ) } > @@ -200,7 +200,7 @@ const DepositsOverview: React.FC = () => { } onClick={ () => recordEvent( - events.OVERVIEW_DEPOSITS_CHANGE_SCHEDULE_CLICK + 'wcpay_overview_deposits_change_schedule_click' ) } > diff --git a/client/components/disputed-order-notice/index.js b/client/components/disputed-order-notice/index.js index 539cb0b16d6..5e0ec8ca6e8 100644 --- a/client/components/disputed-order-notice/index.js +++ b/client/components/disputed-order-notice/index.js @@ -18,7 +18,7 @@ import { isUnderReview, } from 'wcpay/disputes/utils'; import { useCharge } from 'wcpay/data'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import './style.scss'; const DisputedOrderNoticeHandler = ( { chargeId, onDisableOrderRefund } ) => { @@ -201,7 +201,7 @@ const DisputeNeedsResponseNotice = ( { disputeDetailsUrl, } ) => { useEffect( () => { - recordEvent( events.DISPUTE_NOTICE_VIEW, { + recordEvent( 'wcpay_order_dispute_notice_view', { is_inquiry: isPreDisputeInquiry, dispute_reason: disputeReason, due_by_days: countdownDays, @@ -241,9 +241,12 @@ const DisputeNeedsResponseNotice = ( { label: buttonLabel, variant: 'secondary', onClick: () => { - recordEvent( events.DISPUTE_NOTICE_CLICK, { - due_by_days: countdownDays, - } ); + recordEvent( + 'wcpay_order_dispute_notice_action_click', + { + due_by_days: countdownDays, + } + ); window.location = disputeDetailsUrl; }, }, diff --git a/client/components/fraud-risk-tools-banner/components/banner-actions/index.tsx b/client/components/fraud-risk-tools-banner/components/banner-actions/index.tsx index d75823e7bc0..8c96220a23e 100644 --- a/client/components/fraud-risk-tools-banner/components/banner-actions/index.tsx +++ b/client/components/fraud-risk-tools-banner/components/banner-actions/index.tsx @@ -8,7 +8,7 @@ import { Button } from '@wordpress/components'; /** * Internal dependencies */ -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; interface BannerActionsProps { handleDontShowAgainOnClick: () => void; @@ -18,7 +18,9 @@ const BannerActions: React.FC< BannerActionsProps > = ( { handleDontShowAgainOnClick, } ) => { const handleLearnMoreButtonClick = () => { - recordEvent( events.FRAUD_PROTECTION_BANNER_LEARN_MORE_CLICKED ); + recordEvent( + 'wcpay_fraud_protection_banner_learn_more_button_clicked' + ); }; return ( diff --git a/client/components/fraud-risk-tools-banner/index.tsx b/client/components/fraud-risk-tools-banner/index.tsx index 7be072358f1..92b7c0429c9 100644 --- a/client/components/fraud-risk-tools-banner/index.tsx +++ b/client/components/fraud-risk-tools-banner/index.tsx @@ -11,7 +11,7 @@ import { useDispatch } from '@wordpress/data'; */ import { BannerBody, NewPill, BannerActions } from './components'; import './style.scss'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; interface BannerSettings { dontShowAgain: boolean; @@ -35,7 +35,7 @@ const FRTDiscoverabilityBanner: React.FC = () => { }; useEffect( () => { - recordEvent( events.FRAUD_PROTECTION_BANNER_RENDERED ); + recordEvent( 'wcpay_fraud_protection_banner_rendered' ); const stringifiedSettings = JSON.stringify( settings ); diff --git a/client/components/woopay/save-user/agreement.js b/client/components/woopay/save-user/agreement.js index 24098f94631..21a601b7a8b 100644 --- a/client/components/woopay/save-user/agreement.js +++ b/client/components/woopay/save-user/agreement.js @@ -4,7 +4,7 @@ */ import { __ } from '@wordpress/i18n'; import interpolateComponents from '@automattic/interpolate-components'; -import { recordUserEvent, events } from 'tracks'; +import { recordUserEvent } from 'tracks'; const Agreement = () => { return ( @@ -22,7 +22,7 @@ const Agreement = () => { rel="noopener noreferrer" onClick={ () => { recordUserEvent( - events.WOOPAY_SAVE_MY_INFO_TOS_CLICK + 'checkout_save_my_info_tos_click' ); } } > @@ -36,7 +36,7 @@ const Agreement = () => { rel="noopener noreferrer" onClick={ () => { recordUserEvent( - events.WOOPAY_SAVE_MY_INFO_PRIVACY_CLICK + 'checkout_save_my_info_privacy_policy_click' ); } } > diff --git a/client/components/woopay/save-user/checkout-page-save-user.js b/client/components/woopay/save-user/checkout-page-save-user.js index 5e5bd6cfac2..170715eb9ac 100644 --- a/client/components/woopay/save-user/checkout-page-save-user.js +++ b/client/components/woopay/save-user/checkout-page-save-user.js @@ -21,7 +21,7 @@ import Container from './container'; import useWooPayUser from '../hooks/use-woopay-user'; import useSelectedPaymentMethod from '../hooks/use-selected-payment-method'; import WooPayIcon from 'assets/images/woopay.svg?asset'; -import { recordUserEvent, events } from 'tracks'; +import { recordUserEvent } from 'tracks'; import './style.scss'; const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { @@ -93,7 +93,7 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { ); const handleCountryDropdownClick = useCallback( () => { - recordUserEvent( events.WOOPAY_SAVE_MY_INFO_COUNTRY_CLICK ); + recordUserEvent( 'checkout_woopay_save_my_info_country_click' ); }, [] ); const handleCheckboxClick = ( e ) => { @@ -108,7 +108,7 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { } setIsSaveDetailsChecked( isChecked ); - recordUserEvent( events.WOOPAY_SAVE_MY_INFO_CLICK, { + recordUserEvent( 'checkout_save_my_info_click', { status: isChecked ? 'checked' : 'unchecked', } ); }; @@ -116,7 +116,7 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { useEffect( () => { // Record Tracks event when the mobile number is entered. if ( isPhoneValid ) { - recordUserEvent( events.WOOPAY_SAVE_MY_INFO_MOBILE_ENTER ); + recordUserEvent( 'checkout_woopay_save_my_info_mobile_enter' ); } }, [ isPhoneValid ] ); @@ -124,7 +124,7 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { // Record Tracks event when user clicks on the info icon for the first time. if ( isInfoFlyoutVisible && ! hasShownInfoFlyout ) { setHasShownInfoFlyout( true ); - recordUserEvent( events.WOOPAY_SAVE_MY_INFO_TOOLTIP_CLICK ); + recordUserEvent( 'checkout_save_my_info_tooltip_click' ); } else if ( ! isInfoFlyoutVisible && ! hasShownInfoFlyout ) { setHasShownInfoFlyout( false ); } @@ -274,7 +274,7 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { rel="noopener noreferrer" onClick={ () => { recordUserEvent( - events.WOOPAY_SAVE_MY_INFO_TOOLTIP_LEARN_MORE_CLICK + 'checkout_save_my_info_tooltip_learn_more_click' ); } } > diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx index 4f76afeab0f..05b89bb8063 100644 --- a/client/connect-account-page/index.tsx +++ b/client/connect-account-page/index.tsx @@ -21,7 +21,7 @@ import scheduled from 'gridicons/dist/scheduled'; /** * Internal dependencies */ -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import Page from 'components/page'; import BannerNotice from 'components/banner-notice'; import PaymentMethods from './payment-methods'; @@ -50,7 +50,7 @@ const ConnectAccountPage: React.FC = () => { const isCountrySupported = !! availableCountries[ country ]; useEffect( () => { - recordEvent( events.CONNECT_ACCOUNT_VIEW, { + recordEvent( 'page_view', { path: 'payments_connect_v2', ...( incentive && { incentive_id: incentive.id, @@ -93,7 +93,7 @@ const ConnectAccountPage: React.FC = () => { const handleSetup = async () => { setSubmitted( true ); - recordEvent( events.CONNECT_ACCOUNT_CLICKED, { + recordEvent( 'wcpay_connect_account_clicked', { wpcom_connection: wcpaySettings.isJetpackConnected ? 'Yes' : 'No', is_new_onboarding_flow: isNewFlowEnabled, ...( incentive && { diff --git a/client/connect-account-page/info-notice-modal.tsx b/client/connect-account-page/info-notice-modal.tsx index de7824b5e4b..c4859834270 100644 --- a/client/connect-account-page/info-notice-modal.tsx +++ b/client/connect-account-page/info-notice-modal.tsx @@ -10,7 +10,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import TipBox from 'components/tip-box'; import strings from './strings'; import './style.scss'; @@ -32,7 +32,7 @@ const InfoNoticeModal: React.FC = () => { { - recordEvent( events.CONNECT_ACCOUNT_KYC_MODAL_OPENED ); + recordEvent( 'wcpay_connect_account_kyc_modal_opened' ); setModalOpen( true ); } } > diff --git a/client/data/multi-currency/actions.js b/client/data/multi-currency/actions.js index f2247831393..7c28cf73781 100644 --- a/client/data/multi-currency/actions.js +++ b/client/data/multi-currency/actions.js @@ -6,7 +6,7 @@ import { apiFetch } from '@wordpress/data-controls'; import { dispatch, select } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; /** * Internal Dependencies @@ -82,7 +82,7 @@ export function* submitEnabledCurrenciesUpdate( currencies ) { __( 'Enabled currencies updated.', 'woocommerce-payments' ) ); - recordEvent( events.MULTI_CURRENCY_ENABLED_CURRENCIES_UPDATED, { + recordEvent( 'wcpay_multi_currency_enabled_currencies_updated', { added_currencies: addedCurrencies, removed_currencies: removedCurrencies, } ); diff --git a/client/deposits/list/index.tsx b/client/deposits/list/index.tsx index 815e5f16658..21c48d2f631 100644 --- a/client/deposits/list/index.tsx +++ b/client/deposits/list/index.tsx @@ -5,7 +5,7 @@ */ import { DepositsTableHeader } from 'wcpay/types/deposits'; import React, { useState } from 'react'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import { useMemo } from '@wordpress/element'; import { dateI18n } from '@wordpress/date'; import { __, _n, sprintf } from '@wordpress/i18n'; @@ -118,7 +118,7 @@ export const DepositsList = (): JSX.Element => { const clickable = ( children: React.ReactNode ): JSX.Element => ( recordEvent( events.DEPOSITS_ROW_CLICK ) } + onClick={ () => recordEvent( 'wcpay_deposits_row_click' ) } > { children } @@ -130,7 +130,7 @@ export const DepositsList = (): JSX.Element => { const dateDisplay = ( recordEvent( events.DEPOSITS_ROW_CLICK ) } + onClick={ () => recordEvent( 'wcpay_deposits_row_click' ) } > { dateI18n( 'M j, Y', @@ -283,7 +283,7 @@ export const DepositsList = (): JSX.Element => { ) ); - recordEvent( events.DEPOSITS_DOWNLOAD_CSV_CLICK, { + recordEvent( 'wcpay_deposits_download', { exported_deposits: exportedDeposits, total_deposits: exportedDeposits, download_type: 'endpoint', @@ -339,7 +339,7 @@ export const DepositsList = (): JSX.Element => { generateCSVDataFromTable( csvColumns, csvRows ) ); - recordEvent( events.DEPOSITS_DOWNLOAD_CSV_CLICK, { + recordEvent( 'wcpay_deposits_download', { exported_deposits: rows.length, total_deposits: depositsSummary.count, download_type: 'browser', diff --git a/client/disputes/evidence/index.js b/client/disputes/evidence/index.js index 6041fb87755..17501b3c3b0 100644 --- a/client/disputes/evidence/index.js +++ b/client/disputes/evidence/index.js @@ -33,7 +33,7 @@ import Page from 'components/page'; import ErrorBoundary from 'components/error-boundary'; import Loadable, { LoadableBlock } from 'components/loadable'; import useConfirmNavigation from 'utils/use-confirm-navigation'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import { getAdminUrl } from 'wcpay/utils'; const DISPUTE_EVIDENCE_MAX_LENGTH = 150000; @@ -548,7 +548,7 @@ export default ( { query } ) => { return; } - recordEvent( events.DISPUTE_FILE_UPLOAD_STARTED, { + recordEvent( 'wcpay_dispute_file_upload_started', { type: key, } ); @@ -580,11 +580,11 @@ export default ( { query } ) => { } ); updateEvidence( key, uploadedFile.id ); - recordEvent( events.DISPUTE_FILE_UPLOAD_SUCCESS, { + recordEvent( 'wcpay_dispute_file_upload_success', { type: key, } ); } catch ( err ) { - recordEvent( events.DISPUTE_FILE_UPLOAD_FAILED, { + recordEvent( 'wcpay_dispute_file_upload_failed', { message: err.message, } ); @@ -611,8 +611,8 @@ export default ( { query } ) => { recordEvent( submit - ? events.DISPUTE_SUBMIT_EVIDENCE_SUCCESS - : events.DISPUTE_SAVE_EVIDENCE_SUCCESS + ? 'wcpay_dispute_submit_evidence_success' + : 'wcpay_dispute_save_evidence_success' ); /* We rely on WC-Admin Transient notices to display success message. @@ -645,8 +645,8 @@ export default ( { query } ) => { const handleSaveError = ( err, submit ) => { recordEvent( submit - ? events.DISPUTE_SUBMIT_EVIDENCE_FAILED - : events.DISPUTE_SAVE_EVIDENCE_FAILED + ? 'wcpay_dispute_submit_evidence_failed' + : 'wcpay_dispute_save_evidence_failed' ); const message = submit @@ -674,8 +674,8 @@ export default ( { query } ) => { try { recordEvent( submit - ? events.DISPUTE_SUBMIT_EVIDENCE_CLICK - : events.DISPUTE_SAVE_EVIDENCE_CLICK + ? 'wcpay_dispute_submit_evidence_clicked' + : 'wcpay_dispute_save_evidence_clicked' ); const { metadata } = dispute; @@ -705,7 +705,7 @@ export default ( { query } ) => { const properties = { selection: newProductType, }; - recordEvent( events.DISPUTE_PRODUCT_SELECTED, properties ); + recordEvent( 'wcpay_dispute_product_selected', properties ); updateDispute( { metadata: { [ PRODUCT_TYPE_META_KEY ]: newProductType }, } ); diff --git a/client/disputes/index.tsx b/client/disputes/index.tsx index f0876f7906a..edeb602372c 100644 --- a/client/disputes/index.tsx +++ b/client/disputes/index.tsx @@ -4,7 +4,7 @@ * External dependencies */ import React, { useState } from 'react'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import { dateI18n } from '@wordpress/date'; import { _n, __, sprintf } from '@wordpress/i18n'; import moment from 'moment'; @@ -336,7 +336,7 @@ export const DisputesList = (): JSX.Element => { ) => { // Use client-side routing to avoid page refresh. e.preventDefault(); - recordEvent( events.DISPUTES_ROW_ACTION_CLICK ); + recordEvent( 'wcpay_disputes_row_action_click' ); const history = getHistory(); history.push( getDetailsURL( @@ -425,7 +425,7 @@ export const DisputesList = (): JSX.Element => { ) ); - recordEvent( events.DISPUTE_DOWNLOAD_CSV_CLICK, { + recordEvent( 'wcpay_disputes_download', { exported_disputes: exportedDisputes, total_disputes: exportedDisputes, download_type: 'endpoint', @@ -504,7 +504,7 @@ export const DisputesList = (): JSX.Element => { generateCSVDataFromTable( csvColumns, csvRows ) ); - recordEvent( events.DISPUTE_DOWNLOAD_CSV_CLICK, { + recordEvent( 'wcpay_disputes_download', { exported_disputes: csvRows.length, total_disputes: disputesSummary.count, download_type: 'browser', diff --git a/client/onboarding/tracking.ts b/client/onboarding/tracking.ts index f2840d5d930..cdcbe274650 100644 --- a/client/onboarding/tracking.ts +++ b/client/onboarding/tracking.ts @@ -10,7 +10,7 @@ import { useEffect } from 'react'; import { useStepperContext } from 'components/stepper'; import { useOnboardingContext } from './context'; import { OnboardingFields } from './types'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; const trackedSteps: Set< string > = new Set(); let startTime: number; @@ -26,11 +26,11 @@ const stepElapsed = () => { export const trackStarted = (): void => { startTime = stepStartTime = Date.now(); - recordEvent( events.ONBOARDING_FLOW_STARTED ); + recordEvent( 'wcpay_onboarding_flow_started' ); }; export const trackModeSelected = ( mode: string ): void => { - recordEvent( events.ONBOARDING_FLOW_MODE_SELECTED, { + recordEvent( 'wcpay_onboarding_flow_mode_selected', { mode, elapsed: stepElapsed(), } ); @@ -40,7 +40,7 @@ export const trackStepCompleted = ( step: string ): void => { // We only track a completed step once. if ( trackedSteps.has( step ) ) return; - recordEvent( events.ONBOARDING_FLOW_STEP_COMPLETED, { + recordEvent( 'wcpay_onboarding_flow_step_completed', { step, elapsed: stepElapsed(), } ); @@ -48,19 +48,19 @@ export const trackStepCompleted = ( step: string ): void => { }; export const trackRedirected = ( isEligible: boolean ): void => { - recordEvent( events.ONBOARDING_FLOW_REDIRECTED, { + recordEvent( 'wcpay_onboarding_flow_redirected', { is_po_eligible: isEligible, elapsed: elapsed( startTime ), } ); }; export const trackAccountReset = (): void => - recordEvent( events.ONBOARDING_FLOW_RESET ); + recordEvent( 'wcpay_onboarding_flow_reset' ); export const trackEligibilityModalClosed = ( action: 'dismiss' | 'setup_deposits' | 'enable_payments_only' ): void => - recordEvent( events.ONBOARDING_FLOW_ELIGIBILITY_MODAL_CLOSED, { + recordEvent( 'wcpay_onboarding_flow_eligibility_modal_closed', { action, } ); @@ -74,8 +74,8 @@ export const useTrackAbandoned = (): { const trackEvent = ( method = 'hide' ) => { const event = method === 'hide' - ? events.ONBOARDING_FLOW_HIDDEN - : events.ONBOARDING_FLOW_EXITED; + ? 'wcpay_onboarding_flow_hidden' + : 'wcpay_onboarding_flow_exited'; const errored = Object.keys( errors ).filter( ( field ) => touched[ field as keyof OnboardingFields ] ); diff --git a/client/overview/inbox-notifications/index.js b/client/overview/inbox-notifications/index.js index 103fa2c7a7c..bbba6eefa64 100644 --- a/client/overview/inbox-notifications/index.js +++ b/client/overview/inbox-notifications/index.js @@ -16,7 +16,7 @@ import { /** * Internal dependencies */ -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import { updateWoocommerceUserMeta } from 'utils/update-woocommerce-user-meta'; import './index.scss'; @@ -63,7 +63,7 @@ function hasValidNotes( notes ) { } const onBodyLinkClick = ( note, innerLink ) => { - recordEvent( events.INBOX_ACTION_CLICK, { + recordEvent( 'wcpay_inbox_action_click', { note_name: note.name, note_title: note.title, note_content_inner_link: innerLink, @@ -87,7 +87,7 @@ const renderNotes = ( { } const onNoteVisible = ( note ) => { - recordEvent( events.INBOX_NOTE_VIEW, { + recordEvent( 'wcpay_inbox_note_view', { note_content: note.content, note_name: note.name, note_title: note.title, @@ -207,7 +207,7 @@ const InboxPanel = () => { const closeDismissModal = async ( confirmed = false ) => { const noteNameDismissAll = dismiss.type === 'all'; - recordEvent( events.INBOX_ACTION_DISMISSED, { + recordEvent( 'wcpay_inbox_action_dismissed', { note_name: dismiss.note.name, note_title: dismiss.note.title, note_name_dismiss_all: noteNameDismissAll, diff --git a/client/overview/task-list/tasks/dispute-task.tsx b/client/overview/task-list/tasks/dispute-task.tsx index 029204cc044..fdf48c03d78 100644 --- a/client/overview/task-list/tasks/dispute-task.tsx +++ b/client/overview/task-list/tasks/dispute-task.tsx @@ -13,7 +13,7 @@ import type { TaskItemProps } from '../types'; import type { CachedDispute } from 'wcpay/types/disputes'; import { formatCurrency } from 'wcpay/utils/currency'; import { getAdminUrl } from 'wcpay/utils'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import { isDueWithin } from 'wcpay/disputes/utils'; /** @@ -50,7 +50,7 @@ export const getDisputeResolutionTask = ( } const handleClick = () => { - recordEvent( events.OVERVIEW_TASK_CLICK, { + recordEvent( 'wcpay_overview_task_click', { task: 'dispute-resolution-task', active_dispute_count: activeDisputeCount, } ); diff --git a/client/payment-details/dispute-details/dispute-awaiting-response-details.tsx b/client/payment-details/dispute-details/dispute-awaiting-response-details.tsx index 3da128c85f1..1ebaf1954fd 100644 --- a/client/payment-details/dispute-details/dispute-awaiting-response-details.tsx +++ b/client/payment-details/dispute-details/dispute-awaiting-response-details.tsx @@ -25,7 +25,7 @@ import { */ import type { Dispute } from 'wcpay/types/disputes'; import type { ChargeBillingDetails } from 'wcpay/types/charges'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import { useDisputeAccept } from 'wcpay/data'; import { getDisputeFeeFormatted, isInquiry } from 'wcpay/disputes/utils'; import { getAdminUrl } from 'wcpay/utils'; @@ -93,7 +93,7 @@ function getAcceptDisputeProps( { if ( isInquiry( dispute ) ) { return { acceptButtonLabel: __( 'Issue refund', 'woocommerce-payments' ), - acceptButtonTracksEvent: events.DISPUTE_INQUIRY_REFUND_MODAL_VIEW, + acceptButtonTracksEvent: 'wcpay_dispute_inquiry_refund_modal_view', modalTitle: __( 'Issue a refund?', 'woocommerce-payments' ), modalLines: [ { @@ -115,13 +115,13 @@ function getAcceptDisputeProps( { 'View order to issue refund', 'woocommerce-payments' ), - modalButtonTracksEvent: events.DISPUTE_INQUIRY_REFUND_CLICK, + modalButtonTracksEvent: 'wcpay_dispute_inquiry_refund_click', }; } return { acceptButtonLabel: __( 'Accept dispute', 'woocommerce-payments' ), - acceptButtonTracksEvent: events.DISPUTE_ACCEPT_MODAL_VIEW, + acceptButtonTracksEvent: 'wcpay_dispute_accept_modal_view', modalTitle: __( 'Accept the dispute?', 'woocommerce-payments' ), modalLines: [ { @@ -151,7 +151,7 @@ function getAcceptDisputeProps( { modalButtonLabel: isDisputeAcceptRequestPending ? __( 'Accepting…', 'woocommerce-payments' ) : __( 'Accept dispute', 'woocommerce-payments' ), - modalButtonTracksEvent: events.DISPUTE_ACCEPT_CLICK, + modalButtonTracksEvent: 'wcpay_dispute_accept_click', }; } @@ -269,7 +269,7 @@ const DisputeAwaitingResponseDetails: React.FC< Props > = ( { disabled={ isDisputeAcceptRequestPending } onClick={ () => { recordEvent( - events.DISPUTE_CHALLENGE_CLICKED, + 'wcpay_dispute_challenge_clicked', { dispute_status: dispute.status, on_page: 'transaction_details', diff --git a/client/payment-details/dispute-details/dispute-resolution-footer.tsx b/client/payment-details/dispute-details/dispute-resolution-footer.tsx index 9f5dd9f44b8..545eee5d598 100644 --- a/client/payment-details/dispute-details/dispute-resolution-footer.tsx +++ b/client/payment-details/dispute-details/dispute-resolution-footer.tsx @@ -13,7 +13,7 @@ import { Button, CardFooter, Flex, FlexItem } from '@wordpress/components'; * Internal dependencies */ import type { Dispute } from 'wcpay/types/disputes'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import { getAdminUrl } from 'wcpay/utils'; import { getDisputeFeeFormatted } from 'wcpay/disputes/utils'; import './style.scss'; @@ -69,7 +69,7 @@ const DisputeUnderReviewFooter: React.FC< { variant="secondary" onClick={ () => { recordEvent( - events.PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICKED, + 'wcpay_view_submitted_evidence_clicked', { dispute_status: dispute.status, on_page: 'transaction_details', @@ -140,7 +140,7 @@ const DisputeWonFooter: React.FC< { variant="secondary" onClick={ () => { recordEvent( - events.PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICKED, + 'wcpay_view_submitted_evidence_clicked', { dispute_status: dispute.status, on_page: 'transaction_details', @@ -249,7 +249,7 @@ const DisputeLostFooter: React.FC< { variant="secondary" onClick={ () => { recordEvent( - events.PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICKED, + 'wcpay_view_submitted_evidence_clicked', { dispute_status: dispute.status, on_page: 'transaction_details', @@ -321,7 +321,7 @@ const InquiryUnderReviewFooter: React.FC< { variant="secondary" onClick={ () => { recordEvent( - events.PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICKED, + 'wcpay_view_submitted_evidence_clicked', { dispute_status: dispute.status, on_page: 'transaction_details', @@ -395,7 +395,7 @@ const InquiryClosedFooter: React.FC< { variant="secondary" onClick={ () => { recordEvent( - events.PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICKED, + 'wcpay_view_submitted_evidence_clicked', { dispute_status: dispute.status, on_page: 'transaction_details', diff --git a/client/payment-details/index.tsx b/client/payment-details/index.tsx index aa80dd5a70d..0146da0ffb9 100644 --- a/client/payment-details/index.tsx +++ b/client/payment-details/index.tsx @@ -11,7 +11,7 @@ import PaymentCardReaderChargeDetails from './readers'; import { PaymentDetailsProps } from './types'; import PaymentOrderDetails from './order-details'; import PaymentChargeDetails from './charge-details'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; const PaymentDetails: React.FC< PaymentDetailsProps > = ( { query } ) => { const { @@ -23,7 +23,7 @@ const PaymentDetails: React.FC< PaymentDetailsProps > = ( { query } ) => { const { status_is: statusIs, type_is: typeIs } = getQuery(); if ( statusIs && typeIs ) { - recordEvent( events.FRAUD_PROTECTION_ORDER_DETAILS_LINK_CLICKED, { + recordEvent( 'wcpay_fraud_protection_order_details_link_clicked', { status: statusIs, type: typeIs, } ); diff --git a/client/payment-details/summary/index.tsx b/client/payment-details/summary/index.tsx index 7e4061424a3..4b3792b909e 100644 --- a/client/payment-details/summary/index.tsx +++ b/client/payment-details/summary/index.tsx @@ -50,7 +50,7 @@ import { useAuthorization } from 'wcpay/data'; import CaptureAuthorizationButton from 'wcpay/components/capture-authorization-button'; import './style.scss'; import { Charge } from 'wcpay/types/charges'; -import { recordEvent, events } from 'tracks'; +import { recordEvent } from 'tracks'; import WCPaySettingsContext from '../../settings/wcpay-settings-context'; import { FraudOutcome } from '../../types/fraud-outcome'; import CancelAuthorizationButton from '../../components/cancel-authorization-button'; @@ -431,14 +431,14 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( { } onClick={ () => { recordEvent( - events.FRAUD_PROTECTION_TRANSACTION_REVIEWED_MERCHANT_BLOCKED, + 'wcpay_fraud_protection_transaction_reviewed_merchant_blocked', { payment_intent_id: charge.payment_intent, } ); recordEvent( - events.TRANSACTIONS_DETAILS_CANCEL_CHARGE_BUTTON_CLICK, + 'payments_transactions_details_cancel_charge_button_click', { payment_intent_id: charge.payment_intent, @@ -458,14 +458,14 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( { buttonIsSmall={ false } onClick={ () => { recordEvent( - events.FRAUD_PROTECTION_TRANSACTION_REVIEWED_MERCHANT_APPROVED, + 'wcpay_fraud_protection_transaction_reviewed_merchant_approved', { payment_intent_id: charge.payment_intent, } ); recordEvent( - events.TRANSACTIONS_DETAILS_CAPTURE_CHARGE_BUTTON_CLICK, + 'payments_transactions_details_capture_charge_button_click', { payment_intent_id: charge.payment_intent, @@ -537,7 +537,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( { true ); recordEvent( - events.TRANSACTIONS_DETAILS_REFUND_MODAL_OPEN, + 'payments_transactions_details_refund_modal_open', { payment_intent_id: charge.payment_intent, @@ -555,7 +555,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( {