From 3627d1a0fa270b865fa4c9226090e0616b2b82a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Tue, 16 Apr 2024 08:58:02 +0200 Subject: [PATCH 01/24] oct-1396: onboarding progress draft --- client/public/images/slide.png | Bin 0 -> 58540 bytes client/src/App.tsx | 2 + .../ModalOnboarding/ModalOnboarding.tsx | 38 ++++++++- .../OnboardingStepper.module.scss | 77 ++++++++++++++++++ .../OnboardingStepper/OnboardingStepper.tsx | 69 ++++++++++++++++ .../shared/OnboardingStepper/index.tsx | 2 + client/src/constants/localStorageKeys.ts | 5 ++ client/src/store/onboarding/store.ts | 29 ++++++- client/src/store/onboarding/types.ts | 6 ++ client/src/svg/onboardingStepper.ts | 25 ++++++ 10 files changed, 245 insertions(+), 8 deletions(-) create mode 100644 client/public/images/slide.png create mode 100644 client/src/components/shared/OnboardingStepper/OnboardingStepper.module.scss create mode 100644 client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx create mode 100644 client/src/components/shared/OnboardingStepper/index.tsx create mode 100644 client/src/svg/onboardingStepper.ts diff --git a/client/public/images/slide.png b/client/public/images/slide.png new file mode 100644 index 0000000000000000000000000000000000000000..c2cfa6e81b044698453749095d9ac76517858d06 GIT binary patch literal 58540 zcmZ5{cR1Vc_r6V4QM7hKZPnVfcWczvK}(I=t(vt-s8uy$)u>%LqSmsxkz-P zrJz{WF}kgDKZJ7Y#I~|LhD#=(DwV{Pyut?ruhaE&SML>NYLeQHU?j86?8sBii(T$;@Op{*PcU?f&3Vx} zQee++uc^`5w{l6<4>YX_-9*a>u&8_V_SO#u?C}Oq4a>GAcgQedd_Fp?4tkbLPZd^O zX{#6?O7JUZnS(dJco6B0jDpTeN8>~?)nagP&S7Tk(W4?3_aK3p#lc$R278`tu8UuB zb~m}Tn;lx(HUhIXSk$=H{fZl(e{gVOm5u*zxcEidUibGTnBF*wRC+>0ABE11S>*DD z1S`9z_7aFUQ|JFc7g9XsTW?G^&DjfqjQ(P#_KNW!jP#=A^bKeL!TD$#RIPv}4Ds-S zj(2q&j^VqFzcZAywt45*60zoO!lCp@k(`9T1lypU24)f4mxR5iqEUljOJy-C8OVlU{~v#D>qWgTzqf+nE73n^C)b zLz?j;&$%69<}{_}FyqcPbn1;@-wRwK3>(uWHll=Y;EBChi5tBgLHk^*02VDbL^U>V z0rjYu^fuxY8SZx^&jvA81E0EAD`8LP*P(TC`!6ZXoILpO%D~8V|t$ND76TRRa46j z6^6$n1bFEareiP%dh<^VQ1AG+kO5bVA$iX)$-_*sp)h%e)4j+}hwq-j4i>}hf15#f zJtt$Pn9(sijqd8Kdrf*%f5SDiL8r*-pSXx2rXC4ep^w%YMFWU&wHlqJFW~^8T_^UW&l4g5N4$c>)ujaLcW_-YOS>*`Qa$6 z=-FhjEB(Mv+`%(4$fab|b`=Y-X~#hw4G$81d^&h!x_H74K3P8HG><5LS^XRTbavf1 zqZsB^!-D;G(1j)h(h*iq)vC?W7_HF%x*`vDK4RmvLL(M6n_+sr*K3ie;%X&KtLIS? zHa7?xnjdi*98&xdr+$MQr}-OI44V#rF@B)vWgi}S`V$3?gMnwdQDAOjD+H^#{5NW9 z7zHi|GOA?;U~^8*2-_RiLRB3VeO_}g#c{{iIKXQ1BDhD7SJl7!s4Jm+e=BPdxVnya z@cRcBKPp8jq59nX*L0Zu=DfYVb|N%k(vRsu&iH+o zckY!ETIivCl^vy*Rkr1eU7f!X&a0@(VCYVc2kM=KU3oZ@miZa|&EXe%QC>#~B>g3+ z#QsCN?`{ZmT-&Z%+sr#@ystTw#R0?BMV|;XXt94Yiw|9gKi|-3#etF_Vl6odBPCOj z%(&CeoC(%emsP~31Ry^M=Z_}V6*T{JTE%<(b;3hPI0)zGNa;()bUldYW8vy^FmMr& z{Kt9{t#pC_%}3_FyEz27hV%&$OZS~WIUW(NI)>3|^l7bbFpn@Jm$ywK5nBZ;CIO%@ z@Fc@)>{O`cLp|K?_IIBEJ4U6v^7wW*^&9Eqv|z4Zk&!!0t9$CW_l&(gk$BAwPFpmO z9~eaaD_|tTb7+pbSncqreDgzNzz8w0xObN;Y_bZocu_R4)mPNvrswe;o7650Fa2O+ zhd5xe*{q24stVoZJFE~qyt^VZq`_H;2}GZ8w&7^ev&46~&7g>ay<<%+%x37(;lgAk zwrCX3kk8YD zaFrtwW>)9|PtIae9!lb{YOa|j^3?hWkA`Mz2r0!)e#|JhPb>{E3DyDumO!7sm8v*; zYgQ%AcKTzx*os+gm88^qe2VJ3o%5BRqwe@*pQKzpGT*}?KzYfj90*n-*9ooBiP8(A zTwe3!#BA%f%pPI6J>LetT-`-2j!v2kn8u@uu-`E`E|^pM z{jWDQVG?#?hfKwVJJ&T)@@>93+txr>9k!oLOWyP7Mf{^<#3CxRKimT+g}zSyxFkI6 zo=^L-Ms5tfNakczKfm*};1;^j9_>E<7(o<_E3AY%f&VC}m&vL1CZP#;8>bFrFWy3A znxim*!@Ph0N-yo5x@|HYCFS9pMqvF8RRLH7NP6`o9OkX*VhsAz-COe-624kMv{^!$ z@6~=(IN3ka>7~Ci51YiuYNBJVkm`_u2X-bqg<$Td;Nv>`Bc8v3i-RT2ka-eZyJpKc z1^Bc0`A(9sJw)p}6!oqx8L3^rrTD&Ag&g|Bz%?a(DY&HPBh{#KM%b{6)3Shvj zNb~(x|2wkTw1BmZ*v1DED~rKkpWSd}O%~5p3|To&MMg&Wz9SHW0bQ?vSh#HAWGMhW z`5Q&DJ|>zMeH(F?-?;~7>W#!G6pVizSmWkt<9Om+>TJpMa?xq^_F)Vw1eHX)s(E|Z zAtdvN?g644#r@i+s4n?M#EzrXL?n)@2+h zf8(Ib!+{*j;?U{nX!=3~J{)El(vs9TSqMbcp{{|hhN1HdHAzI(Jq=7?804G#haL&= zUt-ZJ0zy3FJjG&?2pZDn9%Z|Qfy6>e|~s9i-@k(TlUyo>0)Pj`1CZ#H80u?b*Y z0>Q1Qyis~9Pdb1L(am_>CFu;Z@QlR-ffM(kWPO2r zO*lRtfnM4p660EzHO5ca-o%(j6otbK(3)F-{K@MPl)>}#YH*B8c*Lb8=!egxGAh~V zY7A}D>o%_7re56RgCkT&wNeUUcT)kA6l@I+sGhDvRid6OhOfV_m$C4tx>F{NQq(rr;Drt8wac07QeHu-f_|=&?I$iW4mq z#1I5gSOD+QkrN)-|q7j;S1V6JE!AqA~LX_$G#Q9ehgPYSx=uba?ZF zLcaPm)6tF-!>lMEA#X1fo0#0v8l2%fBon}7K3J?}c@SG$+HQV-vT2Jra})SQ#NpYT z4gxmEdplSe<}x1wABx;g!m`hg7D{hLes=acsOo}Ufdy+S5W=u`dbc(;f}6Qe#qZ^LgYYoD1PoPLSr$p&6G}hOriy7V4NaQTPU=W3f z*g?_UqNB;oSrNxYF#0kHRiO)8P=b9~0`l_O%wS@#TJw>Y8p{-6Evc%t@3hCsISO?T zoVFbO{qY`A^dR84DR1*Hva2=;;TM2HbIJLgz)vGmy1lD%R2?k#WJmKs`|6)Ya#(Sk zq$P(f^AnwK`2FojhI^>4tDwsVnp49nT^i=C-@{`_D^Y6ONIfdrPHAPMeDlbbCtQHTy%xr0u{=!TLn ztL{l1u-*2Y_uy0LxnpmCD5LfXCMu_Y-8oOT<8PAes6X#%puE*q9&Tv zVYiX97=l!iT}3wb;+rBlskd7j2~&%$#1;YyQ0{{Y-w#J&f_K^oQ|oqWr;DmY^^YRJ z@2iv&kUd#S60&?v+@4#D|SJ-aP89O5&r zeR*CFilo@#CUjHA7Zb>N0N4I)3uJnG2Q7<5P^)Pq1fbI!vA5AY%BkpXEfSH3fa!<0 z?zDzOoN}-~a>KVnz~DCg^8~^+^kx9eIRd@2ia}3~p}@h#B<|{8t^5e3Ip0D^3iNKC zy{6vGdo#@USMRUqXgDjFlrb5%(C}Sd*fw2SgvYJN=f9!FN-jkADmS)KcC}Bc;}?f} zV#=!_K3ga1hXLQ5YLuDy zf{Y^2F|itjfuW6V@e!480iM2EE4X~Q<-%J>4~~74qAnK|^fe{HU()Zk1}1vcr@Aj2 ztA4SJ5{uSj8S4hpSJkwJcHoj?NS(*4%JWyt>rc{0R5lLnLl)x zN+^c-ad;zk&)FUMD+dLz=KIAKX|dZ8f?vrQtX<48z}#6u<%XKRleQ}Ep^V<~ad3~y zEr9&o`w`myW;P7V>kxyBtNR@ZgK8mxTD?%D8vt|aaepCn$w55;7DKu*U9L1b@tmg2 z6}%CV*n=v7Kwx>KJ&pY%Uo;_GqsrPw@^+SM8K zzw)|N38-;F-Sh>m9!9ufv7L^Jw9jI`wWV!NjS%7&Dyd2WzVS_2oeGKM<4pyB2nGki zg#xge-%c;9uBN-9Hq=|^>`>_ljqb>$+EbnO9Ni6Y`Ut3I37I$>vS4$oFR0Z6V00p# z(Mb8Bk=%(FXm&besTFKqK7IO{u&Ay)4_{DJ>f3rsF$1iY$8?|e!8sk#>QHHCuvxY` zcy2NS*hQ8t`;QOoNv^%HO9#Xj+zEXdGQDx~hB^ymOTm#G0nCEh&lfw2rsRkOR}~h` z^A}+afd`!mFci_D(Fru0&7p5~ zvwg=bIqX-c^FoedFA(Xxrcs5!^iS?+>@-wiTnT-3lR}y(&*N^@VDn+^K5C6mpte)u zF{d1f(WI~=hW_?^PWj#p6h(Flkp!|&5iZgS6D7bjQ1X+7Ee2ueE}Z&H1ZoZoCh~P3 z7Cu+NwpY7y)rgXjF~pC2A9{*H_&^D(ff5m^IW0GjiQsq`81-$Oi>+{lKPA3M*JWhx z)6;;J^v?!j!KK3w`d(afHc8ICKIp+jQ~z@6!BQ0mvzio!=fevFSCgx9v~MRorO>%9>kb2u`uhJ~wSKXF&;(e@#^&Jyuk+=9(`nQj*0kzSbX6gmnm zA3UOPGOpT=I+7TfRD(D?-{ILi-a~{zl=uQ1m0)-5=6R=(L>VS}H^e0UQba@fiBr3> zOmK08`QEXgtvbC{4^Eu`g+U(%Q2U`(Qn4J^JOUA|p#8EcvILt;)I^?0g_CPZ+m?z$ z5Yd}X4YZA7$}OzNV^rK@*D!4}Fz|tWH(mrU_)YNjo^*XN#5wrx30uF>HKhCg%~8q~ zs*Y=U7~z+9%$H)0Y$C+|B3dlNfy&^R-8yL0VNxyjjuBzv#bVN~HoSM93j|DH0fMsH zw2_g&Fc=M7kd`PSpaHgO+7B7iq9E#&;QKX(;FHUMu67I3QbH9bT4mI>x0;tO%@@4a zfLIt+FA8CPTm>s+xlg>q1}X2@(so~XLyd3&f^QJ*{8*VB`o2N(+IDe!2lmyz;BPP^ zbru$QKy?S4@b=Qx;^J_#-^85t@yP~kF3#`QHZcOG?hZw5q=&E+VtP^KH!+*AViFGU z=m?M92AQCKcodNv%kXM3@M#w+V4o`(gJ(J1;_MAnn=d0OuMt7! zkfWmFIdv9jZ^ITSI2=t3O~!6gdqwaQ0YPEIwJcgyAyAIwq!Gf=7_NH+I};9AdfvPh zy2h=rhrsW_wn!c*`*+U4VH%zF$K12qgjBTWYUl(CjDl&Ax=|85%z=fxOoBs}>;UIK z_Y7*jhG2p{wBHuE0;m!7THEco1-aoI>e`V{hq3kl1rud>=+Y4J@35A2R?(dfE}uy0g0ZYm(fbZcN=l@#f>Ri2yY1xXATaV^ zjD(&i5x^vec`zx`i#MV34rt}Ab%aB5Ue^f$i}4U>A5N>XYqaVa_XCA&|CnU^t93Nx zy#Cu)Bt z*Qi_xYj-NH#ywKI6f(LikZk^og!~C6{H#u4*_{{RzM#t>NJDo*y6lWxyMlv|7PW{8 zVzu4}QtiB2IEAqiU@}dXakfm?_UzBW1}~=s4_3#F6Z+FF9J-WdHXp^FaCPkiAlMNd z6VCiD07I1kY+t*BD{sT7wP84sDyN=o{VoAWQ7qf5b`Hl%Hx}xTY*VX(W1aQA(d^t|bb2K_n}W3R;_x~@pBPwkbXv%) z1L@EK{DMg_!dCwkt1d&Wyv$Y5DW({V1d@(&q4Szkkt!z=Ig>*{Dmth+zP|1|I)K34rn0eKyuPni-q#*7|ZSEtW@wFtS zsHquGHJ?&=3F=zAVU!qU%WmgI#n8R|l|ggO!-;z&@Oj98xjXZzk=A`q{`#EI`d=L~ z?CY~u_FMY3KugV_<(pcPu3=poJcIbrpq`P*<~}GKQ7oF4ZgnpxeWph5Nqn^L=HLJJ zqfQrrr;DV}h4yH0&e@*#jQ!9EC}}d&ToVt*u8ftXIY~v32=fMsE}EhH%e%#=zri!H zSb-nT`@$~iL9(YTQ2)M?x2%)qBV)Ela2gl4unWR+%9-S)1VLB!=Se5?B*bD@0%;(} zIh9JgsB^A4___bmvT#;|$DpgMC4%aql`9D!hZ>EVg3SL&b z-1Z=y7gRqo7p(4P##<;pA5Wo$-D~he>OW!;qEuld?P;C%N|U~Uk6_+siL6`s${dXA z0-hq;h&iPFwqwRvfv~qM!cRv315FwE_Uo~jnz}aRm6i?I{KO}Vng4o5Z=*Jk9|=C~ zrKV>lww-_2hdQmnXha*{nzZ3?0#2hg3}tl9N3%mp*V_?s6B0=;ksW$0t=5^4w0g)+ zkCOPex-7l0hG?t~sMF(5OTjr_BxU}#dYuRi-}wYkM6h{NFG~qax*2ltJC>DJC0$|G8Lu6>*p*k8d6kOWd#>rw$O=e*MtE-{j&Vo`seX8}=9RTJpNhY5v0CuW(*;zY&K1YH z2g5cFUjbeyp8SQo^o&Zr>RlR)hp)yA+qIO~yb6<^)B4Fg+%g&1_hz-d5;Tmk*Q1AR(;aZP?No!+Hi?TY2h+)(?kDpi@ZYhagi@4h* zSrNP2Wh@O{zK|keIW3$VJNB(+eKCH_f=it#y}pYj-Km3f*_WD|zVWw0XTILs2YsgP zOl)F(`4)etsn2rz7tijh1w~Puhj7Wh6JIeWtK)m%@$51E!<*b?3=i_g9{PUm7qslZ zJ@i`P`TKsa0~&pp`R+Tj5cZ6-r~guclJouAzlu4d$I1-8hP+DPBcJ6kosP-~idVv& zn8YvCXnCK}v{bb;!Rn39ALc-fF8~C&qKe2fo&iA&j30CWy<}2!nY7Zj5|RYov~N)^ zP!0UTgpgsY9M+u{dxTDylJ)m)4CAmd@8G?Ydq=kR7X0gi(ty%1q&@$^SvZ~^YWuE} zIThz*pB&3BX7-`@^q@S+Pa`?54pM$$O0crEZJxfzK=HtwWf~G73ZpGDQx;$RI3xoYf1KhPXhe#N?TD zLi+VCb(w6tX0k@_QhSOwJa^tBVqjtq&d=t$nAYAec6taw-~Kw8asBN~}5$(?zZeMrTL+oOUrXmft3oLhJ7y zlK%t%H`Jc$n7NBtR8(c{0n5}1479}Y`P+Sp?Oj!t&Qy?&$uVRuyDkh_jIUEi5T6#b z6`u3)UF8D{D2<`V?J3GoM#P={=J}JI)%D-y2^TqUx|rbUwH|2V{2zS}xO%A`14gF+c1@mii0P~pkv1Lo5fR= zAWkH>^s?Pj%*^;ekA+7nErV7=^g{c$1rgr6ZXD!5JT&`b*-ph2Hzqz6LD>Eo-v6eH z+)OG>=SMLzNalOp&i3m5OT~Mk|5)v1a%`C6yD+W+RoJ}+BF#TwJul-8TUb9mA-h$X zV#YL%3QTf8-rjVEewNp-)#B$2mgi|2lTte! zo4ui&3a9D2>d5r-lf?x@Y(+?vzfqZymsgWP(1(2996g&!yXZ{rMax{zfX_SWXJ@yO z2#AyO4hOvwlMnNBxqmOnwTELsSWJSUe$?25t26P9PShv1DQ?b#cnjhB`yYg~@XMLq zYa@LRs|59Q&WKLKTioPV=90QgfkXJVq7T!9arz30`|#p>tov3_>Ky^2uVVJJjCVN3 zM6&do(xbp~9Ow0}D_+-A-ZZ5Y-fR)b@ibKz9kBxBzL5K9YCu11I{16$B`7=PeYJMO zg)ozaPL|h&;s)pM#==!TX1l(U7Hg;;YmVBhgGVv@d?H}N;HmNYSXqdP|8M8lF3`#aqjeiA%H8N(}(DO1ew=lMv z%djzSV9(i~{%EG`GDnY7Sv)|!oE7|+<4d}KygTK~$I2Xc?}=|1J-Na7O|*WQ*LJgH zKncm>%30L++Mea}HsH}+;eR5-3@3~DV^O2tnK9Z&N0qWSeeY6>gs`>t%?Q!JciMrv zO#Ltlp^gBK8o)|A!j0!g!Oi+CtH#{Im@^8UK*`^w6-C*N(L?x=u?1-$e>H zoN1uSf^oNZS!tGdi^ryA4HWnbthTD)(};d^3JW*kJNgQ5cx2FKZc=Y;aFo6VZ` znxzlRVZ(L<1U`>taSy7li=Xp}AW$u6t5%v^cJdvL$8+}8&@J~SxXlYySJo+9fkeGE zC%$}4zE3On9kw}}TrP%@meev4nYwnX-mjw}9J$>LFbj%o!RgsKi+L;4~8#@xHQi#KjSRv77VVmMdG4 zn9wP*H7yr;BBHT1x+iBMToCDs5Gc zl7QR!{h$TsW@BZTMF9J&r_L{~lF?t#;sJ>95SYmP^xuBp)U$UQ7cyxK|F^q)UjUtu z{g0*e_WF4)l_0~c`~@{@ep<;S_*IjsI?1JOI&T@B8taZgjtRHNXSvR9U2T8EK0;XL zxFHEI-awO9+>X4Z^qL2S#A!8HzmEsNZ5H%dKMgF?>1k-%^ChOPfwyN%m|a)W|F>Nl zI`8D47B1iU;s!RW+lo{Ue90}$qV4?g!{05JdtMc~?;5^JA*qC>zDLVs_SxJjzqLO0 zO(dblA@7?0QYTA)7Wae0^Jo=(DT|_AmPj7-l5s%`B$~l&DEB@#2c!KHJj^u4O})fF_E510JX&Q>nYTZrpceda!IxQYNMiX{&xW>*#YgILp^05R z_sA!^pDb>xFuykjT)%u?+@dA{mr7;YY@`NQ4t*k!ym&H0QwtYd&fwxE6I}{bgWIbI z%5`#OWtj7 ztXvmGTkbP4yfrwJWSg`|#bg?$2|!SL*s{2z9iaRV0ZcgBUhEeuHmQgZI1w^8^su@F zH(ao1i5Q!q9(Z_oWHf=IBwUH<%HT3yq0WZJeE6S`lITz7N)<$^#|u6S5Rc_+NfMR> zjsh<)W5Q!qIN6pRcdFRy!*R>1T$xG z8QW`(j_Du$Z;zPnKTUm<93&?!FZxX0R9{zRC?)p1M+%fx`pb-X`mT$q0^}!G*&-F@OUewXg?XM8sOKO^0y9qoSHS zQBYJ2N`$NSwtoZYB+R<~OaQQcjn<$rbi4g%nam&dlREV(d{kp1q-ZS*v!{o06Fcdy zFBmi|Qgx}{8a`B~(={y>#TNeykO{ozjqsz3v>xIg0#GAw_c+5CcL5>Y)VeJ8hZd)_ z<=T8G-VTvq#hM3;gUUJ{jNPv3X7wPJA1twdLeA3WY4^gUS|9#Q8eYf|1}TgM`7Cb zor&yXXNBo^mf`!P57@A%TC&A+gYVuI9wr&z=X1d8%{Z)lgf{S7&zU1!fiLQwv#;jj9!P&{*&QJjex{RoH zqIWprUY6uy7ZS8w>3UyQS`+wboAI^DplKp)iEz*2XsOJ=vwH!v* za`(I%OCH1tn&0>|7$nbA7no$CWA{0AF{0>BwZb_3(vQ{*mR(%D`P?(vj*oQzOM@k6 z}GYBzR!NdljGh)ljoz=mS{m+KY#rMBy7 zGfaJg1vOIf43u)WMGmd-?T70ZY-f${5Ev=SxqW-@J-ekvj|Tcyt) z?`T>|$M&F7a%lO^?h9AqygA|({GyicpBPj52RaSeo!U>0U9AVrwXuEYtbIsHP5Fnp zsN4~8^2ubJiyYDETa#!fn~eNX4+#qs=PIU*ocz!J|LY1=7%7HJr<8L!czuhg61X-l z^lR_pX>VkuQGb>T?4m23q~2$@YPrm!mi#}7rTD4?fZWC56 zjo1!-k$XiOm-@Ec0m&S z=lD6i?W!*<&pc|?-l;rC{H=BJdvPdFelH=Ce=|nXc^28hf58~-s3w)ZC4gu#>1iYi zYAtrLXz@*Kp7%m3h|P|V`SPCJibb@CiVW zr0fiLx|Ky=t*b?e;s$FSU8myd3jA?YOf(@JeTEA_EW3$qsrU7G-M*Ix0|`U)IyMsG zDzcfER_XGfm(QLhK6A9|f%tRUQOWQiBC&H@J+Dv_?S|SFl$iVLQBMXy$a*(oa7+h= zneM9HKL1>jHvPkGk?JsQE9Bej)-6=Js^6nttDeWGW9>wd&?S5Z_;-}X+M*$4KRwR$f zu*vX48M_U&>B;@FIrEO?LgXfu&lC@7cHhv-ZD@kuKV!KMRr`1RkB#BO6AJZo-5cl1 z{_xf9o>+&IRnsW3#p@3EyPrYr4QHO~u^#9^Dof#&3xK9ND9XKKJ%x0US-PP^`)0x? z(~s_VXX0|SMX$eN#xB4#XuHMyO7Rl`x zeVM!*pmeb+oz}D7c38wnhipH|IsRXK+9F6t?5DYf}kh8wmTS;SZzzW|kR5d&qMb zn`golnSKDnR+RvBxoF;)`2yW5{`)}(EwR3(jpMzPN-tg+|Fc}m!uCwfOQi(T$1wCy zj0Io($TQ<_jPY7YcLka~tBj|L<7eY$sEo1DW6cHL$QmCR-%zMc{*_oab{$YDIPtGc z`0vK>Q6-4hE&p!fXQPv0V+ARbn>9ZwUoidk^n>Ld6Z^QkSM1*Vzm{D0aH3aYz4ape z$6YCS)Zw3%4kcy#`>%}8*ySBrTtA4f8sh5CwnH)4VEe)3hVSQ5KzMUZ5^db0S@uqUnPJaW>LF2MreG^kHDLJuEU0AfnQLO3K9s);y5AA5;Rpxl zT}B)>^3b>mOx*jJw+D9O94J+}|LIwRsO*?JV~n-tg8ssTsHCnNToe9J-^`fB0nT?F z<;R1R+GkH2T~lSW+l!j(S&3L5|G4iAT%-Quq+yjcg<4)<3EKGB zd7sX50u>GWYR__ZykX){fK{4ze68=sgamrOAVQ7oMo{WV&{Vz6`mA#QlCiM-)uUO) zojn;#&+n0HL>u3F7iM(b|igGAaQ{HNVJ6NsdB)oi7B^C zsmQ!tGVM(-EWfZqci+2aE zS1SPG6n-m?_Z)0I+5PO}=Bj^->1lxYNW6>CwR@p1iad_OHdw<@uCo{@MhoVNT*yd) zWtCabfu1wQd73&NRHcI?rQd$HqqX)Jfi@Xkb@VTbE@LhzPP6JWNIUN{adzSW+GJQS z1lSuX+N-)u<3g;X!)$QcIYF6Wa z&Tmg;q>#O#QsTS2UB$%y;w;w?_V*swN>(lY7|Epj*8i}(#=VmN{AO9=&F)R6!B6}D zqZ8Xh3cTBZxuK_hXJfyy%*UQj$CO(rlisu>3+V;@S2<>3M#2mD2~j;ME2|{WGX?WR z+pGWZjRj@&n=>TTt9;#wlh zmXl$rNT7N=YUO{y-4k!u*7p3cLgx?C1s(pe#{BteoiJvTdtWo8PcY6xPb0K4OyEJ! z^fTIhz(lOR7LZ)TXWu`*at;!nXalE!fXs^wj60^v!8>-cY?T|O!EXc=65fp(D*ujf z4h=EvylDkn9u!3~U5fK(?lVlq%OY+Okz+#4yeKQM@Jw8X)=i*VK4%>)3dVe5YR; zJ$HHZWtIuYvum2sh@E8!@ z^C8zA{BnP$tJJNs=w5<7dqvh$@h%KI5t1BVwVP3#tjp4ExU0>jcY7^Qk^zByQ0W4}+R>uj1#9u`Lu zf~Hq{C=CSc^eC@CTN8Y96MyGvqVT5ZOI1VW2BRIG3+F6K);}zoIEt`4vUV2D2Jc7P zA8*5ZO;b(3$xAR-MVrs%;hI{-KDR&J#YEaVo7p;MQ zbM?Jn9mrk?${wUU!;_(;F|5#NkBOx|fVT9;1p4SWU4z^pE$$dzp?+f;RW zaZ0MmrMqTmI0$Yzzy<#2c+!(OgUtf49+Ko~E9HzKC+stNqt`b>Yuf*COGNv%N@Nv! zmGGBYQ*uhg$0aIGz-d^tA_%kdtE(0jgSQ!|UK#)VH%~B-PN-->lZx#VirXXxSGa~;r1Izh9P-ao$qaGkC(YHg3g zi0W%d#cbt%X)@Ad$>ZyDyfE*D%5siCj;lY^?$4^!)f%d=3s$H+P&eoZbL?^yr?)Bg zItLf!+7+j*`n~!f$geYTPM#6U{mPb!bMH_yc&e3*@=&?tET~E!#d|5!68uvn!v-~w z+*hR_B*-ZHS>5VP1hI%5P(Y}@0LZ6y7i-81$v^QJ7>U~E&(1Qu`94YTMYp(I=;aM6 zPw6X;rH+BUhT;|fo&$2Z4;vbanH6X!t#{DoRNk%&VN;zkNNISVo_R$kq|xO-oXLex z;gYCFxT0)`#%Uaz(T}{VX9zR?99EN>&Z^=?7x^Wu0zhl-=rSC;X8M}*(XLTsLf)!H z%0h+Sq5Q9Uz`Oz50NwvMCyixkiT-_4=j3(?=VSlJ?KUaEm3T1)vmV#1+vA#wstb6=%JQ|#biu+>ec@@I37>9X zdy!)*`q<1qVLg-)nez7J3IDSe{J%>_S6PE7WSH$ic-JB3qR|GQdCdbC$s3=dmEznJ7MIm&s#lT*_JWzX>Sd%L3b~=}QTI)t43c%f)@) zqKL8PS|=PuG|8L!pFat6grcIC3$Op!h`uZA9O+sY37_awQJO2IYtFL=GyH2g9a!w~ z7QL=M*9*NWe~#+azX<9P6Dl+A6Z%p&Of%?G(|M11SHPTO>mTW!jw zI@YjjdMRPst(VA}>!v&HO-@@S->Rn?vcZsK4S?!fmX#0Q3L#UaWkWWf9`0EjYZ&HP zo~5!%?S`P4Mr6rBC|RoYI6KY>%U_o*w;)eIaak4f)c+Y5;%7DmO*4Umb^l7^4Fwg( z&kmxEUYb>3MKVl7UFMShA8G&L*3=uleWLW<3=u*LEkHm}dT%C3FQKCX(gj31N)zct zX`wffND;vTC5W;A7fH^GiYJZv{1-E{+KQ^zOE zm<_SgW{XO}ZCV-E!(A9A z>iM30%IudC9>~i)1&WwI8FnG&r%T1_6H#3TrEv*ehMG8g2btZuaGf}4?xBNUOY6Bi zEnz(ud!Xt>5EeZ_?WL0yeGZpn2hSVdqwOW5s8x4$ZH8bCC+4bUFRu`=d5!HoGe6>RQ~!i&hUm^;C-r}A1)K|;F=bAGzPuT z+qsh6=`k)%r|(W-Ywe?bY-_ED!_PjW#whwOolqPo;Wr-Zx_wJGw{D?7>Ts^`MCW`o zj^#_CW-{Ak8vc88J~oVQV-^`FGHN`}sm*)Rt;Q1LI9gSa3!r9ecGUSpEq-^cG`^6O z3U@^@%!;J=F*dwv%d7wDAQufsp%(0Rk<dbA%5;EC5=s@u{w5zMp8hWnc z7^7jbxR&uKwini<(oE_etppxK%V&cX9ZgiH(PswNrw;1-u#xsF15)59!IBDl#wwz1?+>H_JxPSzYn?BPT3b)n$cpv)=K-%wWPbR0&QLtVS~YHaB6k1 zjQ!Bk@mS{S%J)hYYf-x*)*sR5(sf9ZrJQZWL}wl zclVVVVRBdOvmi)va~G@D%Z@Hq?WN$YCvK87!_k<@&pPjZ6739NqbqG>SYC_ZC!zkgK79sdrK<7Z)qZ7f}^eZk#+(RApx5vCG*i$?bk2O#W`r zAzVkC3I9w=y{@avzIda_62c;Of9vZ9CxsLkCx~RRm4w!OU>uSZ2n$=VX)z<_Z2zj= z*2@A^raPqo&BaXKr;+N7;yt-RLbd_jJ`7L>j2kAE?)kQ@P5TBU%LD{*F5D<8z6Q`2MlEaXzaPWI@p`Q z{so|P>H;nL#wGkUX>LI>2V8@Ma=qr%B*U+7_6M%o#@>Uckl?}PSij*Hffi#>SEuZ8 z10%AUVUD+Z`MDex>#h?;Ys6_p-sZCN|t_ZY@6IY#du;z7f1Nx60jB6dmt`IRf=^ zYq}*~I|fh*%LOkjcbLP;TL8lPq>$m#4~^m_b)s8>yCN#cP0q!b^2T|*vtQ0JP z_96i(MR$jbZ_*oQRWTZ2D0XN>t71tt>z(XXn9n;&0XS_WsU^pVd!Q;0FFf*W6`x%m zsM^K-Sk{Z^8A0yC@#fCsf5ER)bCZoqJr`5Da|#DG^j=0YL~nloh{w1{E0e$or*iwnvr%C+eSG2>0sRJ; z(^~i~6T39G3wTm={b>o3vHbOwkVjDeT z)HQawYD<;t8Ax|Jj8de=cWu@dS=xRgZ({EGhRVwM1@8}yR0Bo@RbAo+B2U>wibohN zA1D}VJgb`ZWjyX!YZ}1nBWf3o<3uPyHC`vS-`(j~`3V^7{Q>k7&dndD6->i@(kce; zG;f#OMA4@PeVfjciD3l>kgw@+f|j7|{5v4=|n(8^*jn*G0YeA(C>l%Fw;V&!0` zKkL6+=hdDCB5xrVRTZ{cy7w=Xp9@*F44z{rofb`F5$wiaPlgMN0DQbs&(c>gpiHBZZJoin^v+2=Y4)78fA5fuzuXY z>D9;?Xv6z4Zl{>5_)LT)xiFO?yuH0Y`c3>KL-lKFnz7oFkJwMXwsB2(&iH7k;lk-u z+ZrTv+szHJHi;R-2|^S48N)YP)a<#v0*W{=ulc@T8mzjTCNxX>Tw|2@uVwnV`Iy0! z%UbGnS*IN`@MtcAo-2M)2@XU6^PAvngL7-O%3L`K(H~k9sY=t$H^javta7z$mU>@1{Tx=|VqWXmR1 z?$~9Nn`|xCMxJVK(bt%C8|iBW$6w#WQ3`dG)pz3qWauEK%cEgS-ARO0Q|f6ArHLC; z^^B7EGLmGA@6yBP>Mhqj8co-wG%leUpH0Ae76kAg2YuU)r1Z`v=c*alDLZ~NMy@yd z6dl|lV6ghS^##qKdug%)#MQLXWapym?zZS4&hdu%+2hDh(T8$?KEM(pX#6_7CimjC zM~Qn7NpPjLbo!ad50a02Emj33s)85xvomC(T+86o7|pXI$wU02o(YZLd1_Y+zmS47c0nkLD#y0{|vPyB7*R1G!L#X&cP4~rdvM0xRO`VjHs*V-{v zmr__7NBLUm)=u#;dKpMO9(+6nd?-KIAx>K>;6Q?AKAtZjA;f|ui z_29(7k;=wY9q1Udq`N_Qufds~s^cax6A4w^CFt;~H&d=nlej{~f4gK|3$w@Yntifu zj#-+Q`*jRAQYn28&aCC2Tl(eX&V$$XNw)qy+%colbchV|1UnTiV zNg$Dq%SBoo)kUxC-D(L17CVz_nPuMyuXahM-%VsnIH?{aLY!crUUE}{=UCJ80vd?F z#6&GK*z!QUJHJKs8M%w?^@&zzkYVUY#-`nIxZ)0YtvM)sv;UH+hQ%3pb@9 z4D>)e5560H-9hAfVHzlB&05Ffv3d~q6C~e@mKv*>x|$Gul(e=7X^OJBlvfcg zHr@ z?1cX#E8n;V(&8mwXPOUJXzI>c+S%jAS8(V(u4c?;r23`K3bSD;rzdAM{J|WAY#(UC z_R0Xe^KRi=gnE-}u`k3Wz;?JNH+Jqg)0*$nkTRRQHc*&ACU*l07=OL#lHtV-mYjA= zGlP(?aRPL3Id75xvdIhd00=g8e4b99pQJiS{4@cXRw$o<>$NHMPQ2V&#~y#*gh%c( zOgs7%$I%w&byCEMRcpoj^?WZm_n>)%Cc4-D9UG10UzuX$#0yNl?_Q%SvT zF-Uw6b24636M7IS9!m7VKKduH10D9_qQ|}U>yw?H6c;}Tb=W?%QyXEAk{=%Qzw@zG z8?my@H2oTLOXiuFi5RPTQurw+Giu}wP4112#;~!m9gRFQ=5yHG|Lh^dhdo?5qHKh8 zEf*nT2$=HPyZ*pIdHVP@L1s3#4^g#(tfg*CAX(Lo>43iG%R1=@OJSw+%gatjUWwoC zHtjr`zp3(jQZ+p`n=h}E^Di+AW$G5ER>HFvp_bN@$#2uMGODxO0kYI5DSr*8XmqB{ zKf7I02}BHnM)=nz7ojHT+sxZym6iK}HV<2WqdxqlkBcV(f+&vMLUc@SXf!|V^g*YL z?*~>TdLI~N=A|3@F?1ZR9o8l)HoeQ9#1)Dp?qul;SvS&Iv>aNZGJ2kTA@N+`_7gtm z&$#i*&_(y88?nK1RSxHf$3=zK-KT5CfN%YBJD%gPL$=S-0ar&8Ju%kpN_nsIh1{Z; z2?|%ORqmLhc*Q7(?Y=YgF$q&=(cuguKO|IPr71P(<^QsgAMxvEI(bI{ET0r_=v;_dzXtoaGWMNnzAQJ|_E)gM20u zM%fNEbSa8|$Hlv^c#4f|5?^PzoWyPD;vg57smXIO+(BIi;?$1k3MeXhG}9{qf{7}$ zkBcAGY_6r9FMg8_54Cy{jY<&r&qwJw#fMY>$mf#lZ2#EA)Znbsx#0ZHjQnz)6C1Qn33uMbF)4#SPpIt zM11L;Go|mUk2G)jD0Ox<$(b#uCMJ*{DHK$FyM^F+-x2N7YRPSD@_&;$)b^K1%@t}h zP@C-S1L%EylcHiw#)~POCDiJ<4dRS{k2$oT)p=N~o|lhRJHpr~F4mgtFxm2M ztT0Axwy9203T~K}Wc03TB}IIha~5?d57s=@xYpZ(l;+6m1tf?T1@h#4_<23%!8!I} zp+=IEYd3)f9EVlUGEv&prc?5#;C=H*H%^F0vx$X_NQj-x_$E7IUKsp2pC?Q~*T7KH z=Kn6cMu?MaQP76GEO?@5L9vmm@GW}v59T)E7c(EIMO63l)i$qn8o5krZn0J!-f#Rp z`c4RZ7y7zGy0A$Ek9^2{DdT*XX+=+rxH_ITzLG+w8 zoe+s&dq4OectP>p?UEb*^Q4j7XQaKzQJg@&tp8#c7J%;Qg|^97&fOusGPg^$8GvUg0uJK4$ik+Ow2Oaj5N z;m??oC}QU)G?nA%{QVG3+j~k$GitZR67KWQTV@#YP@@0awq?J`R=%?V+a|fC94;W) zA@o5U4JW1306WR~tyr$-CXB2@``9Jt9c24oI@7y=5#_>);X9e#cmL`LW=1aDxmgls zOVk8m2P#>4)4NMeiK`?%G4JslD^;)!l~np2-4ao?>{bg;1ER;A|IW@+i9ygFMf{3g zlqo2`^~7v(catt$z}>JzPqK@Evz{YhvH2ZEK8N z9p6N5NL3>O>s4n-zxZexKXbB{83?Oh9crVm>3`w_k_)K*QwvNS z)iq@oI_u1p760rG=ZY;R*Q7JQQ*UQSzhWQqfy&m2eHX4BdF}&NSeyV@P&%_y7B3^4 zQ*Am>dK{`Qh;8V=f~{5ng85?FmCsL?g@o-wP*2)5T%lr^J4G@PLJGVap(;PvKbt01J-yx8baR7r)r%@b0OF(dQELJR;>usR;s4Q>EVc`^sd|S1~KweCj4$R zc98Qf5w0riS!EU?BV?n@=(UizM9(gJijD^hbl9Tuu`A>nz<+2)EIM%mr5H24mqmT6hm3`%pv`xs8) z1e@WGyJLo~dBh*oEr#T(HpZ`$U-$})ImM%rLYx?;fYkxqMn36VXUmSO-6unJk4vDE zY|+3f9~KW_5Z@9iP=oA11!K>j^9!LkPp7DNC&gl3lv2+k+u`jR;m&EvH`C{x&#TEr z7aKp-U*gqBTm&rn2WM5|y->Oo20LJ^hMgFz*Gvo8qqv1c0-g%$J{t*N;$A16*3>-d zTGX(TL7|pRD^JRWDi+Go(25QWaSBZ7U#JP|a6D=!12;ga!aHyn%8BjY{1GKD)*X64MKvo0d54!u7lo&r?kV_rGv1rj&n^En)Y?WXanP(! z>MoXmX)Cd6aG{DqP&H|RkAo_w9&qk^v6Tj)%GZ0r0)HaqOOtHLzbF^!lQbv9{t?R- zTSSg5Mi&gp63X~4bR@LDC{a3@BpAv&BWZ8Bhkrxbc;R82?_A%`5$HGw=MyjT_dXNf zmSW;C?U-%!6j!TMw3wlMNn_xIi0a^}EjqOdC{4f=vJ%5(oP_JCF4ipe#M{uQR3K1$ z;xH=>RXwL7ziNkYC5q_<5Cd_WC5MUytcJ`^OnX~Ll|b_ZMq5iXZczwO21A-MOdU%t zKJ+5b22(i#Hu`-ap9F7TkQ{5GV~Qz_qi;(5(a)DBF!V`l<^90eNROgqI)1OG=gnyiL01p0%BlI;%5q=O?eeNsFXGfR_f5l_ZN6h=%+Y_(Ff6xS(6KcP{8QiZ|^M+D zHkF@n5igghSHcWI<+3XW1D7*~D}NHN@Yk+F{wB!Os~!jq86nsI+eri}DdyDBhAzh_ zsAXb!;wp&E8n!yv9I0@8r!MLP@d^#wceh7O@GffZp$fk$*`&?3PR{!cuto~uF;TqB z{cN;{N5bK3X!_huq2vjp$&TN9i6Z}McIZ+?c9c(i)RAF#p1a5R-*GUoTl+QGG#elJ zm1v6%JBPP7Rsxuci+LcE`%UB;daWhRa8H$k=VSf$r6Q)t%KPtv9CU5+-m&zXH-7FoN8rMvya6jMl+H{oF^BnE?5dH~P1cc~Q}~VE8%j6T@GrCz)({7njlj)g z5~Rc|Ae?*?X|7S}^$;5JQvG&`39A|VJ7md9X_rKfR)Q<6$imu*iLAdLl6 zg!kYKe>;zzxooPs6kca?!YXTE6Jm9NvylOC7rF652?OWqz3nX^Zv6l?fw5Z5A|kE=BXKabRoJ`O;PX@ zy5p#N+Wkg&kC!~2 z^tUa2%oLNp*>4!Sn(~xV4YQ}p9b^4rVNB|47|vv9yjb+hh4kk*^4Cs1|M_cixrNQ2 z+kD5$ZM3MWpQl{lYFxu*mWzH)N!iL6o?wrU4F7e4d98;v?mLr2c};-9uV1kJu~d|6 zQQrJj0v{3f|NkA-)sG5OIL~?X{GTn<8IqYNYR0SR6+teImd0P3h~Pi#H*To5vIIn0 z?}d8J$ZNU>5MH}PbP&8M9hK7C_sReNa5jSy+WIH;1|bwdF%(a@BGv6 zCH7CdoU((I4&{kY~H2%gg^G?6ePbF&%E>8-ZbR3&`d^d` zr@Tyo-&yr%;fH)`s@kHfm1n$pWgj?qz312}9*{p1$D09ekwnOaBzw5I*=&4B+V68v z*{q!5%ez@xM|rujQu-md_w}tMlP*g#tZ5CK1vt1#_2oxu5&e(<)s%i-<7jAr_)?*+ zxS0x~M{o1dO*H+YRaaw*W@n(XuJ`#lQ`I`MR(dcvV^28{Cd5U!BJl7=Ys^nn+QaVZ zh}*x~COo19#I<`Fyw{@ZGF4a`ML-L+C|&Kj$YMHdZ>Z3#_kRPI2igEz7>(DG^mf!^FHQ!KB_SpbAE&M$f6+$xLo`czk!QZL?brRv*L|TC&DNw}esCZLzp-54YVtk!VfBOIdw0xhxv0iu z6k|wMx3NXN?pnNiY}e`TkecjUgz{tOD4rfK2CMBJe49f(C-?t8$UwF8@WYd1%)btqCM=kbRlu{!9fu9eL5(6qZ}D41 z@m`P!lt+2NgmZd3ZddaCFqkP7mq$K~=VQ)zib;IE8F9Kdk_sErTtsq(fnIftdGGtp z=r&!{9xsa?PB;$R8GM(DCM~IJ;orXQ29w;l9%PjE4z7eIB?rYf)v;o}dcqcqRIk}C zcLvzQ3fAU9uAAQM&z6t8jOC*4N&iL{Fy(Md72E4Al|L+Ypmw~JJ4lCiRU6@~O(QWb z#oe=eJh0iHmY0yYI)~z`t`=#k#kUyB17=<`B|8+4!a!iP2AhqMfH{R@r zR+%>5gX{$~hkTf;Ky(-#g*yJ^5kmsu0cnkqzD4? zOck*mfxZRP?j(+UDIsAL0+MFb3t$c-78D=LZKd^{vdG{ttT{QE;{DIB;H0a_hTS0@ ze7?^&02??Ca3)0Aaaja7R1$H0W^bi)lz;h(GGXy5jGsH^jU%$?(=YJGiIPm<@Rim| zJQL!GI@UXfG6cAjfR%dF&?M9^|8Xcn9sDircb8YM!W41m?CWKswi)+x(BBWvUsh=O zt@{)?4@{JU1|+;KJ-oM?h(?}()S^FEPMSwg%fxqisxf)nlw=Bz@Tb%^vq1{&pRmnS zdN=oFeN$c&qhS^Pw4CsjdCz`<+Cbo6We z)wo~sZMql-9lPfb;-8O9_$CS8La&B-Ls?7Js=kUbxKc^g{X0>y)9t@Tn&dTy4r*GrhtGpk-y^kjY<4BM`0ftrKJE zgO*o!BRMyB{>|L|@?xPXh!M;Z!6+Ez>Ott%v>(*cf--;-Zc1St_2OPnj&v`^iVofX zLKPm4ITEG3T=0dsX04xl1orbF2IXk0lk*i-f;E*E zJX*^K$z-|a!oM3I<=BDFW3s<|CgWRrx}=WpmHaTIz=oAPl6;}l@NyYhWJ6HP7B0kX zM1PCU6quyzUpwUC?F@Sr4>v<-6%MQVxy)O#V3ct}oJ<_FQ7dol}uwb|owq9UxdF6TZC@kdGRMKG5c^6YzVm zGa@JMw|^mC9sQk|Q0{bEr;qOwWr}yQ?4S>D6|<_z9ZY_4-GS<>wja&Pj&NWxcG>=evsjl)9Ukfv5o7i%F@D5(l5A%b@)_;QFy@SA)dKARGWKG{#56rL@ zAH}8%LoK7OzjdcAG;b*vIzwACjP~ZX`!G(sCGl}I;bPOt|M!+^#RjJTR@1;fyLy?~ zDqJS)mr|Pa@T;a@uF~_xRsHx`s$24@wS8%QxDaQ3I^DCrQ1({Jk9Ju}R{S5K?hO9t z&m=&)T}HaVTo)TFM4gYrjEwGuS;Pa@%TD`9VSe*s@h*KL`<4rw7*-HHHN$3vD#Rb0 zltuMH_m(`Ov<{<4LJ1gzm9yt;eeMjFhU8?|iECZ^`QoWgHpwq#5>s^#$P(n!9_xQZ ze@(W2$tPJ>sxD*X$EMl4>MjW2zxy<==Gi?aaO3@|rGLRAR{^cZL%E6No8m)^7?IYWa zIjLw4Dj8bZs4aExlj?bG6QZSR8lxP7PNT#{53xTmO@Ah6@|Gp7zf%;I7Q!AV6G>gy z!oa5QA5ipAWK!%b^3IW`HqEJ(ZrXJ!NNOvD`=64Q>F&WIqUjMO>9zY(U#Bzr3Z2vwa9S1yh+%{Hb`Q~^NA>ea}~UTLiu zMDswcfO8YUEr$kwIpTPd>DZ65g=>Vhj(8L>c2D)%7038!Z0NUDQ=~ePyk@>)fVXE7 z(a-D5{%vE2UoteR#CZiS(S5%6>;huN@@-wPL=Gkxt4MYC$E?=)%NsiP54yh;=gN4L zJpSjQVgQ~+X8*PBr$d=dyT!W(f8X6t$-lQeiGW*_iSwqgXm}9qLbrhjCFK%G0Ibz$ zIoN|}T}s-mgmaUx3QvUSg`lq5-cMDg$I(=Yu_nK2Zq@PSWkeH>eOmRKFph4$0fJLuPO8Q{1Njlh??wA@W45+o{dw(sSFES2Y9bQjjXlkXT)QNHyCXNo$}%5lnE27=jq27Ts4CT@cFaYp?CL>-0OWbj;_*E9bTpj zLo;68cA250B!*E^sN{hWQI-&dI&%0xdnOVD1I`PXdIE^Otf(-GV_J3M&G%8EN>Ac3 z^GxzDbN+)$+_fYg;bp9m3_Ry~=chWyQ~X7Ni<>Csf9K-1i2WZIH}{cZD6aV$6@d)t ztJW9S-;#w81ckjny0PTSh@x2e`S$v`Rc(!iqqymWkLn(uV-e#4rEim`4 zmxZeK!Eb|#Jg!#=+1C>4TkGC2p;CHrOP#N6yDy0E7~gdjoi}0BlTN$-sxRk2!kRjf zFVug!My*3AO7%PgV^U<&KWZ5U;6(kw_=vouYr+B^`=8N!Os=Ye*%w-%%MXrY)S8M9 z$r+`Yn&TM@w7KJ>(b0)S4=-XfZOau?-d!2e3bc)UQcPgqa^QI3gDH!auS@^ti19Cc zXNpi}_}W4cQU8bQm3=L6b!#X>SjF6va1=j(S<(_oFswZr>OFj4LGAS_#?8~GG%z^; zO7<$aIib0nfH~&6Xz5ZrwE9sH_bzxr*&n?A(a!Y#_m@BJ5PCA$Q?XAxDuk2?FAJN` z(@)&C`~>?DQvV7`X(NierapxJC{KG6UlPWrXs;)_n2JyXFedo>*qcG z-G48FJqK=UxIg3H&41f5T8a$75~wc6(m^DzVb>B*>OnUO8r)}E&c+Cbw{jyP*ISg} z{6Yye{q1&hqU|_zjL{UYP&^id_!cq#J68L3>F04KI9=UvNc{771h#gWs2Gb+|K0@Q z-vPNUCdw5ugy$LBK)RG`Pw3rv$p@9qQMC4&PkN={q*a8n2Dne;5g_tT08Uw}$W^aK znb6ku!_ad?&WK{`3fTh=m{%`+RIxA?Dxcg-@lD zM$WAtd5A&=cOakitiuJRq(|v(RBp;gTSf*F%0L=?hje%d`=3g)*kOm- zzS!YcS^k)?iIny8I3l9+^Z)?Cw{>in4X})CL-ijkCzM6&TH)#_O$=QZzeZJcGm%Ys z4IK?Ka!#}*qjc8%b!DwjGkB_@Ng&1~P5qiX*NVU*o^rd<80ZH}h|*e$h+&6Fg}Um) z%i)^oUNzUk8px$^gZ(EjrmDQMmp@_x&X3%1=$zri^zp` z`f&V`kWZ3dEa32ziiaZ-M^yr1s7L26fHl!45B`(G94#22*9!oU5Z_4GpC}ax^4xmj zO5>mnG{uVESBue8`kpqp;;D7QtW4V)NyykIu+W2^2D$0H2;AI^NY@+-z6_e+P*-yA zYGP z#iBK`D2kb5W@fLE&YtVcjsh3?T5@|I`$Z@ zx*Lpq5#04IOB20}i~~NRh)1QuwS2%|m2RP)eRJ8A|0IMp420EW1gjn(7-reaG-cW_ zs%RvWRwNa#shqb&DVe*(49ySI{z-4fu9*RU?%vm!@c&D+@FgMqQEi>r zySKYzp|4L5M&bVd%5$EK#T<0%d3(66p16hV_JTKC$OkSwm=CXtKPG{))(Y{)lR~+g zRhyw?=qF~hZtNB*_LDUQB(-=GdeNT5lLLWvJ=j1Ag z-y#;HgHgWhf%P*#JqvC%rUBbCU|hfYxTcYM)S|hbYu>UWU{%n@ipr4WsldEtLZrn} zb7$zBVd-ZeVF@-+o-wykapEBUS-sHqB#-%fjN%(8nX{k`VR4G}pX3@gm5jJog{!Da zQcL%MIisF8-$P=_#~6usgo^YNQB~ITlReU)$x>9>J+~>)IzP9De(obe3ZkycS%ZU0 zoq@%4O^ainec-tLHF!S+wX?dtGnI`FMSGI@9CSqKa?5g7 zEdUWus!E1V^T==1{Jz^IPqi4Wd3m;ymC~B$n{ABOwS5W7Zf*mI6yUC`+0?&J5?JTk^(0zI! zaOCa#KeyfKqUv=cxjYj;pRPd=85$YSq++yzve9GBGpR5JVohDL9TrTp*5Th2J}AucYNYasOv!UbU>_%g2wHo>8Qyb&wV+PjiBj+F{DDoS_8%p# zP62_#wH{Eb?I!vVV?`0l1bn{FnV?S=1hqlxYI6=ts^H-3@Tf;=EMU#uW7zzw?G)=U3=w3IT>1$^VwLaJ3&gM}*0qIx*uP@-;NFKCi zH3s`hAH;r}e`3b4DAN-kg4#h*mc6^zdE)$D_Qq~#OF*75q@LLsFev&>WWk!Py?r6D z@NCsiCY5Z^Ow8J9#VzvG!!}y88z}t3-egi$U@GeE5zw>LY5MrrPKY9)V-2k;z-!$gH8sPs>aYbilr(zQ&i%!j0cIgOZQ{XZT z7Z9y-LhbUmKNM~yEnMOHOqui2=rUOq0WsnIJWZ_8S|B*PgIKt7i9l#Hc;SE=DT!b2 zmBdy+6@1LPZ5`E8#^$eb$TXg4bXI9;!{4nIwSMbnAv=9C{!ShtqvBF8}elLQiKi?pj(P^*8|C|@il(g8F8o>#1uTsmr+j6m8KzmL@Ak+ znzl(?o&96Rq=p=SQja~qU@xplxmp<+Oez6ctJOE9FP2MN=jQ@Onc&AyLCnrVMMab= z#qMHm-tx;k8XCQa~JzV&IXFjKENIfZH+R3S&3@=JF z`dj&@bPgb_joeTc>g+PklJ18YxfYq|Qrq=X7oXBNcht>*D!6+xKIrqTxp_CNaX;YS zdIO^>y#qtjP54VLn}3o_A{B>j|Too01GYf&ay<6WU9Kq_O1> z$g*S_LcmLvRzp`VROCO=fR*ubuk!l~I=m7dT3Menz{P~8YhFB7-_OyQ#}8#g9Z51n zeYrBjkKR2fq2O+kJ^tkJPhb9ccYb$6qMv`O8B+!aiLZ#f9gt_aU>i}^EBnK^<8WuC zUtn90-%asZ*CKUBl+!L-V!YM(uSuJ zO%>B9!d&>%zMFxkpLj)RjM3+yYhnRse1y?oQ;&$@kO#|D*CWc}FKulvdk_B`F-7PA z0QYzA-{z~-!)nHI_}4ef7XT}(jbKX(^Gof0-kyhALcg6ieiB0e=Az&7tyeK4`*r*8 z8<=ULRPL|$4?j}Swg$@iO78vLK19g(|7CG!lZ_jc85<$a%#fsvj-i85V^p zlC&_x_roK1j1832!ENUjMn$ouip>=6w5FeRt;f3W-xE8$KHCaR&=22`jGK#QMhYmK z@u|b8knbw0x~j6FxP6+Q>S7&cAn!FNK~8mNH*=aK#zPbu;cG%lC1pLvyR~m$_ybqBx9v~Y;2AcjR0L9s{Na2gL~Bd6S&k|jkp~iz z7x_a$qzEHhpM|r$y7#KBexe*ds^W5s`~dYn|Do7RNR&gzoAWypjNv-t_q~;@ejny& zM}VOZncNV|ARJE;#Q{fZzly=Ln`|D8w*+f_!vw^e0F2bwVIaorHT64erEp+K*Qc~S zoql3WjEUXD{>2&Q(%rB8d&8dd{_z%ny#_DX}Fv)Z}kW<^zN;PXY_3ydu zh${IVkF@rTwP%}25IkKB?8&+t3}h&lOzs*r@lPKLo2N9C4H_RQ-l&w05&Asyuz8k5 z2C`xKv4PU@#1zjPk3DO?MTg*1hgCLuT zus6_UODexckPAE7_WT0=)WPsx5th@Z=^Rh6zLjj3jqruak`qEcg}_sNF81M4esH*2xR-&`ix%8+_6lzg$F4A%WsMo5?E~{1QX;RUGPs zYogE8s@yyuMqSg(e6|;qe7f1rH`>F4oqE>fgLppD@sok38Gitn7PR6VRu3>A5i|T* zi)@j{k+tqAB!n0Ec7x`=_!(#whSfRCN~cUkrbV z4c+d_YS(Nmy&4)vFD$cYr}NAa*hb1%fHml38B-lvf$lTp$_@u90d+G(+K3tP&1Hk4 zrwi#2KBldGo%ZpxDC~JpSifnbK4E{>~Ezr`otKe&e`>jF9jaRHBKTEQ+5FjvL#?r*JNw6l~Q|SoiM9h0Xxfm zzAHcQ{&c{Y>pi1WxhJdiJN}xL0K^icP$gCQC8|;Ia94UA6Ta7SA*z;3yNli66U|7i zsnTlu+im(bA=*O7P@25E)8QHw%gM1ga55u^2u9&7B?A7QR`vs~pr>IS$)&+P2{ZV^>FrD>HEOsS0I`M1vtZp2BaYz3BG12u z_5a5FJ8s%fz4eX!{!s+}tG3M3hy0^&TD|>BX&1BHr`dJmsK>MD zDsuS0tMLI@#!l1nI?29txpz$*bb}BY^FjBw8tqy@tM&H`#e!`fweBSiZdQt>mk-4K z(pZdh#Mrfg09tPa$u12tsrq3k*oRQ^ULr233_^HAhv2H}KqFT4pFZAof_!c+oZ~=r z9<>B2)y~g7(Wbau@oRgrUqJ%|9Eawh^)4z8Gq@H^>AE}0O(gI$S_ZBrL;;-%57p5# z-Ti@lmNz*)tqUZIi>9$z?*aPynqw(BNojuq(A=NDQa=D7e0b{Ty>*I*nM~1S=xI-N~22(4Z3c=H^UI70RykGeRL@P9z_?M6^JfCD- z0;H*f9Ml-R>A`*Ru!GG*Ql9coj<{51vBT$gb>wk$UA9fvxU%wpBavGPdoW8u)7xC&U?+@gDw`_()bK7@7fu7LNS1?TNGtg)hXLGY`D5EwQOvAdd@P&XEsXoC+!o65NB_$=-HsB zk6&Z945E{}gOZ#u0lNtQA2#2%JV6;_A4$LU7v)U0wD3=K&@h(ypm4~WH>?^Oh`-x_ z6S*BMmMkP6&grSvavF<6RO8qS2)J(__-&rp_-}9*KTEaw7$$nxt4ehFj*Gq`D+P6} zeCfz$6u(~Xj3)uY-&T;1j|A^7fulN%vZTXeO84#)l0g4qK;GrMcW1!>2Wyb+sjdc!8U>*U=X#9>7bZXADQ-WJC9saqrql8V$_w2_fJ9Uak0 zAI~2Msp^FF@O)IP&U1g0mU4wTX8=7NC$F8phj=;Sn--bq3*3*7d9WwePmuNS|H#T8 zuJ+no{)ChbR?d>a^#bf2$&=}XSvEx4OO(NnHJYlkE`IhDPFElMGfdYAchwLq!?_7@ zSjLI9D`?x77W7y=Jwp!YydiY;kv^-_8>D2Fawf(+aTpR5JyUdPPyE8KmNA1yN>Jo) zynik!{>kTYj8ftMsfa%z?aTSEhO6GEla!}Y&>imezRLWOM`Q}qKu>*RP{(OEN8A0< z%)4X(Jx}A(#*$tN;?t@q+@%Rd80EvnyHFLCs*L zbS^GIxnub7Mmd-EDEcazn|vycUcctX)r3v-%2#kwoQuZwYHBgji;RbOi~L#bRM7@E z@E!6d30uk7J#PRk@XUY^3+Kn7evhWxrpp8IDT_7wDa1+tMgr>*l635@6#hvuz1$RE zm=5*?8>sT1R+uC5W>Ud%+wOpiW{dp&KXR?-36Q|%L;Ow+A?3a;eZ24k`?8l}Ayk`Z zy{RuwWEVKq#OyY4+R);&0b%vhR#tSx{Tgh0Yu*8!0l)FUiLa{Aj0Aj)2xP|Jxc`0^7Vo(h=YelA)cmCz&1ra*{VE1 zsM(~Pb3Z5%KnTDfJMpQhNy|dCT74`|-(@y^3*)^}-l?4rDn;zf&c?71)>3BH*u~zS z*RVgjicSyb9GuVKK+n%g9ruy*$YU50W-t^XxuBo z^My+9gED}d(K++)y-2NUlD%cxgU42;SQNSTon%zD(1>z`1+VWCsBRL`)H z4*vV-Oe$~i1!yjOST`s~;Q$&V+H9Y4jsL`pimnT>V1ckzRJKH@}wc zJwQ)aST`v<(wlgz>2Lt0vxo1-!rW%hIy-flWUqx+oPq-}>#P z?7!6j*uZ1KOKRWlh%ZGEGe7%r1ClMKFdvV!q@2od|0eY*B(7``D75< zo)sk64V;X=i;AL_0-uul#kpDcQ)rEw-Oe<7vlQ+<06&L9{$Xd7nPP*ip#FfnZOZ^} z-5$~4F9v!&O3@DOdC&}yI^SI2tki=!&ZE3q+c+|xl_IB%bb^xIR$6r;UOimZJyaGf ztZP&nx8&cyPW3YxPN~5n6yYScqU7X0V3kly*y_^m{4@@-X`;1=U({?I+&~*Rb zQ3HNd#W!#Zr=20i4-M7?(*J=3vABfU9<{cE%Ws>$VPrpHYv&6~69Yg4W7~>q?%CdK zH$W{OJghzI?h)*!Kf(JkJ-+%j3kTV+#Huhzn4}^(^-WSKg;YYaXmd`+X$b0(;mr4; zn7$*VFHzr*egQRibTS8zl0RD0jEZ1O<_+1GL39`F2tcW&aJ)ll7e#r064y}mdo4*a z&p;SJU>cHqpJtd4foX2|uX^--2#W~N5vrF8b~)<7>*1=H^bRyWw&dG~VYj0O4I5P( z&#X#j+AAO7qz@4}&3~g`UFy$T9r%4+A8p6Ue83oZ>gs!#Aw7)g2lTHfe1O~J!-0+} zTPMXYpYj=(jXlfx>pPxtBp>9JgeEa&NNoh!b}5vUH+g(?IK+mLHXl*X@N|ZtIY8cG ztuK$km%Dh2k^vurdf@5if#N3d@RG6iuAre%S^{Oy?r$&nlZm2Y_>y%M!Gx3gpD5oL zwbdF6G~9`Q3P|fTPM=zgIj%v}q+(3ZxatQ`S!Wq{~g+&M4m%EY#fp<|Z=zBYio&bl!*hzsp zdi-t@IX#zIg!_Z_Jpi30^A{xgH@rmmTeB}O`stfUo$cp96M1v6!E8kx_iSCPaYG@_a<%l9lg*GifJgRz$ zdU-l$Pg43ND1K5Hm%Ty2+VnD6_9S_(CzU6ZM_2}6BEOhAT*Hf5-{#vsmUXUr?E$oO z%SQT1CNO#aT^}NlJ^n{O@KedyIn{Yz`jpw+z#ZtKlL`Y4%e=58OUOoerQ(FXjTA6> zOh&X1AQ7ZhEmcyh9?E#jU@C*NMGAD|*2;u3TXh#1_6*Cbcy$pA&emmLtgfqFVA9X} zb{(?MMPqfbOYC4aOq0JY@^)G#Yx27ji0BA^WRWU@mEQS|KfT_3e=rJYK--+hAbpjb zk2y@lq31w)uS9gl-^0~9zs803YFK*Y3o+I*m*yslmyjnh#=YNvBZG|0!9?LSwrh)0zcnDbjW%G17AZt!Oq&5CaP^A?i87U;7$1; zia7v=FdLmJJE-USwj&lBVi2~O`2P<>7#>fI3F|%v>nJ$!u#5`EhW<4h?KRO9^#M)U z`TE}~meRi&ja~dMGHT@7$`ND+cAF8tZ5}l#4MMHMC)R`Sg0={6pUFZ5$;;OABbY2& zjr==)c$QMGtObbOsZ!EdlilYDLyVFQDGE zeg-4$e)zE`2FMBcM#ko)g)vzz*@z6o*=CxkGZoMVAIx3x-uV?lCq^JD9#^s46?S5G z#PD5~hETX!^a=axH+^am7;lBOpGQ7AybrO2#5~k$F#i5x6NkY5`W8ABE*1wB!F=`D zFtd0S`2)HH&6@ai20aJE%(t`p6BD5x&^y>KgwC)cF4w7(xJ`10-1c8Nk9-hjl*wI}Q<2X~lV|laOuw$99I;4150->i-pn z0I1z85Wv9097mi+Y}UzP7C;-aI<)4Rhwz;IQ||jm)vVI?!Q8BLj5xfCqO8vle72v1 zTQjwf`}3YcrfB>M`ag!+>C($itBJZyYrfpP@2rnsbtG3))ULyT_}oLHZg(g2D?5|`~rsH8;(KaKnC-K6_)Gb|`XkmO;G zK+K*`w)w2QX@mP>J}|WvynSQkJE(&{1ROjq?k(`nb89VL5FEh2QvIOd^fm4NLFkbi zl-{JdoMHRWO$YTf`TBAF7K%4(q=CtcZ>rl11!jH`Fkal)?A zn-UnUy@!;TM~4F~z~MHErvkLZVOd7X9$(0+{Ru7>3HRM>ScXkMFL~Dk2LR?9a%SOL z5(DkvPZ~wJ{E12-E^nD|9$HrW{B)g?P#joP=+XdYej5?h6rbv+gwU~%KUu@cYCnXD z8%DyfdxGv$sm2V)T{0fOuQ~O9!xp@$I-i-WSE0J2cM8(FkdD(X5iS)2?7^6iLDP1p z;nZebHwQ;w%m0*J_un~QVh#{O;8A&l(O8(GHK@g+1C0&!<7(zTtt`jWok2E@gY4S7 z9GeG~8=S}3NYS?tKq|=DcSJdr>@mn6NF{r(_{m5eEEoW#(`E;jeHJFB3QhO=S67!P zCLDWV$~R;|vTGMZA6;*0rLZ3Qc8yMh?^JwPuF5LATe33<$?g?gldyzJZHR;LrCD?U zFRocF`*@ja1hD*EJ19X%p@5BXfdzt}{@7vYPS?@%`VT@2db6%d3|G`l@%U}`D-fu4 zyfLlWXKGga(SEsj5Wx~@%nGMe0s`PtvywV0ZSA3tB)tVN$m!;^08I%-k96S}tE{bZ63kiJuL1pq`E#Ut z1`r*2%vsxwP(~X*ARx8XGdJqn2_k_K~>2sXDY* z6f2Bz+x~Yd*vw&_7t?pD8N>~!e(Bln6W$IDip2&{+ZpL) zz39H@jy3sK)WQ1L)0I{wFd=&8=e1OL4W^~zk)9?stut}M^{_e8W;3&e%`C5JqMnuBX*WZb4{H7JOfmuEomUpwq;tv zh|GR4=Poe6coJ0Ig$|mIc2)2>omN1(Z?^Z6pU=Vi&UsY49*$8;}797=}cW9?Id7rM;tB+UV8uknNE z$<7K@Sr?#&gpe{}Z4~kEdN+2rw~jh?{^JekOg*|&87OO2Np8PBi4QzuE`M^6DPY;2 znFXK&jTiu_b|YX9HwFRlPF*Z_iX}tZ6T0X)ZtE>GG1>wnrixAM^aw7YBn4|S(+R@G z#4*|C93Ud|7g=~HZHAUn_m&7{F*$R9fV0{gp-%Hu~Goa3P= zX(qA6;k5kn1Yq2H5m1b=c_h<^N(K60|J1ncjRL)BGS1JRFU7_ZkLf~_24Lh(!XvTa zZa&*@R5YCSZB&`g_Oin~?oWbZKYa-O%Ifs4_(S2D2f;GpqsIjMZ}zM6cd|M_3ZEY^ zqcS`*=Y>E-5C%-N>|H6U7=Y1-5w> zcnR5a(}h{%u5Ir6d-Lm>_tx^4%DHmU$ub%G9-U)wo zKs}cyWkT!Q4u3(o+||>d787j&#Il+oefz#{y67f4t|JH`fFZ1T>vx=hme!M7=f5(6WtOTnA?<2KY*;xTxmjZFhH%wBp7(Fp)K?Nw{ ze`QH5P22{i#^g74X5lEcpwII_$9z^vDGa)SZf~ieHk8a8#A>@jG81E00OPsB@cZ@y z+2uf9CrL4nXIQpPTCxRj`G6$`TDQ1+=lj_$ zck?%|Ih#^J0N~u0AYmx75^I+d&?f&pbFsMuoEva}duOA0f_J64sQ64=x}&;_q)PUM zdL$aNZ*gDeg*1Tc6#wq9$V=+`Pdb_EzCjt#5?XM|>Va5rV3EtO@}{i#y{}|%Nj?86 zpUm`6sDcKb2!5tDMjIF?0!k5@Rp;AWcfl*ibZKHk`q80bdW9IE@adZTh2W=j z_4BQ6vGhCi1l^~So-tv*7MZ>@JraWkv**}1%+jZY^uDvJz%0TZP+HT#zEzx<|L)^k zA(amb^NvPC6H}9jx2#eGAw0}Uv_$g6Z#Kl}2#jx}4t*;HM6!nT8NVfF-TH+YGY1UG z_S|YB(A&ASn52?C4vbcAsELa5!7JZ4U~&ny6_WZ{oOyomro7zTx_a-K&~$xb9HGgK zRgZ@Z*S1IDqcwwN$_IRrfS}TIzrYYKAzw!uLj$0FjRF+wr=ATEx)uo2&YkMd%(%W1p8nyBo9{`-FV2)X1QU}Uy*zhkv zO|Oo9fxFu|!PI$gMAGTF|IqhjF$dQf`%AqRYK^Qz~k zJ#fb;G{y7|>9Ciy@Y`~Djsy91!G`|$oh_!Z(zS3_s}B`3*SqmCqB31a=UU0465g?QS~Qi2Z& zcUwusY)e7LS z|Dx2b3hxM|EzW8|*AJtvjjMMM^jV2IL4!91OCx{)THum%CQy&r=KLRAi|106i3weA zNtM&AKChaM=ks}dz>~#GqSFF8e*bCZ5T5+9&kfkk`1T}{Bx&(wM3~Ua1Wg<0KPF;$W}gF~X_&D5aMV%#kvdq?vx$g;z}}oT znUtQL^@T7}7bq5E&D;uOzEusoNdsZxdYrS&03^oS@OflYMs4ctw_$rk_e6s^UJDHN z#OSHEeoZY1*42snEsl**-|x8wk?Z*f-qk-K4NT!Dmfo&TS9Cv6ESSQcX}B+DV7%Zp z*nRgco0;=0E(NyVZQLX7jqra_Nf#jW7fsDf5F@i`HGMmoc7bcVzMr_DVg@J_hWc?h ztpKd08Qm%3LRDaH*J{uS7ZJDe>hI*xIxur@|CSp#tbjh$;9&asyQfk>GGg&y%zTi+ z7dClseIt%Xet&-X#IWk2WRdX9YQv+6@p>k^UCK1RWQjt0wtoPg zKY;eB%<%Ew2VfYfLguYy7|@KT?OZ_;hVwweRYHMS*7N5W&eMDSqT4NXNB2IYF+9pi zJo;kajcgOCr*YXB9xteR0K+O)_A}skHYIs?f3Dd6$5l`U25mHVtPD|cs->M|jl*Cq z?_FPUp10T{xaBL#<=EEF_YOM^Ldf|n!;0QyOYQuj-lFExv6+7K;=`2ZRiLX&pt6dR zN+&|h=a2uwS>G!8&->W&q}cM~U`;34{r)vg!)87rFUscEEJ>PI1k$axF8yQ_=X2BVGQl`uyF7Xr&T=(G(MeXHv4kw6e z6yRM^au44dFxLQIU&_P=u0dthxaT$v!?i<`Iof`9qOAE?p3dbx-a3=J=hPML1hIop>e3eMa>veS9V!u3iCdl$8I$ zTXQYD6S>Ryyo8}Lx*+vgBe1DMEXD(EWk>R_K9!ysbI|)Szjj;W$mI3Oajq{WUM*Ot z^=+xs=W#??YrbSo zK`;;8Cc#V$XoF#&LK87OC8p!C|3$qTCYL|Fv~R_>dYfTADeQvrPuSk;AhFTz&#s+5 z3~cPy4i^#VQdb6ac-=}y?fDakN>_KV z8j3JXqIAsk*tQIitH18GL)6QNLLi`iHXZsY3br>?0d#_`*IQ>;x46X%%?1rlui$?Ao%FtJD`_M|By?a zBP`@ZSB<~r+&$-2e{P66SW~BUWPAk6`}0FFA698jTtS0HR4ED z3lbe$3hZtce94G_FO$2W0hi{Vz1|JCk7k2R9c|jXkjNB>+fpG_Ov2%|$qqsFca8m|~3U zfzEyJrM`@{e6o@Rh$RvSZUxs!91M#zHnL(m7ZD<~qP$+0%R%kMe*SzD3I($daWh>< zJo$DS-V`P|{KEJ)F7PBggf=;$YuA$E zSXXRdwFA?tXuh+fP40dSS_SlJeGl3|+^jCysU{yGjm_TdNu=t;AOt^h57nA)0Yb>> zc7c6CTDnld*ahVG?k$6bF3O@_Br<}IzimqWW_ihuPdWD~$P4jL!llBI17n1vur!Cn zZt+KNJj9l35Sr}K_4yNzc8QHDLCqh59*^kCBA{xN%M&LxRUiNmW50}Onrdj!j(@~v1LZxSI@E-+3EN38-B z6}UJAct{~E{xV%2mCCXHz?8|C+kX;FkvJgGkEz@!xga9CPC98p^JiL+h=K}Im9%Qt z(fUDW;n1!`Yk}BoAOD-xR|j(p>Hplxi*#Io{I(axW9MbQuUi(J&Ha$S52B3vBz47{ z^wAL{aJ?;H|1X5#eN0!IxZ=`UizCORH9Zo zr<9%tyEQCZyxW_b`0h@?IOYXpRdtJeE~HB zxMt68Ne=K>(*wg;;a};pc{PgMDa_5>=d)jxBvJqD3Z9kpLVz`CS2~#nZl9Fk zR%M*RT39Nl9c$??Uc;bwtSa^gqyhwp!K^q;%m?Tl6H@6%lMIMX9!8m-Lk*E&92fz` zs0C-zoM34F@x!t)!NOT#M#~e}u2xC(tdzKb9d#53TOTD$?D;>m8E| zo>gT%(|}r*;MGWdbe0I)?r|_nH4eXZN>F~0XU`SmQ>P~Z!6Rbl;T;K#xTCBY zzRVD#UKxdotudCSiQ>OrBBY1dyAM*99jwWFb|QUFfNH`(S`_0qgtH*xq(vZpD#5-p zu|RPkg+ZJd`7p>>?dxglEoD$Rk_vcTVHX8Z!6kx;A_6MPwWsE1kIgVpO`1E%h(9AN zgTE`iVBMIc{a5s6K;MTP8=O;uf*!(~xRxJt6Pa;&i&DJgCsaQ8fy;%8MmK09itPz1CcU zBtwP^)c%8S9-DP00_@n&;|gaWJk_V)BBl5k>U0^KAmFJ z$sR}>)^u3`l6!lrf#Rm*=yxUET69<`7-p{`X42?)^gJY&on^ZarC0b?n~g{Kd3K#L zmAOs2CZ{}fBm$lA6}O(hA7ecG0-Vf?G@L7pm!UhxKtDD4ah`3xIFvT}(3|UI;R4=q z@nZaOh8w1d?6^y^sV3f~ybgx<-%n$LH5PelhDH?SslpV*<^|Ww)~{9UuI%@QG)g+5 zjrQr%+(0Z1iWUEP64tHcNjql7q(YU4ZXLtJ!`nch{6n~;)Z$O9k|_cR#iN$+laf~y z|Ah-C!o%!;4lktI(f(YJ0VCc%tr;UM%VLpwK0Ul*;JD1n?|Sr0o4q!**G7-JcyDxX*f$*XFLE= z8@;rf$Vb*9CtRUWonnJM#|mb@%vOFmPt?> zM)~B;&yfSQpoC>7s7Mmwa$`YIUV>eH!hOi9zlV&3NMZ*u0YiiUQON>o^Yyb9?1ZkP zEbl_3>XJ2u51ri zwk=H5oETOy!=99&H<)1p1T!ZCFPrqku3w#GggQA(zmBkZs5zhOcA+y4pB1(Ys2 zu|1HMfGWBKiq37-SQ^9$e1mh!Us?9wRk9L)^B>`$6K_j-l8X%DwPfMUrxH`AcIW61 zuMvM@^-bgELU<)vq%@Q^y-G$BMb^DveZfnCBZ?IYAsK}BQ^^d4x41MDxRD{D8HPw> zi0+g)_`Q3z@3s4N!S>M?h8OAjB*Xeec!hA+KgP;w{4yF~f zXffl)`>_)eP4ny67-nbLQ{>KE!eF_{YA~!5l!_%C`Y$G8Hbh`*vKze3g*5}--#jp@ z2}c+sjb$vlSsRKYGJU?SvCdc&z{^y&7g9i2LAUh6pLYL=^W50TpD?*`dzmSZ`mw$l z3Cd+!T*k$yLmz^*GXGF?uX7Gmvg&Bw%S>^jfh9sO^M2_mPS+_!zi&IG5gqCPCzE4!b)x7)Z#EnDYZpn^2s zzyhF;NV_@xMN%mCqbi7sH5Z zijAG6%{M{JzdJ8v<;0q|X9`B$4^X_mZYl!I&%<5=-+Dmo&|k zS)uR67g9o37mWJYCN-sN5UmV4V4s+$uN6L3AI~ z0%4J~pqiNbjoeJ)c6etdt_F#xPTBW*){?x&r=H}>#zs5QxoBtoy_toavxez$4tNxMFsU0xBq8Wg6RYb2A;!?kBKE(UQB zLqLiI0#EkTf{kqu4jlh2@%aXiWv1G$M6tuMzaLhh`|rW>u4TAB89{AYKj|&mjBBw* zP)Z{kf(8ht?%Ujt(_^9R1n5Ow>7nW2Mgp>hsVk5&I4L=E8qivBAw*gvXVlqEq;GAw zBIYHClw0%HILCieiC05~|Ckx(

Sqr7=Ze;olLuiIy=C3e$Bh2;;6vs}ocwp0Qh1 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

W zU&<$=r3&xd0aEA|;VSd4C7!ZU5)!B8|4gmLIl2$z$ozC9{qkiLZOA7J-9iKDSr|zy z;j)%0f^6pvy*j2Qq?Bm$cXPmJZXr6&{NXqsdbpPPJ2Uhe=tzGOD97}qCaT@AsLNsppv>pX@&5AwC9vh@ zuJKxrzsX+ixRi(=1TgG)@&llS+P~Ilma?*dqHL%oj^JX@w=Y`WR-E<*lAL{7)KFE;x0kB0y&P z$cH!<3K?`BYAySu*x_$7etjZm;WjT>Bq&reny(eLTSLydYu@Jf3@XzMJ0SoZazC?b@y2Z9|3u{D#57 zfJGkj6E4C_Lj?{cyF}!uDs^GG8T<#{dL!oI+YWJZzZZF{oGmjNEk z*Ac8f&bfEP9c-NtltH2N3Xmnb6c@o#q+S1 z!^L%OLJc5|bn@Q|Qm(@(_PYFU&p}KQ75QSY6!QNZimEg12D{xI7p07_M5jJIY@KP_ zDCz&sfofoArKDF^6Q7>GGNEMncKwJj*?IVyf#?6~4B*UMGGXL8OWxgqYp3;<-Ri>_ z94>z%4mgo&lud83SMDyXBwn(aNGH6r`2w(|83g64w*l$)xP`EC_j@IUx}H?KzER?q zf)VHrzq*#Ii><44^bF1`p}XqmSijlPvE~((rN`{Ev&hJ!h*B(0N`Kk)9g41^x6?73FRK}xWV>-2oypE z(kgGOXYe|=&`0Oy?V5ZKoV1?`um&F1kCHQVNG%xgNXw}C&!TqfR+$#0{|KhU?He_P;c#nj`=RQninb->vi-TV0|2#Igx z-sboA^ni}B9%)O)`ar!W4R^hBdza4}E)V@ZFIlBN@XVL(5uNycZ@$d%Jt*}JbMzPg zQeDuk#ENQq~NOfXnm5qK<9ls)kiPNzeg)G z!y{}Lcqc9!#p43u<7w+iYH(@%a;SQe#hFz6^OE>&iLnxEE)Dw3cwWZ8SvV%l9V)m=3@5WKV-!@@4n#d95|@F2ywSlq9)6v&km5$ST3mTwbiE`UXc6@$6cIg zp6Yxp@)Y;M^H);IxkLt0wAfDR(`Tn4Ph0q1#qZ=L-!5Svar|4?-0v;f0VM?_s>kw_sn7-0-L~?;#awt`ORdB^PBpdLZuQnrVkK4oI z%&Wgv#C-Rg*Dn{DF2~D$O~p(h><1Z%*?h#WeVSiixlwT0bO&Q0gBL)Ws}Q8pyV;L? z`o)CdmSU)%(#MwjitPEUx!!*W)R;3>_*acaW(n4ZG&n~H#=JUAWc5CKz!N{RcS)v% zL6S0$wc9&_mS4n-RG6+ArRs)!Y`8e49e#Mu+kDq|aCy{K+CYO8u6NnG<;ua#d0$q5 zcge-^-QaPPAC^{W;BRx!_|!+Ij_aL4^yKG?&2cu8cQZt>9d6*TTo2s?c% zdp~t(+Y;z@lrxh)xK;T`bx5`@-mj#6wt=L9ubDV8UGinU;qu*yoywzWi&gS#>sZ z##w*QN^n{|FEd!$Yqjt|o`xCpdJZmbsruYw{P86%T4FA+sMvSR{?6;hG@Z~c)*l(^ zQU36wL9${=_to~=`9qDkUazgfOVi1pwe!ubgHeJG+lXD6s2uRKNuN@!Tx_h_2^qQ- zx*hhTR!U8hl#=e=G5A@`mbJ{g(n4)%`xl*lI|1Pi#Xe)hXVGhQL)u`?tM|`|FFeeS zG_Ky8%fK!Srla@GbKb2MI%=FQ?>_O1yxfYv%n|{ga@T3G-`n=EcGt(wQtjOhv?bbL z7#!_>hY7(Pyz=L}oBtBajXK{uga>;EZ~dtTjkCzH7tfXS{4`Hw7B}@hic=Tcel~D{dGTuuF=Gy!WyXF1 zTAlf^HFT7WS9~=)!F@5>K|6BQCYd{Y^;p_1=L~aYrtWR&RT*R9%LJt>lTq1~=DO@4 zk5<9<;PlRP=KU+WC)jHHlFZwPs#plC4GjaF3<1#xYo8|Ybm@l+kJVQcP^{c7qToUF z*wT)o{-no=m1sY|)4$K0$NLMat1@N6%?OjoW!O=|5_sv9Lt6j*qxANmvZM6e5AC?P zX-D`H-fV;P9xAFB3WrDy=P}E`&z|`@N+pi_8C%qoFwBW6O|sXPX@A+-aTMMi9mN0s5-qKVWVr6l2Fr!(Q zVv+8d8yMgRshN0k!(gOXspc=MKS@~mgmc+)yR@%nl#POhJ)UR;PBFo=f3D4~cRtH{ zh|fl+8ycL}JL7s{+7em6`gAX7^84~!uB0#Z`+(2S#x>^GI~Nb9W8Z3A=D!-Ts6$@%{0vwFjLyneVl9W~1y^UKuTJrEhFh-GR9>;(k#7d4g*NrcjoGI~tYZIekjng= zQC`n{2lJ`Q>HWSDH<{Hi0f#gW{o$dGpYM&?N$U(V@Si)F+9R+^7w<|?-|=;|pWZIo z!n&E3VK_?S-r{4-y4;eh=5VArTX)`MY$~XTAP@Y8yGNf zpq~c=oqwLDj|cC}w_VM@GkI*5>dp5?7E}nflOpsIj?Q#)bjlF`nPd1$a~b?_1Djb9r1J8t+V>1V4z@PM&7;W zcws=;@bhrqD-+CYP>jyI_VrSn z=v~YPex`xozJsXk_?`FC`Fncr1${KC^IE%h2E_V|=%4(H@?RTVxi9iP5&Z{!X?6|I zyVOTdOaIByCg`90i>J|u{+HfkIwk%0-kYa?mrnHW&hL=^xxV~C&HjkimK-7}LYFz< zbgPU4-w-J`IL4t_|Df};?Kt;TIZdcr2&UJ||n0SV$UU2xaG^p8i z9z=U(F8qcl9ZwAQ++5eF9eeLF>JR**0e*JgpW}YOgKKZ%>OFtdo*l+v_VaYZ?~1SW z25;X-m)br$=gtpn_e9U&^RWrg(SMlM)BoCJyF~x&IIh<(d-Xjn{YU-IF$)K4@1jTl zD8oHZm;RZqME?&?|MWrJymQw_2HJM#O2-I9QiP$i#4to^1RWv45G_G0a}tyR(vNe$ z3(TG1A({#%$29PR+s}z#n#mIr9e4WV#5DN17h3Nt=Nm%7SIfcxmKhehzovKfnGH60 zHoh-)hyZ2bexBH=&R*9e*KMipd_O;@{|bxhhI(+`-c5HZgoObLY5hb6Lh+0sw*j6d zB2@oAF*q%J0Y;`n8+UFK^q*N9BsTz{9TvWLeP0+{{zEh|`}epqIS)a)!97bvC(ZM( zO`=gcydP@=>~mjn^=Y1U3{0hYVD+@QH0ed}t5<)|_@^!CpPw;e&Ru=F^uKK#Q)>Lq zg!G@>d+pJ`?+0u2-|J_h|NqwP?~49C0}wFrvotAY6xQbN*|l-OG;$H1n+Wzj6VNaS z?tHj3;UyBxIG{n8p-3&GFmn&>)3H>78yg+UpHHqcxv%-AOuTH?uZTE3Tti299NZ8{ z3}zk0m&JKUbxEn=fGblHJu7py#K3sTO>NtN0Garf1~~uQP#^GvXVaEEm~h*95uB^X zCTefo#{&AKn7oaZ&2gD6MV(_{G)*(9K3=_cc4BS^`BWE;qoHJBlv~w z&;6G3?0p$0M=P#RMg!v}y5_ylp|7AVJpHG3ELo&~Zg)@rn8+gf=Q21Q_s5`Jy7cef zSAE7c`geUJqC(-|I=s#*-|LgRr)JrqA?g}WrZfg~X) zuvV7!798U-aC&AUoYG_SqA5Q=7&Qdy9z1dYHT(IFMf=@O&-pu%TRrAFb?#5T9uWuZ z>l6&QO>=pJ|9u=Gp|?Y!x<&un1cwj6i9%lz;_&d=^A3je(P3>}*O z7e)VxCZv7RfADT+^xt`oF8$xnq$c{mZ~9N$wsFZXlmMJ+vJVi=`MreA4cH$^p)q0* z4B%!WhQMJMV>i(QU&H=L{urpisuhOs+~6GDGbI1@;g6R(fS+9b{eEDgy7P-pX@N71 z7$isNzVmRKaXSxQdgI%%Gx#@cNcKT?5PM-Mat0#?HOk#Kr9#|q`Udm%3-=_Y( zfvY#DNAHI7rFWg8{Cyu^O1D%upWhY(@K!&^)Hh{rInXcGcj@}rPUy+iDIrZXkwx@R z?=Y3;No&u(ME_A=2>Q$e(LX5Rqe6e0vVA!wrs^o0a1R?Ad^mQh#9M z0mu<%3Z9(@XP<9!ZJvCY?yIdnUsM-gXD%b9<%yR2V(0oSUss>Rb@OHR%J%OqY9lv! zvx3r$&__3k1$lbfZ_42>xZu4cUk_i$pzWjfY~CT&DWsp%(0{~$^yoi&4?X(#W%%cH zc{cS&`}nl3k1W!E>b%b${rB2`8~S&7%+iVe@0yrkad6j$g>!zw@%zoorhAqq@vt7) z@0I0Vax*wa1KdaxXh2A{u0Fq@E<;eC?ttl^)8~=qgGE@T8{nenj0hm{eL*`l14T-+ zNhW-T==|6YoU=ULzMnXfyC)h{oBq)2!vO(>?=CI$!4CX$&B_ka&g#MCcG@p28`u2c z7WEsx$G^*7{kR@kgkaS@1OodtkKxmVVYb$m*{fFM2!!^9JtK=#NSku(#$F!eep+w#27FZ1^%VX zh|lMp+yE1aT|n&7dG4&^oU4ll67E=0$MMWZLj)@?sipzpcEm_SN?S2bGos=1$q|`L z|9dqmqw9z1w;>mD`>JeaZBJHK;ce#Z~ZZ;6%9r$xDy}vIa(8TXFb)F?^bDuYf zshhg+`A&UPO5vAKbFI_=XA|hSJkT|hldI3x^dEes&&u32{Zn0L&pNgfOP2vyXt$lv z|5CdIblyAPrMq5V6#a*N_EhvAy|-~3hjGjK!byC)^D^hXbbMdH3yz4Jh{bcM-G}0| zDfKyFD@QZC(!zQOehO^*F4>%I(VY<^&PhuFrqXxP|XHo*LSuZz`|$bKEZVv z=aZ(Vl^(O&>mRdyUl+RWi6epaQeO-2j=%IWi)+!K8V2j88rT6mf z5PZ(hb*6!Te6Q`^|bd#{6!!2sIJe_MDiJ<6N=ldoqF zuKzUybdRG>^FclwRFWI~X#k1tA3F8-_oubCi`sq-etnYLrK7vBRF`OwoLwIs{V%MM zMr}GPr-Pl)|JV+?UHYefm`+=_ZE-*JAJus&?=|{&(xyfF@73)z5dHu4=s$YLT<^8K z@8^9-nKX?==VqNeYAAnCS|L8)*|r6D)TKc)YS^grVjB=-a@$#bPtowcZZnS7x8>Lw zJcD|L0}ACcd-h)XsI1O&r%oB+fEeWK`wFl&XbuZ)liRfS6*DeOOucvF^XS!SAVy`KQM-MU{=y}AbF)cY37+3KD8G%NLqCeEew4IY&Kqq?|zx9+p!UxEIEdU5)# z>EA!sQr>*b+n@~5zbnu6(Tatov2gxf>BoyWZThV<x4J`C?2PeGhf z9WWTVI{SN?0|kb?{-WuH*7x+ok`dJX7ZbVBs0IqW`rzc8&+vr?+X_kp6w0qk47v z%qso6w6nUcnbD;ci2gr!`Va5N`Ef_|-z)obhkKcWKfiYGQo7SL#JI(v=j+?cW38hs z)!{Vt>a|zY5!arr_a1tAo(56gYx%78g { useManageTransactionsPending(); @@ -34,6 +35,7 @@ const App = (): ReactElement => { {!isSyncingInProgress && !isProjectAdminMode && } + ); }; diff --git a/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx b/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx index 4630456d05..54c6faef51 100644 --- a/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx +++ b/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx @@ -25,16 +25,40 @@ const motionAnimationProps: AnimationProps = { const ModalOnboarding: FC = () => { const { isConnected } = useAccount(); const { data: isUserTOSAccepted } = useUserTOS(); - const { setIsOnboardingDone, isOnboardingDone } = useOnboardingStore(state => ({ + const { + setIsOnboardingDone, + isOnboardingDone, + isOnboardingCompleted, + lastSeenStep, + setLastSeenStep, + isOnboardingModalOpen, + setIsOnboardingModalOpen, + } = useOnboardingStore(state => ({ isOnboardingDone: state.data.isOnboardingDone, + isOnboardingCompleted: state.data.isOnboardingCompleted, + lastSeenStep: state.data.lastSeenStep, setIsOnboardingDone: state.setIsOnboardingDone, + setIsOnboardingCompleted: state.setIsOnboardingCompleted, + setLastSeenStep: state.setLastSeenStep, + setIsOnboardingModalOpen: state.setIsOnboardingModalOpen, + isOnboardingModalOpen: state.data.isOnboardingModalOpen, })); const { isDesktop } = useMediaQuery(); - const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(lastSeenStep - 1); const [isUserTOSAcceptedInitial] = useState(isUserTOSAccepted); const stepsToUse = useOnboardingSteps(isUserTOSAcceptedInitial); + useEffect(() => { + if (isOnboardingCompleted) return; + setLastSeenStep(currentStepIndex + 1); + }, [currentStepIndex, isOnboardingCompleted]); + + useEffect(() => { + if (isOnboardingDone) return; + setCurrentStepIndex(lastSeenStep - 1); + }, [isOnboardingDone]); + useEffect(() => { if (isUserTOSAccepted !== undefined && !isUserTOSAccepted) { setIsOnboardingDone(false); @@ -51,7 +75,8 @@ const ModalOnboarding: FC = () => { if (!isUserTOSAccepted) { return; } - setIsOnboardingDone(true); + setIsOnboardingModalOpen(false); + // setIsOnboardingDone(true); }, [setIsOnboardingDone, isUserTOSAccepted]); const [touchStart, setTouchStart] = useState(null); @@ -147,6 +172,11 @@ const ModalOnboarding: FC = () => { (isConnected && !isUserTOSAccepted) || (isConnected && !!isUserTOSAccepted && !isOnboardingDone); + useEffect(() => { + if (!isConnected) return; + // setIsOnboardingModalOpen(true); + }, [isConnected, isUserTOSAccepted]); + return ( { } isCloseButtonDisabled={!isUserTOSAccepted} - isOpen={isModalOpen} + isOpen={isOnboardingModalOpen} isOverflowOnClickDisabled={!isUserTOSAccepted} onClick={handleModalEdgeClick} onClosePanel={onOnboardingExit} diff --git a/client/src/components/shared/OnboardingStepper/OnboardingStepper.module.scss b/client/src/components/shared/OnboardingStepper/OnboardingStepper.module.scss new file mode 100644 index 0000000000..c75c519559 --- /dev/null +++ b/client/src/components/shared/OnboardingStepper/OnboardingStepper.module.scss @@ -0,0 +1,77 @@ +.root, +.wrapper { + height: 5.6rem; + width: 5.6rem; + position: fixed; + right: 4.8rem; + bottom: 4.8rem; + z-index: 5; + border-radius: 100%; + cursor: pointer; +} + +.wrapper { + overflow: hidden; + &:hover { + transform: scale(1.2); + transition: all 0.4s; + } +} + +.slideImg { + position: absolute; + z-index: 1; + height: 5rem; + top: 1.65rem; + left: 50%; + transform: translate(-50%, 0); +} + +.backgroundCircleSvg, +.progressLinesSvg { + top: 0; + left: 0; + position: absolute; +} + +.progressLinesSvg { + z-index: 2; +} + +.stepNumber { + z-index: 5; + left: 50%; + transform: translate(-50%, 0); + top: 2.4rem; + position: absolute; + + path { + stroke: transparent !important; + } +} + +.backgroundCircle { + fill: $color-octant-grey2; +} + +.progressLine { + stroke-width: 0.2rem; + stroke: $color-white; + stroke-linejoin: round; + fill: transparent; + stroke-linecap: round; + stroke-linejoin: round; + z-index: 4; + + &.isHighlighted { + stroke: $color-octant-green; + } +} + +.tooltipWrapper { + position: absolute; + width: 5.6rem; + height: 5.6rem; + bottom: 4.8rem !important; + right: 4.8rem !important; +} diff --git a/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx new file mode 100644 index 0000000000..14cfdfd5d5 --- /dev/null +++ b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx @@ -0,0 +1,69 @@ +import cx from 'classnames'; +import React, { FC, useMemo, useState } from 'react'; +import useOnboardingStore from 'store/onboarding/store'; + +import { motion } from 'framer-motion'; +import styles from './OnboardingStepper.module.scss'; +import useOnboardingSteps from 'hooks/helpers/useOnboardingSteps'; +import useUserTOS from 'hooks/queries/useUserTOS'; +import Svg from 'components/ui/Svg'; +import { four, one, three, two } from 'svg/onboardingStepper'; +import Tooltip from 'components/ui/Tooltip'; + +const OnboardingStepper: FC<{ className?: string }> = ({ className }) => { + const { setIsOnboardingModalOpen, lastSeenStep } = useOnboardingStore(state => ({ + isOnboardingModalOpen: state.data.isOnboardingModalOpen, + setIsOnboardingModalOpen: state.setIsOnboardingModalOpen, + lastSeenStep: state.data.lastSeenStep, + })); + + const { data: isUserTOSAccepted } = useUserTOS(); + const [isUserTOSAcceptedInitial] = useState(isUserTOSAccepted); + const stepsToUse = useOnboardingSteps(isUserTOSAcceptedInitial); + const cxcy = 28; + const viewBox = '0 0 56 56'; + const numberOfSteps = stepsToUse.length; + + const svgNumber = useMemo(() => { + if (lastSeenStep === 1) return one; + if (lastSeenStep === 2) return two; + if (lastSeenStep === 3) return three; + return four; + }, [lastSeenStep]); + + return ( +
+ +
+ + + + + + setIsOnboardingModalOpen(true)} + > + {[...Array(numberOfSteps).keys()].map(i => ( + + ))} + +
+
+
+ ); +}; + +export default OnboardingStepper; diff --git a/client/src/components/shared/OnboardingStepper/index.tsx b/client/src/components/shared/OnboardingStepper/index.tsx new file mode 100644 index 0000000000..882ff6e981 --- /dev/null +++ b/client/src/components/shared/OnboardingStepper/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './OnboardingStepper'; diff --git a/client/src/constants/localStorageKeys.ts b/client/src/constants/localStorageKeys.ts index 7e9d1071bc..c2b46be375 100644 --- a/client/src/constants/localStorageKeys.ts +++ b/client/src/constants/localStorageKeys.ts @@ -11,6 +11,11 @@ export const ALLOCATION_REWARDS_FOR_PROJECTS = getLocalStorageKey( const onboardingPrefix = 'onboarding'; export const IS_ONBOARDING_DONE = getLocalStorageKey(onboardingPrefix, 'isOnboardingDone'); +export const IS_ONBOARDING_COMPLETED = getLocalStorageKey( + onboardingPrefix, + 'isOnboardingCompleted', +); +export const LAST_SEEN_STEP = getLocalStorageKey(onboardingPrefix, 'lastSeenStep'); const settingsPrefix = 'settings'; export const IS_ONBOARDING_ALWAYS_VISIBLE = getLocalStorageKey( diff --git a/client/src/store/onboarding/store.ts b/client/src/store/onboarding/store.ts index 3e46a8fbc2..0055cff39e 100644 --- a/client/src/store/onboarding/store.ts +++ b/client/src/store/onboarding/store.ts @@ -1,10 +1,17 @@ -import { IS_ONBOARDING_DONE } from 'constants/localStorageKeys'; +import { + IS_ONBOARDING_COMPLETED, + IS_ONBOARDING_DONE, + LAST_SEEN_STEP, +} from 'constants/localStorageKeys'; import { getStoreWithMeta } from 'store/utils/getStoreWithMeta'; import { OnboardingMethods, OnboardingData } from './types'; export const initialState: OnboardingData = { isOnboardingDone: false, + isOnboardingCompleted: false, + lastSeenStep: 1, + isOnboardingModalOpen: false, }; export default getStoreWithMeta({ @@ -13,17 +20,31 @@ export default getStoreWithMeta({ // eslint-disable-next-line @typescript-eslint/naming-convention setIsOnboardingDone: payload => { localStorage.setItem(IS_ONBOARDING_DONE, JSON.stringify(payload)); - set({ data: { isOnboardingDone: payload } }); + set(state => ({ data: { ...state.data, isOnboardingDone: payload } })); + }, + setIsOnboardingCompleted: payload => { + localStorage.setItem(IS_ONBOARDING_COMPLETED, JSON.stringify(payload)); + set(state => ({ data: { ...state.data, isOnboardingCompleted: payload } })); + }, + setLastSeenStep: payload => { + localStorage.setItem(LAST_SEEN_STEP, JSON.stringify(payload)); + set(state => ({ data: { ...state.data, lastSeenStep: payload } })); + }, + setIsOnboardingModalOpen: payload => { + set(state => ({ data: { ...state.data, isOnboardingModalOpen: payload } })); }, setValuesFromLocalStorage: () => - set({ + set(state => ({ data: { isOnboardingDone: localStorage.getItem(IS_ONBOARDING_DONE) === 'true', + isOnboardingCompleted: localStorage.getItem(IS_ONBOARDING_DONE) === 'true', + lastSeenStep: parseInt(localStorage.getItem(LAST_SEEN_STEP) || '1', 10), + isOnboardingModalOpen: state.data.isOnboardingModalOpen, }, meta: { isInitialized: true, }, - }), + })), }), initialState, }); diff --git a/client/src/store/onboarding/types.ts b/client/src/store/onboarding/types.ts index 34267ebfa1..1c27501db7 100644 --- a/client/src/store/onboarding/types.ts +++ b/client/src/store/onboarding/types.ts @@ -1,9 +1,15 @@ export interface OnboardingData { isOnboardingDone: boolean; + isOnboardingCompleted: boolean; + lastSeenStep: number; + isOnboardingModalOpen: boolean; } export interface OnboardingMethods { reset: () => void; setIsOnboardingDone: (payload: OnboardingData['isOnboardingDone']) => void; + setIsOnboardingCompleted: (payload: OnboardingData['isOnboardingCompleted']) => void; + setLastSeenStep: (payload: OnboardingData['lastSeenStep']) => void; + setIsOnboardingModalOpen: (payload: OnboardingData['isOnboardingModalOpen']) => void; setValuesFromLocalStorage: () => void; } diff --git a/client/src/svg/onboardingStepper.ts b/client/src/svg/onboardingStepper.ts new file mode 100644 index 0000000000..a07f0ed1db --- /dev/null +++ b/client/src/svg/onboardingStepper.ts @@ -0,0 +1,25 @@ +import { SvgImageConfig } from 'components/ui/Svg/types'; + +export const one: SvgImageConfig = { + markup: + '', + viewBox: '0 0 6 12', +}; + +export const two: SvgImageConfig = { + markup: + '', + viewBox: '0 0 9 12', +}; + +export const three: SvgImageConfig = { + markup: + '', + viewBox: '0 0 10 13', +}; + +export const four: SvgImageConfig = { + markup: + '', + viewBox: '0 0 10 12', +}; From e6ff8f008b6c5512aa367898b7b162d54769d2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 18 Apr 2024 11:40:09 +0200 Subject: [PATCH 02/24] oct-1396: onboarding stepper + e2e --- client/cypress/e2e/onboarding.cy.ts | 83 ++++++++++++++++- client/src/App.tsx | 14 ++- .../ModalOnboarding/ModalOnboarding.tsx | 91 +++++++++++-------- .../OnboardingStepper.module.scss | 33 ++++--- .../OnboardingStepper/OnboardingStepper.tsx | 73 ++++++++++----- client/src/constants/localStorageKeys.ts | 4 +- client/src/locales/en/translation.json | 5 + .../src/services/localStorageService.test.ts | 17 ++-- client/src/services/localStorageService.ts | 32 +++++-- client/src/store/onboarding/store.test.ts | 81 ++++++++++++++++- client/src/store/onboarding/store.ts | 28 +++--- client/src/store/onboarding/types.ts | 8 +- client/src/store/settings/store.ts | 1 + 13 files changed, 352 insertions(+), 118 deletions(-) diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts index 3dec91c7b6..3b180f3f53 100644 --- a/client/cypress/e2e/onboarding.cy.ts +++ b/client/cypress/e2e/onboarding.cy.ts @@ -1,10 +1,19 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import chaiColors from 'chai-colors'; + import { visitWithLoader, navigateWithCheck, mockCoinPricesServer } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { getStepsDecisionWindowClosed } from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + getStepsDecisionWindowClosed, + getStepsDecisionWindowOpen, +} from 'src/hooks/helpers/useOnboardingSteps/steps'; import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; +chai.use(chaiColors); + const connectWallet = ( isTOSAccepted: boolean, shouldVisit = true, @@ -26,7 +35,6 @@ const connectWallet = ( const beforeSetup = () => { mockCoinPricesServer(); - cy.clearLocalStorage(); cy.setupMetamask(); window.innerWidth = Cypress.config().viewportWidth; window.innerHeight = Cypress.config().viewportHeight; @@ -219,13 +227,14 @@ const checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px = (isTOSAccepted: }); }; -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { before(() => { beforeSetup(); }); beforeEach(() => { + cy.clearLocalStorage(); connectWallet(true); }); @@ -254,7 +263,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => navigateWithCheck(ROOT_ROUTES.settings.absolute); cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').check().should('be.checked'); cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); }); it('renders only once when "Always show Allocate onboarding" option is not checked', () => { @@ -296,6 +305,72 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => connectWallet(true, false, true); cy.get('[data-test=ModalOnboarding]').should('not.exist'); }); + + it('Onboarding stepper is visible after closing onboarding modal without going to the last step', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=OnboardingStepper]').should('be.visible'); + }); + + it('Onboarding stepper opens onboarding modal', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); + cy.get('[data-test=OnboardingStepper]').click(); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it(`Onboarding stepper is not visible if "${IS_ONBOARDING_DONE}" is set to "true"`, () => { + localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); + cy.get('[data-test=OnboardingStepper]').should('not.exist'); + }); + + if (isDesktop) { + it(`Onboarding stepper has tooltip`, () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=OnboardingStepper]').trigger('mouseover'); + cy.get('[data-test=OnboardingStepper__Tooltip__content]').should('be.visible'); + cy.get('[data-test=OnboardingStepper__Tooltip__content]') + .invoke('text') + .should('eq', 'Reopen onboarding'); + }); + } + + it('Onboarding stepper has right amount of steps and highlights correct amount of passed steps', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + + cy.get(`[data-test*=OnboardingStepper__circle]`).should( + 'have.length', + getStepsDecisionWindowOpen('2', '16 Jan').length, + ); + + for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 0 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(1); + cy.get('[data-test=ModalOnboarding__Button]').click(); + for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 1 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(2); + cy.get('[data-test=ModalOnboarding__Button]').click(); + for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 2 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(3); + cy.get('[data-test=ModalOnboarding__Button]').click(); + + cy.get('[data-test=OnboardingStepper]').should('not.exist'); + }); }); }); diff --git a/client/src/App.tsx b/client/src/App.tsx index e6dbe75a2d..c9edde6a0e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,7 +1,10 @@ +import { AnimatePresence } from 'framer-motion'; import React, { ReactElement, useState, Fragment } from 'react'; +import { useAccount } from 'wagmi'; import AppLoader from 'components/shared/AppLoader'; import ModalOnboarding from 'components/shared/ModalOnboarding/ModalOnboarding'; +import OnboardingStepper from 'components/shared/OnboardingStepper'; import useAppConnectManager from 'hooks/helpers/useAppConnectManager'; import useAppIsLoading from 'hooks/helpers/useAppIsLoading'; import useAppPopulateState from 'hooks/helpers/useAppPopulateState'; @@ -9,11 +12,11 @@ import useCypressHelpers from 'hooks/helpers/useCypressHelpers'; import useIsProjectAdminMode from 'hooks/helpers/useIsProjectAdminMode'; import useManageTransactionsPending from 'hooks/helpers/useManageTransactionsPending'; import RootRoutes from 'routes/RootRoutes/RootRoutes'; +import useOnboardingStore from 'store/onboarding/store'; import 'react-toastify/dist/ReactToastify.css'; import 'styles/index.scss'; import 'i18n'; -import OnboardingStepper from 'components/shared/OnboardingStepper'; const App = (): ReactElement => { useManageTransactionsPending(); @@ -23,6 +26,11 @@ const App = (): ReactElement => { const { isSyncingInProgress } = useAppConnectManager(isFlushRequired, setIsFlushRequired); const isLoading = useAppIsLoading(isFlushRequired); const isProjectAdminMode = useIsProjectAdminMode(); + const { isConnected } = useAccount(); + const { isOnboardingDone, isOnboardingModalOpen } = useOnboardingStore(state => ({ + isOnboardingDone: state.data.isOnboardingDone, + isOnboardingModalOpen: state.data.isOnboardingModalOpen, + })); // useCypressHelpers needs to be called after all the initial sets done above. const { isFetching: isFetchingCypressHelpers } = useCypressHelpers(); @@ -35,7 +43,9 @@ const App = (): ReactElement => { {!isSyncingInProgress && !isProjectAdminMode && } - + + {isConnected && !isOnboardingDone && !isOnboardingModalOpen && } + ); }; diff --git a/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx b/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx index 54c6faef51..6684080c68 100644 --- a/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx +++ b/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx @@ -12,6 +12,7 @@ import useMediaQuery from 'hooks/helpers/useMediaQuery'; import useOnboardingSteps from 'hooks/helpers/useOnboardingSteps'; import useUserTOS from 'hooks/queries/useUserTOS'; import useOnboardingStore from 'store/onboarding/store'; +import useSettingsStore from 'store/settings/store'; import styles from './ModalOnboarding.module.scss'; @@ -25,23 +26,28 @@ const motionAnimationProps: AnimationProps = { const ModalOnboarding: FC = () => { const { isConnected } = useAccount(); const { data: isUserTOSAccepted } = useUserTOS(); + const { setIsOnboardingDone, isOnboardingDone, - isOnboardingCompleted, + hasOnboardingBeenClosed, lastSeenStep, setLastSeenStep, isOnboardingModalOpen, setIsOnboardingModalOpen, + setHasOnboardingBeenClosed, } = useOnboardingStore(state => ({ + hasOnboardingBeenClosed: state.data.hasOnboardingBeenClosed, isOnboardingDone: state.data.isOnboardingDone, - isOnboardingCompleted: state.data.isOnboardingCompleted, + isOnboardingModalOpen: state.data.isOnboardingModalOpen, lastSeenStep: state.data.lastSeenStep, + setHasOnboardingBeenClosed: state.setHasOnboardingBeenClosed, setIsOnboardingDone: state.setIsOnboardingDone, - setIsOnboardingCompleted: state.setIsOnboardingCompleted, - setLastSeenStep: state.setLastSeenStep, setIsOnboardingModalOpen: state.setIsOnboardingModalOpen, - isOnboardingModalOpen: state.data.isOnboardingModalOpen, + setLastSeenStep: state.setLastSeenStep, + })); + const { isAllocateOnboardingAlwaysVisible } = useSettingsStore(state => ({ + isAllocateOnboardingAlwaysVisible: state.data.isAllocateOnboardingAlwaysVisible, })); const { isDesktop } = useMediaQuery(); const [currentStepIndex, setCurrentStepIndex] = useState(lastSeenStep - 1); @@ -49,34 +55,16 @@ const ModalOnboarding: FC = () => { const stepsToUse = useOnboardingSteps(isUserTOSAcceptedInitial); - useEffect(() => { - if (isOnboardingCompleted) return; - setLastSeenStep(currentStepIndex + 1); - }, [currentStepIndex, isOnboardingCompleted]); - - useEffect(() => { - if (isOnboardingDone) return; - setCurrentStepIndex(lastSeenStep - 1); - }, [isOnboardingDone]); - - useEffect(() => { - if (isUserTOSAccepted !== undefined && !isUserTOSAccepted) { - setIsOnboardingDone(false); - } - - if (!isUserTOSAcceptedInitial && isUserTOSAccepted) { - setCurrentStepIndex(prev => prev + 1); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setIsOnboardingDone, isUserTOSAccepted]); - const currentStep = stepsToUse.length > 0 ? stepsToUse[currentStepIndex] : null; const onOnboardingExit = useCallback(() => { if (!isUserTOSAccepted) { return; } setIsOnboardingModalOpen(false); - // setIsOnboardingDone(true); + if (!hasOnboardingBeenClosed) { + setHasOnboardingBeenClosed(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [setIsOnboardingDone, isUserTOSAccepted]); const [touchStart, setTouchStart] = useState(null); @@ -145,7 +133,7 @@ const ModalOnboarding: FC = () => { }; useEffect(() => { - if (isOnboardingDone) { + if (!isConnected && !isUserTOSAccepted) { return; } @@ -159,23 +147,50 @@ const ModalOnboarding: FC = () => { } }; - if (isUserTOSAccepted) { - window.addEventListener('keydown', listener); - } + window.addEventListener('keydown', listener); return () => { window.removeEventListener('keydown', listener); }; - }, [currentStepIndex, stepsToUse, isOnboardingDone, isUserTOSAccepted]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected, currentStepIndex, stepsToUse, isOnboardingDone, isUserTOSAccepted]); + + useEffect(() => { + if ( + isConnected && + (isAllocateOnboardingAlwaysVisible || !isUserTOSAccepted || !hasOnboardingBeenClosed) + ) { + setIsOnboardingModalOpen(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected]); - const isModalOpen = - (isConnected && !isUserTOSAccepted) || - (isConnected && !!isUserTOSAccepted && !isOnboardingDone); + useEffect(() => { + if (isOnboardingDone) { + return; + } + const stepNumber = currentStepIndex + 1; + setLastSeenStep(stepNumber); + if (stepNumber === stepsToUse.length) { + setIsOnboardingDone(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentStepIndex, isOnboardingDone]); useEffect(() => { - if (!isConnected) return; - // setIsOnboardingModalOpen(true); - }, [isConnected, isUserTOSAccepted]); + if (isOnboardingDone) { + return; + } + setCurrentStepIndex(lastSeenStep - 1); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!isUserTOSAcceptedInitial && isUserTOSAccepted) { + setCurrentStepIndex(prev => prev + 1); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isUserTOSAccepted]); return ( = ({ className }) => { +import styles from './OnboardingStepper.module.scss'; + +const OnboardingStepper = (): ReactNode => { + const { t } = useTranslation('translation', { keyPrefix: 'components.shared.onboardingStepper' }); const { setIsOnboardingModalOpen, lastSeenStep } = useOnboardingStore(state => ({ isOnboardingModalOpen: state.data.isOnboardingModalOpen, - setIsOnboardingModalOpen: state.setIsOnboardingModalOpen, lastSeenStep: state.data.lastSeenStep, + setIsOnboardingModalOpen: state.setIsOnboardingModalOpen, })); const { data: isUserTOSAccepted } = useUserTOS(); @@ -25,44 +29,71 @@ const OnboardingStepper: FC<{ className?: string }> = ({ className }) => { const numberOfSteps = stepsToUse.length; const svgNumber = useMemo(() => { - if (lastSeenStep === 1) return one; - if (lastSeenStep === 2) return two; - if (lastSeenStep === 3) return three; + if (lastSeenStep === 1) { + return one; + } + if (lastSeenStep === 2) { + return two; + } + if (lastSeenStep === 3) { + return three; + } return four; }, [lastSeenStep]); return ( -
- + setIsOnboardingModalOpen(true)} + whileHover={{ scale: 1.1 }} + > +
- - + + - + setIsOnboardingModalOpen(true)} > {[...Array(numberOfSteps).keys()].map(i => ( ))}
-
+ ); }; diff --git a/client/src/constants/localStorageKeys.ts b/client/src/constants/localStorageKeys.ts index c2b46be375..e8a7060b85 100644 --- a/client/src/constants/localStorageKeys.ts +++ b/client/src/constants/localStorageKeys.ts @@ -11,9 +11,9 @@ export const ALLOCATION_REWARDS_FOR_PROJECTS = getLocalStorageKey( const onboardingPrefix = 'onboarding'; export const IS_ONBOARDING_DONE = getLocalStorageKey(onboardingPrefix, 'isOnboardingDone'); -export const IS_ONBOARDING_COMPLETED = getLocalStorageKey( +export const HAS_ONBOARDING_BEEN_CLOSED = getLocalStorageKey( onboardingPrefix, - 'isOnboardingCompleted', + 'hasOnboardingBeenClosed', ); export const LAST_SEEN_STEP = getLocalStorageKey(onboardingPrefix, 'lastSeenStep'); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index ad255ec3c8..bfdaf89c32 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -54,6 +54,11 @@ "waitingForConfirmation": "Waiting for confirmation" }, "components": { + "shared": { + "onboardingStepper": { + "reopenOnboarding": "Reopen onboarding" + } + }, "settings": { "patronMode": { "enablePatronMode": "Enable patron mode", diff --git a/client/src/services/localStorageService.test.ts b/client/src/services/localStorageService.test.ts index 6f4b52827a..a69aaf6aa0 100644 --- a/client/src/services/localStorageService.test.ts +++ b/client/src/services/localStorageService.test.ts @@ -2,9 +2,11 @@ import { ALLOCATION_ITEMS_KEY, ALLOCATION_REWARDS_FOR_PROJECTS, DISPLAY_CURRENCY, + HAS_ONBOARDING_BEEN_CLOSED, IS_CRYPTO_MAIN_VALUE_DISPLAY, IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE, + LAST_SEEN_STEP, WAS_ADD_FAVOURITES_ALREADY_CLOSED_TIP, WAS_CONNECT_WALLET_ALREADY_CLOSED_TIP, WAS_LOCK_GLM_ALREADY_CLOSED_TIP, @@ -41,24 +43,21 @@ describe('LocalStorageService', () => { }); it('should validate isOnboardingDone', () => { - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'true'); localStorage.setItem(IS_ONBOARDING_DONE, 'not-a-boolean'); localStorageService.init(); expect(localStorage.getItem(IS_ONBOARDING_DONE)).toBe('false'); }); - it('should validate isOnboardingDone', () => { - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + it('should validate hasOnboardingBeenClosed', () => { + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'not-a-boolean'); localStorageService.init(); - expect(localStorage.getItem(IS_ONBOARDING_DONE)).toBe('true'); + expect(localStorage.getItem(HAS_ONBOARDING_BEEN_CLOSED)).toBe('false'); }); - it('should validate isOnboardingDone', () => { - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'false'); + it('should validate lastSeenStep', () => { + localStorage.setItem(LAST_SEEN_STEP, 'not-a-number'); localStorageService.init(); - expect(localStorage.getItem(IS_ONBOARDING_DONE)).toBe('false'); + expect(localStorage.getItem(LAST_SEEN_STEP)).toBe('0'); }); it('should validate displayCurrency', () => { diff --git a/client/src/services/localStorageService.ts b/client/src/services/localStorageService.ts index 7a5df1383f..d57319df32 100644 --- a/client/src/services/localStorageService.ts +++ b/client/src/services/localStorageService.ts @@ -11,6 +11,8 @@ import { WAS_REWARDS_ALREADY_CLOSED_TIP, WAS_WITHDRAW_ALREADY_CLOSED_TIP, ALLOCATION_REWARDS_FOR_PROJECTS, + HAS_ONBOARDING_BEEN_CLOSED, + LAST_SEEN_STEP, } from 'constants/localStorageKeys'; import { initialState as settingsStoreInitialState } from 'store/settings/store'; import { initialState as tipsStoreInitialState } from 'store/tips/store'; @@ -52,6 +54,19 @@ const LocalStorageService = () => { } }; + const validateNumber = (localStorageKey: string): void => { + let value; + try { + value = parseFloat(JSON.parse(localStorage.getItem(localStorageKey) || '')); + } catch (e) { + value = ''; + } + + if (typeof value !== 'number') { + localStorage.setItem(localStorageKey, '0'); + } + }; + const validateAllocationItems = (): void => { const allocationItems = JSON.parse(localStorage.getItem(ALLOCATION_ITEMS_KEY) || 'null'); if (!Array.isArray(allocationItems)) { @@ -69,16 +84,15 @@ const LocalStorageService = () => { }; const validateIsOnboardingDone = (): void => { - const isOnboardingAlwaysVisible = localStorage.getItem(IS_ONBOARDING_ALWAYS_VISIBLE); + validateBoolean(IS_ONBOARDING_DONE); + }; - if (isOnboardingAlwaysVisible === 'true') { - localStorage.setItem( - IS_ONBOARDING_DONE, - JSON.stringify(settingsStoreInitialState.isAllocateOnboardingAlwaysVisible), - ); - } + const validateHasOnboardingBeenClosed = (): void => { + validateBoolean(HAS_ONBOARDING_BEEN_CLOSED); + }; - validateBoolean(IS_ONBOARDING_DONE); + const validateLastSeenStep = (): void => { + validateNumber(LAST_SEEN_STEP); }; const validateDisplayCurrency = (): void => { @@ -138,6 +152,8 @@ const LocalStorageService = () => { validateWasRewardsAlreadyClosed(); validateWasWithdrawAlreadyClosed(); validateRewardsForProjects(); + validateHasOnboardingBeenClosed(); + validateLastSeenStep(); }; return { diff --git a/client/src/store/onboarding/store.test.ts b/client/src/store/onboarding/store.test.ts index 2c854e7723..8f31b1c017 100644 --- a/client/src/store/onboarding/store.test.ts +++ b/client/src/store/onboarding/store.test.ts @@ -1,4 +1,8 @@ -import { IS_ONBOARDING_DONE } from 'constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_DONE, + LAST_SEEN_STEP, +} from 'constants/localStorageKeys'; import useOnboardingStore from './store'; @@ -10,12 +14,30 @@ describe('useOnboardingStore', () => { }); it('should reset the state', () => { - const { setIsOnboardingDone, reset } = useOnboardingStore.getState(); + const { + setIsOnboardingDone, + setHasOnboardingBeenClosed, + setIsOnboardingModalOpen, + setLastSeenStep, + reset, + } = useOnboardingStore.getState(); setIsOnboardingDone(true); + setHasOnboardingBeenClosed(true); + setLastSeenStep(3); + setIsOnboardingModalOpen(true); + expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(true); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(true); + expect(useOnboardingStore.getState().data.isOnboardingModalOpen).toEqual(true); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(3); + reset(); + expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(false); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(false); + expect(useOnboardingStore.getState().data.isOnboardingModalOpen).toEqual(false); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(1); }); it('should set isOnboardingDone to true in localStorage and state', () => { @@ -26,6 +48,22 @@ describe('useOnboardingStore', () => { expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(true); }); + it('should set hasOnboardingBeenClosed to true in localStorage and state', () => { + const { setHasOnboardingBeenClosed } = useOnboardingStore.getState(); + + setHasOnboardingBeenClosed(true); + expect(localStorage.getItem(HAS_ONBOARDING_BEEN_CLOSED)).toEqual('true'); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(true); + }); + + it('should set lastSeenStep to 3 in localStorage and state', () => { + const { setLastSeenStep } = useOnboardingStore.getState(); + + setLastSeenStep(3); + expect(localStorage.getItem(LAST_SEEN_STEP)).toEqual('3'); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(3); + }); + it('should set isOnboardingDone to false in localStorage and state', () => { const { setIsOnboardingDone } = useOnboardingStore.getState(); @@ -34,6 +72,22 @@ describe('useOnboardingStore', () => { expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(false); }); + it('should set hasOnboardingBeenClosed to false in localStorage and state', () => { + const { setHasOnboardingBeenClosed } = useOnboardingStore.getState(); + + setHasOnboardingBeenClosed(false); + expect(localStorage.getItem(HAS_ONBOARDING_BEEN_CLOSED)).toEqual('false'); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(false); + }); + + it('should set lastSeenStep to 2 in localStorage and state', () => { + const { setLastSeenStep } = useOnboardingStore.getState(); + + setLastSeenStep(2); + expect(localStorage.getItem(LAST_SEEN_STEP)).toEqual('2'); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(2); + }); + it('should get isOnboardingDone from localStorage and set in state', () => { const { setValuesFromLocalStorage } = useOnboardingStore.getState(); @@ -44,10 +98,33 @@ describe('useOnboardingStore', () => { expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(true); }); + it('should get hasOnboardingBeenClosed from localStorage and set in state', () => { + const { setValuesFromLocalStorage } = useOnboardingStore.getState(); + + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); + expect(useOnboardingStore.getState().meta.isInitialized).toEqual(false); + setValuesFromLocalStorage(); + expect(useOnboardingStore.getState().meta.isInitialized).toEqual(true); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(true); + }); + + it('should get lastSeenStep from localStorage and set in state', () => { + const { setValuesFromLocalStorage } = useOnboardingStore.getState(); + + localStorage.setItem(LAST_SEEN_STEP, '3'); + expect(useOnboardingStore.getState().meta.isInitialized).toEqual(false); + setValuesFromLocalStorage(); + expect(useOnboardingStore.getState().meta.isInitialized).toEqual(true); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(3); + }); + it('should return default state when there is no value in localStorage', () => { const { setValuesFromLocalStorage } = useOnboardingStore.getState(); setValuesFromLocalStorage(); expect(useOnboardingStore.getState().data.isOnboardingDone).toEqual(false); + expect(useOnboardingStore.getState().data.hasOnboardingBeenClosed).toEqual(false); + expect(useOnboardingStore.getState().data.isOnboardingModalOpen).toEqual(false); + expect(useOnboardingStore.getState().data.lastSeenStep).toEqual(1); }); }); diff --git a/client/src/store/onboarding/store.ts b/client/src/store/onboarding/store.ts index 0055cff39e..cc3c7d0c65 100644 --- a/client/src/store/onboarding/store.ts +++ b/client/src/store/onboarding/store.ts @@ -1,5 +1,5 @@ import { - IS_ONBOARDING_COMPLETED, + HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE, LAST_SEEN_STEP, } from 'constants/localStorageKeys'; @@ -8,38 +8,44 @@ import { getStoreWithMeta } from 'store/utils/getStoreWithMeta'; import { OnboardingMethods, OnboardingData } from './types'; export const initialState: OnboardingData = { + hasOnboardingBeenClosed: false, isOnboardingDone: false, - isOnboardingCompleted: false, - lastSeenStep: 1, isOnboardingModalOpen: false, + lastSeenStep: 1, }; export default getStoreWithMeta({ getStoreMethods: set => ({ reset: () => set({ data: initialState }), // eslint-disable-next-line @typescript-eslint/naming-convention + setHasOnboardingBeenClosed: payload => { + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, JSON.stringify(payload)); + set(state => ({ data: { ...state.data, hasOnboardingBeenClosed: payload } })); + }, + // eslint-disable-next-line @typescript-eslint/naming-convention setIsOnboardingDone: payload => { localStorage.setItem(IS_ONBOARDING_DONE, JSON.stringify(payload)); set(state => ({ data: { ...state.data, isOnboardingDone: payload } })); }, - setIsOnboardingCompleted: payload => { - localStorage.setItem(IS_ONBOARDING_COMPLETED, JSON.stringify(payload)); - set(state => ({ data: { ...state.data, isOnboardingCompleted: payload } })); + // eslint-disable-next-line @typescript-eslint/naming-convention + setIsOnboardingModalOpen: payload => { + set(state => ({ data: { ...state.data, isOnboardingModalOpen: payload } })); }, + // eslint-disable-next-line @typescript-eslint/naming-convention setLastSeenStep: payload => { localStorage.setItem(LAST_SEEN_STEP, JSON.stringify(payload)); set(state => ({ data: { ...state.data, lastSeenStep: payload } })); }, - setIsOnboardingModalOpen: payload => { - set(state => ({ data: { ...state.data, isOnboardingModalOpen: payload } })); - }, setValuesFromLocalStorage: () => set(state => ({ data: { + hasOnboardingBeenClosed: localStorage.getItem(HAS_ONBOARDING_BEEN_CLOSED) === 'true', isOnboardingDone: localStorage.getItem(IS_ONBOARDING_DONE) === 'true', - isOnboardingCompleted: localStorage.getItem(IS_ONBOARDING_DONE) === 'true', - lastSeenStep: parseInt(localStorage.getItem(LAST_SEEN_STEP) || '1', 10), isOnboardingModalOpen: state.data.isOnboardingModalOpen, + lastSeenStep: + localStorage.getItem(IS_ONBOARDING_DONE) === 'true' + ? 1 + : parseInt(localStorage.getItem(LAST_SEEN_STEP) || '1', 10) || 1, }, meta: { isInitialized: true, diff --git a/client/src/store/onboarding/types.ts b/client/src/store/onboarding/types.ts index 1c27501db7..5afa5104c9 100644 --- a/client/src/store/onboarding/types.ts +++ b/client/src/store/onboarding/types.ts @@ -1,15 +1,15 @@ export interface OnboardingData { + hasOnboardingBeenClosed: boolean; isOnboardingDone: boolean; - isOnboardingCompleted: boolean; - lastSeenStep: number; isOnboardingModalOpen: boolean; + lastSeenStep: number; } export interface OnboardingMethods { reset: () => void; + setHasOnboardingBeenClosed: (payload: OnboardingData['hasOnboardingBeenClosed']) => void; setIsOnboardingDone: (payload: OnboardingData['isOnboardingDone']) => void; - setIsOnboardingCompleted: (payload: OnboardingData['isOnboardingCompleted']) => void; - setLastSeenStep: (payload: OnboardingData['lastSeenStep']) => void; setIsOnboardingModalOpen: (payload: OnboardingData['isOnboardingModalOpen']) => void; + setLastSeenStep: (payload: OnboardingData['lastSeenStep']) => void; setValuesFromLocalStorage: () => void; } diff --git a/client/src/store/settings/store.ts b/client/src/store/settings/store.ts index 4af349b745..205bfcd7cf 100644 --- a/client/src/store/settings/store.ts +++ b/client/src/store/settings/store.ts @@ -24,6 +24,7 @@ export default getStoreWithMeta({ set(state => ({ data: { ...state.data, areOctantTipsAlwaysVisible: payload } })); }, + // eslint-disable-next-line @typescript-eslint/naming-convention setDisplayCurrency: payload => { localStorage.setItem(DISPLAY_CURRENCY, JSON.stringify(payload)); set(state => ({ data: { ...state.data, displayCurrency: payload } })); From c0d70915cee6296081e9968959b58be4f46a571d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 18 Apr 2024 11:47:00 +0200 Subject: [PATCH 03/24] oct-1396: slide.png to slide.webp --- client/public/images/slide.png | Bin 58540 -> 0 bytes client/public/images/slide.webp | Bin 0 -> 16462 bytes .../OnboardingStepper/OnboardingStepper.tsx | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 client/public/images/slide.png create mode 100644 client/public/images/slide.webp diff --git a/client/public/images/slide.png b/client/public/images/slide.png deleted file mode 100644 index c2cfa6e81b044698453749095d9ac76517858d06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58540 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

Sqr7=Ze;olLuiIy=C3e$Bh2;;6vs}ocwp0Qh1 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

W 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

4uG6`f71I@Xd5kED$EDbjrnfNTT(=;wUTgOCRp6 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 literal 0 HcmV?d00001 diff --git a/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx index f1b0deda64..a6ae601eae 100644 --- a/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx +++ b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx @@ -61,7 +61,7 @@ const OnboardingStepper = (): ReactNode => { variant="small" >

- + From 53bbd5564caa8cd6b1e6f39b09769d8311c94764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 18 Apr 2024 14:38:27 +0200 Subject: [PATCH 04/24] oct-1396: adjustment for mobile devices --- .../shared/OnboardingStepper/OnboardingStepper.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx index a6ae601eae..15ff9cebd7 100644 --- a/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx +++ b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import Img from 'components/ui/Img'; import Svg from 'components/ui/Svg'; import Tooltip from 'components/ui/Tooltip'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; import useOnboardingSteps from 'hooks/helpers/useOnboardingSteps'; import useUserTOS from 'hooks/queries/useUserTOS'; import useOnboardingStore from 'store/onboarding/store'; @@ -21,6 +22,7 @@ const OnboardingStepper = (): ReactNode => { setIsOnboardingModalOpen: state.setIsOnboardingModalOpen, })); + const { isDesktop } = useMediaQuery(); const { data: isUserTOSAccepted } = useUserTOS(); const [isUserTOSAcceptedInitial] = useState(isUserTOSAccepted); const stepsToUse = useOnboardingSteps(isUserTOSAcceptedInitial); @@ -44,11 +46,11 @@ const OnboardingStepper = (): ReactNode => { return ( setIsOnboardingModalOpen(true)} whileHover={{ scale: 1.1 }} > From 3818c5ab9dd7b4457c948f8f80a6c83efec7e46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 18 Apr 2024 14:45:33 +0200 Subject: [PATCH 05/24] oct-1396: adjustment for mobile devices - higher --- .../shared/OnboardingStepper/OnboardingStepper.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx index 15ff9cebd7..98226fbaf6 100644 --- a/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx +++ b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx @@ -46,11 +46,11 @@ const OnboardingStepper = (): ReactNode => { return ( setIsOnboardingModalOpen(true)} whileHover={{ scale: 1.1 }} > From 221ef3982d7e299c84eb42aead7299ea36cbabc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Tue, 23 Apr 2024 16:29:57 +0200 Subject: [PATCH 06/24] oct-1396: e2e: hasOnboardingBeenClosed=true --- client/cypress/e2e/_2makePendingSnapshot.cy.ts | 7 ++++++- client/cypress/e2e/allocationItemWindowClosed.cy.ts | 2 ++ client/cypress/e2e/allocationItemWindowOpen.cy.ts | 2 ++ client/cypress/e2e/allocationRewardsBox.cy.ts | 8 +++++++- client/cypress/e2e/earn.cy.ts | 7 ++++++- client/cypress/e2e/layout.cy.ts | 7 ++++++- client/cypress/e2e/metrics.cy.ts | 7 ++++++- client/cypress/e2e/patronMode.cy.ts | 8 +++++++- client/cypress/e2e/project.cy.ts | 4 +++- client/cypress/e2e/projects.cy.ts | 4 +++- client/cypress/e2e/projectsArchive.cy.ts | 7 ++++++- client/cypress/e2e/rewardsCalculator.cy.ts | 7 ++++++- client/cypress/e2e/settings.cy.ts | 2 ++ client/cypress/utils/moveTime.ts | 3 +++ 14 files changed, 65 insertions(+), 10 deletions(-) diff --git a/client/cypress/e2e/_2makePendingSnapshot.cy.ts b/client/cypress/e2e/_2makePendingSnapshot.cy.ts index a1b96c0ee4..2c69c31c12 100644 --- a/client/cypress/e2e/_2makePendingSnapshot.cy.ts +++ b/client/cypress/e2e/_2makePendingSnapshot.cy.ts @@ -2,7 +2,11 @@ import axios from 'axios'; import { mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; import { QUERY_KEYS } from 'src/api/queryKeys'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import env from 'src/env'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; @@ -22,6 +26,7 @@ describe('Make pending snapshot', () => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.playground.absolute); }); diff --git a/client/cypress/e2e/allocationItemWindowClosed.cy.ts b/client/cypress/e2e/allocationItemWindowClosed.cy.ts index f5a42a70ea..22e12b7589 100644 --- a/client/cypress/e2e/allocationItemWindowClosed.cy.ts +++ b/client/cypress/e2e/allocationItemWindowClosed.cy.ts @@ -9,6 +9,7 @@ import viewports from 'cypress/utils/viewports'; import { QUERY_KEYS } from 'src/api/queryKeys'; import { ALLOCATION_ITEMS_KEY, + HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE, } from 'src/constants/localStorageKeys'; @@ -56,6 +57,7 @@ describe('allocation (allocation window closed)', () => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); localStorage.setItem(ALLOCATION_ITEMS_KEY, '[]'); visitWithLoader(ROOT_ROUTES.projects.absolute); diff --git a/client/cypress/e2e/allocationItemWindowOpen.cy.ts b/client/cypress/e2e/allocationItemWindowOpen.cy.ts index 58a9360a9b..75bbb5d18e 100644 --- a/client/cypress/e2e/allocationItemWindowOpen.cy.ts +++ b/client/cypress/e2e/allocationItemWindowOpen.cy.ts @@ -12,6 +12,7 @@ import viewports from 'cypress/utils/viewports'; import { QUERY_KEYS } from 'src/api/queryKeys'; import { ALLOCATION_ITEMS_KEY, + HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE, } from 'src/constants/localStorageKeys'; @@ -63,6 +64,7 @@ describe('allocation (allocation window open)', () => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); localStorage.setItem(ALLOCATION_ITEMS_KEY, '[]'); visitWithLoader(ROOT_ROUTES.projects.absolute); connectWallet(true, false); diff --git a/client/cypress/e2e/allocationRewardsBox.cy.ts b/client/cypress/e2e/allocationRewardsBox.cy.ts index 1259263a5e..e23bc56041 100644 --- a/client/cypress/e2e/allocationRewardsBox.cy.ts +++ b/client/cypress/e2e/allocationRewardsBox.cy.ts @@ -3,7 +3,11 @@ import chaiColors from 'chai-colors'; import { visitWithLoader, mockCoinPricesServer, connectWallet } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; chai.use(chaiColors); @@ -16,6 +20,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.allocation.absolute); }); @@ -108,6 +113,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.allocation.absolute); cy.intercept('GET', '/rewards/budget/*/epoch/*', { body: { budget: '10000000000' } }); connectWallet(true, false); diff --git a/client/cypress/e2e/earn.cy.ts b/client/cypress/e2e/earn.cy.ts index c3bb07dc3a..9a4e207148 100644 --- a/client/cypress/e2e/earn.cy.ts +++ b/client/cypress/e2e/earn.cy.ts @@ -1,7 +1,11 @@ import { visitWithLoader, mockCoinPricesServer } from 'cypress/utils/e2e'; import { moveTime } from 'cypress/utils/moveTime'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; @@ -30,6 +34,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.earn.absolute); }); diff --git a/client/cypress/e2e/layout.cy.ts b/client/cypress/e2e/layout.cy.ts index 9ea9954d24..57f7fbccda 100644 --- a/client/cypress/e2e/layout.cy.ts +++ b/client/cypress/e2e/layout.cy.ts @@ -1,6 +1,10 @@ import { navigateWithCheck, mockCoinPricesServer } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { navigationTabs } from 'src/constants/navigationTabs/navigationTabs'; import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; @@ -16,6 +20,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => cy.disconnectMetamaskWalletFromAllDapps(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); cy.visit(ROOT.absolute); }); diff --git a/client/cypress/e2e/metrics.cy.ts b/client/cypress/e2e/metrics.cy.ts index 829fbfbf1c..411dac359e 100644 --- a/client/cypress/e2e/metrics.cy.ts +++ b/client/cypress/e2e/metrics.cy.ts @@ -1,6 +1,10 @@ import { mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { @@ -18,6 +22,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.metrics.absolute); }); diff --git a/client/cypress/e2e/patronMode.cy.ts b/client/cypress/e2e/patronMode.cy.ts index b5f3455c47..ce8445e8ff 100644 --- a/client/cypress/e2e/patronMode.cy.ts +++ b/client/cypress/e2e/patronMode.cy.ts @@ -1,6 +1,10 @@ import { connectWallet, mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { @@ -18,6 +22,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.settings.absolute); connectWallet(true, false); }); @@ -347,6 +352,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes beforeEach(() => { localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.settings.absolute); connectWallet(true, true); }); diff --git a/client/cypress/e2e/project.cy.ts b/client/cypress/e2e/project.cy.ts index e811fb70aa..406e02521c 100644 --- a/client/cypress/e2e/project.cy.ts +++ b/client/cypress/e2e/project.cy.ts @@ -1,7 +1,7 @@ import { connectWallet, mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; import { getNamesOfProjects } from 'cypress/utils/projects'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; @@ -40,6 +40,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.projects.absolute); cy.get('[data-test^=ProjectItemSkeleton').should('not.exist'); @@ -157,6 +158,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.projects.absolute); connectWallet(true, true); cy.get('[data-test^=ProjectItemSkeleton').should('not.exist'); diff --git a/client/cypress/e2e/projects.cy.ts b/client/cypress/e2e/projects.cy.ts index 9d096bcdf2..62bddadd78 100644 --- a/client/cypress/e2e/projects.cy.ts +++ b/client/cypress/e2e/projects.cy.ts @@ -4,7 +4,7 @@ import chaiColors from 'chai-colors'; import { connectWallet, mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; import { getNamesOfProjects } from 'cypress/utils/projects'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; import getMilestones from 'src/constants/milestones'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; @@ -115,6 +115,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.projects.absolute); cy.get('[data-test^=ProjectItemSkeleton').should('not.exist'); @@ -213,6 +214,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.projects.absolute); connectWallet(true, true); cy.get('[data-test^=ProjectItemSkeleton').should('not.exist'); diff --git a/client/cypress/e2e/projectsArchive.cy.ts b/client/cypress/e2e/projectsArchive.cy.ts index 237fcbd624..fb9328c82e 100644 --- a/client/cypress/e2e/projectsArchive.cy.ts +++ b/client/cypress/e2e/projectsArchive.cy.ts @@ -2,7 +2,11 @@ import { checkLocationWithLoader, visitWithLoader } from 'cypress/utils/e2e'; import { moveTime } from 'cypress/utils/moveTime'; import viewports from 'cypress/utils/viewports'; import { QUERY_KEYS } from 'src/api/queryKeys'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; let wasEpochMoved = false; @@ -12,6 +16,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => beforeEach(() => { localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.projects.absolute); }); diff --git a/client/cypress/e2e/rewardsCalculator.cy.ts b/client/cypress/e2e/rewardsCalculator.cy.ts index 8ebb2c48b3..79f526a6b0 100644 --- a/client/cypress/e2e/rewardsCalculator.cy.ts +++ b/client/cypress/e2e/rewardsCalculator.cy.ts @@ -1,6 +1,10 @@ import { ETH_USD, GLM_USD, mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { + HAS_ONBOARDING_BEEN_CLOSED, + IS_ONBOARDING_ALWAYS_VISIBLE, + IS_ONBOARDING_DONE, +} from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import getFormattedEthValue from 'src/utils/getFormattedEthValue'; import getValueFiatToDisplay from 'src/utils/getValueFiatToDisplay'; @@ -12,6 +16,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.earn.absolute); }); diff --git a/client/cypress/e2e/settings.cy.ts b/client/cypress/e2e/settings.cy.ts index 087b77329e..1217806db3 100644 --- a/client/cypress/e2e/settings.cy.ts +++ b/client/cypress/e2e/settings.cy.ts @@ -4,6 +4,7 @@ import { FIAT_CURRENCIES_SYMBOLS, DISPLAY_CURRENCIES } from 'src/constants/curre import { ARE_OCTANT_TIPS_ALWAYS_VISIBLE, DISPLAY_CURRENCY, + HAS_ONBOARDING_BEEN_CLOSED, IS_CRYPTO_MAIN_VALUE_DISPLAY, IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE, @@ -17,6 +18,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); visitWithLoader(ROOT_ROUTES.settings.absolute); }); diff --git a/client/cypress/utils/moveTime.ts b/client/cypress/utils/moveTime.ts index e8ca436043..f2a3a78b01 100644 --- a/client/cypress/utils/moveTime.ts +++ b/client/cypress/utils/moveTime.ts @@ -1,6 +1,7 @@ import { QUERY_KEYS } from 'src/api/queryKeys'; import { ALLOCATION_ITEMS_KEY, + HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE, } from 'src/constants/localStorageKeys'; @@ -15,6 +16,7 @@ export const setupAndMoveToPlayground = (): Chainable => { mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); localStorage.setItem(ALLOCATION_ITEMS_KEY, '[]'); return visitWithLoader(ROOT_ROUTES.playground.absolute); }; @@ -105,6 +107,7 @@ export const moveTime = ( mockCoinPricesServer(); localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); localStorage.setItem(ALLOCATION_ITEMS_KEY, '[]'); visitWithLoader(ROOT_ROUTES.playground.absolute); } From bfb2e83b473b1d40bd77eaf1bc5dd7eaebc910ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Wed, 24 Apr 2024 16:31:56 +0200 Subject: [PATCH 07/24] oct-1396: e2e AW open --- client/cypress/e2e/onboarding.cy.ts | 37 +++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts index 3b180f3f53..e00d3938b5 100644 --- a/client/cypress/e2e/onboarding.cy.ts +++ b/client/cypress/e2e/onboarding.cy.ts @@ -4,13 +4,12 @@ import chaiColors from 'chai-colors'; import { visitWithLoader, navigateWithCheck, mockCoinPricesServer } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; import { IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; -import { - getStepsDecisionWindowClosed, - getStepsDecisionWindowOpen, -} from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { getStepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; +import { moveTime, setupAndMoveToPlayground } from 'cypress/utils/moveTime'; +import { QUERY_KEYS } from 'src/api/queryKeys'; chai.use(chaiColors); @@ -227,6 +226,30 @@ const checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px = (isTOSAccepted: }); }; +describe('move time', () => { + before(() => { + /** + * Global Metamask setup done by Synpress is not always done. + * Since Synpress needs to have valid provider to fetch the data from contracts, + * setupMetamask is required in each test suite. + */ + cy.setupMetamask(); + }); + + it('allocation window is open, when it is not, move time', () => { + setupAndMoveToPlayground(); + + cy.window().then(async win => { + moveTime(win, 'nextEpochDecisionWindowOpen').then(() => { + const isDecisionWindowOpenAfter = win.clientReactQuery.getQueryData( + QUERY_KEYS.isDecisionWindowOpen, + ); + expect(isDecisionWindowOpenAfter).to.be.true; + }); + }); + }); +}); + Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { before(() => { @@ -239,12 +262,12 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('user is able to click through entire onboarding flow', () => { - for (let i = 1; i < getStepsDecisionWindowClosed('2', '16 Jan').length - 1; i++) { + for (let i = 1; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { checkProgressStepperSlimIsCurrentAndClickNext(i); } cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(getStepsDecisionWindowClosed('2', '16 Jan').length - 1) + .eq(getStepsDecisionWindowOpen('2', '16 Jan').length - 1) .click(); cy.get('[data-test=ModalOnboarding__Button]').click(); cy.get('[data-test=ModalOnboarding]').should('not.exist'); @@ -397,7 +420,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => }); it('user is not able to click through entire onboarding flow', () => { - for (let i = 1; i < getStepsDecisionWindowClosed('2', '16 Jan').length; i++) { + for (let i = 1; i < getStepsDecisionWindowOpen('2', '16 Jan').length; i++) { checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); } }); From 764c96b1e85aa47d8128c47a920aa9fe608b8306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 25 Apr 2024 12:46:15 +0200 Subject: [PATCH 08/24] oct-1396: split onboarding e2e --- client/cypress/e2e/onboarding.cy.ts | 478 ------------------ .../cypress/e2e/onboardingTOSAccepted.cy.ts | 168 ++++++ .../e2e/onboardingTOSNotAccepted.cy.ts | 98 ++++ client/cypress/utils/onboarding.ts | 227 +++++++++ .../ModalOnboarding/ModalOnboarding.tsx | 2 +- 5 files changed, 494 insertions(+), 479 deletions(-) delete mode 100644 client/cypress/e2e/onboarding.cy.ts create mode 100644 client/cypress/e2e/onboardingTOSAccepted.cy.ts create mode 100644 client/cypress/e2e/onboardingTOSNotAccepted.cy.ts create mode 100644 client/cypress/utils/onboarding.ts diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts deleted file mode 100644 index e00d3938b5..0000000000 --- a/client/cypress/e2e/onboarding.cy.ts +++ /dev/null @@ -1,478 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import chaiColors from 'chai-colors'; - -import { visitWithLoader, navigateWithCheck, mockCoinPricesServer } from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; -import { getStepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; -import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -import Chainable = Cypress.Chainable; -import { moveTime, setupAndMoveToPlayground } from 'cypress/utils/moveTime'; -import { QUERY_KEYS } from 'src/api/queryKeys'; - -chai.use(chaiColors); - -const connectWallet = ( - isTOSAccepted: boolean, - shouldVisit = true, - shouldReload = false, -): Chainable => { - cy.intercept('GET', '/user/*/tos', { body: { accepted: isTOSAccepted } }); - cy.disconnectMetamaskWalletFromAllDapps(); - if (shouldVisit) { - visitWithLoader(ROOT.absolute, ROOT_ROUTES.projects.absolute); - } - if (shouldReload) { - cy.reload(); - } - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); - cy.switchToMetamaskNotification(); - return cy.acceptMetamaskAccess(); -}; - -const beforeSetup = () => { - mockCoinPricesServer(); - cy.setupMetamask(); - window.innerWidth = Cypress.config().viewportWidth; - window.innerHeight = Cypress.config().viewportHeight; -}; - -const checkCurrentElement = (el: number, isCurrent: boolean): Chainable => { - return cy - .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(el) - .invoke('attr', 'data-iscurrent') - .should('eq', `${isCurrent}`); -}; - -const checkProgressStepperSlimIsCurrentAndClickNext = (index, isCurrent = true): Chainable => { - checkCurrentElement(index - 1, isCurrent); - return cy - .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(index) - .click({ force: true }); -}; - -const checkChangeStepsWithArrowKeys = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { el: 1, key: 'ArrowRight' }, - { el: 2, key: 'ArrowRight' }, - // { el: 3, key: 'ArrowRight' }, - // { el: 3, key: 'ArrowRight' }, - // { el: 2, key: 'ArrowLeft' }, - { el: 1, key: 'ArrowLeft' }, - { el: 0, key: 'ArrowLeft' }, - { el: 0, key: 'ArrowLeft' }, - ].forEach(({ key, el }) => { - cy.get('body').trigger('keydown', { key }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -const checkChangeStepsByClickingEdgeOfTheScreenUpTo25px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - cy.get('[data-test=ModalOnboarding]').then(element => { - const leftEdgeX = element.offsetParent().offset()?.left as number; - const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; - - [ - { clientX: rightEdgeX - 25, el: 1 }, - { clientX: rightEdgeX - 10, el: 2 }, - // { clientX: rightEdgeX - 5, el: 3 }, - // rightEdgeX === browser right frame - // { clientX: rightEdgeX - 1, el: 3 }, - // { clientX: leftEdgeX + 25, el: 2 }, - { clientX: leftEdgeX + 10, el: 1 }, - { clientX: leftEdgeX + 5, el: 0 }, - { clientX: leftEdgeX, el: 0 }, - ].forEach(({ clientX, el }) => { - cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); - }); -}; - -const checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - cy.get('[data-test=ModalOnboarding]').then(element => { - const leftEdgeX = element.offsetParent().offset()?.left as number; - const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; - - [ - { clientX: rightEdgeX - 25, el: 1 }, - { clientX: rightEdgeX - 26, el: 1 }, - { clientX: leftEdgeX + 26, el: 1 }, - { clientX: leftEdgeX + 25, el: 0 }, - ].forEach(({ clientX, el }) => { - cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); - }); -}; - -const checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 2, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 2, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - // { - // el: 3, - // touchMoveClientX: window.innerWidth / 2 - 5, - // touchStartClientX: window.innerWidth / 2, - // }, - { - el: 2, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { - cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { - touches: [{ clientX: touchStartClientX }], - }); - cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { - touches: [{ clientX: touchMoveClientX }], - }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -const checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px = (isTOSAccepted: boolean) => { - checkCurrentElement(0, true); - - [ - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 5, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 - 4, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 1, - touchMoveClientX: window.innerWidth / 2 + 4, - touchStartClientX: window.innerWidth / 2, - }, - { - el: 0, - touchMoveClientX: window.innerWidth / 2 + 5, - touchStartClientX: window.innerWidth / 2, - }, - ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { - cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { - touches: [{ clientX: touchStartClientX }], - }); - cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { - touches: [{ clientX: touchMoveClientX }], - }); - checkCurrentElement(el, isTOSAccepted || el === 0); - - if (!isTOSAccepted) { - checkCurrentElement(0, true); - } - }); -}; - -describe('move time', () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - it('allocation window is open, when it is not, move time', () => { - setupAndMoveToPlayground(); - - cy.window().then(async win => { - moveTime(win, 'nextEpochDecisionWindowOpen').then(() => { - const isDecisionWindowOpenAfter = win.clientReactQuery.getQueryData( - QUERY_KEYS.isDecisionWindowOpen, - ); - expect(isDecisionWindowOpenAfter).to.be.true; - }); - }); - }); -}); - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { - describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - beforeSetup(); - }); - - beforeEach(() => { - cy.clearLocalStorage(); - connectWallet(true); - }); - - it('user is able to click through entire onboarding flow', () => { - for (let i = 1; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i); - } - - cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(getStepsDecisionWindowOpen('2', '16 Jan').length - 1) - .click(); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); - }); - - it('user is able to close the modal by clicking button in the top-right', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); - }); - - it('renders every time page is refreshed when "Always show Allocate onboarding" option is checked', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').check().should('be.checked'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); - }); - - it('renders only once when "Always show Allocate onboarding" option is not checked', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('not.be.checked'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - - it('user can change steps with arrow keys (left, right)', () => { - checkChangeStepsWithArrowKeys(true); - }); - - it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(true); - }); - - it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(true); - }); - - it('user can change steps by swiping on screen (difference more than or equal 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(true); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); - }); - - it('user is able to close the onboarding, and after disconnecting & connecting, onboarding does not show up again', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - connectWallet(true, false, true); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - - it('Onboarding stepper is visible after closing onboarding modal without going to the last step', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=OnboardingStepper]').should('be.visible'); - }); - - it('Onboarding stepper opens onboarding modal', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); - cy.get('[data-test=OnboardingStepper]').click(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it(`Onboarding stepper is not visible if "${IS_ONBOARDING_DONE}" is set to "true"`, () => { - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); - cy.get('[data-test=OnboardingStepper]').should('not.exist'); - }); - - if (isDesktop) { - it(`Onboarding stepper has tooltip`, () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=OnboardingStepper]').trigger('mouseover'); - cy.get('[data-test=OnboardingStepper__Tooltip__content]').should('be.visible'); - cy.get('[data-test=OnboardingStepper__Tooltip__content]') - .invoke('text') - .should('eq', 'Reopen onboarding'); - }); - } - - it('Onboarding stepper has right amount of steps and highlights correct amount of passed steps', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - - cy.get(`[data-test*=OnboardingStepper__circle]`).should( - 'have.length', - getStepsDecisionWindowOpen('2', '16 Jan').length, - ); - - for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 0 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(1); - cy.get('[data-test=ModalOnboarding__Button]').click(); - for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 1 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(2); - cy.get('[data-test=ModalOnboarding__Button]').click(); - for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 2 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(3); - cy.get('[data-test=ModalOnboarding__Button]').click(); - - cy.get('[data-test=OnboardingStepper]').should('not.exist'); - }); - }); -}); - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`onboarding (TOS not accepted): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - beforeSetup(); - }); - - beforeEach(() => { - cy.intercept( - { - method: 'POST', - url: '/user/*/tos', - }, - { body: { accepted: true }, statusCode: 200 }, - ); - connectWallet(false); - }); - - it('onboarding TOS step should be first and active', () => { - checkCurrentElement(0, true); - cy.get('[data-test=ModalOnboardingTOS]').should('be.visible'); - }); - - it('user is not able to click through entire onboarding flow', () => { - for (let i = 1; i < getStepsDecisionWindowOpen('2', '16 Jan').length; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); - } - }); - - it('user is not able to close the modal by clicking button in the top-right', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click({ force: true }); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('renders every time page is refreshed', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('user cannot change steps with arrow keys (left, right)', () => { - checkChangeStepsWithArrowKeys(false); - }); - - it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(false); - }); - - it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(false); - }); - - it('user cannot change steps by swiping on screen (difference more than or equal 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(false); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(false); - }); - - it('TOS acceptance changes onboarding step to next step', () => { - checkCurrentElement(0, true); - cy.get('[data-test=TOS_InputCheckbox]').check(); - cy.switchToMetamaskNotification(); - cy.confirmMetamaskSignatureRequest(); - checkCurrentElement(1, true); - }); - - it('TOS acceptance allows the user to close the modal by clicking button in the top-right', () => { - checkCurrentElement(0, true); - cy.get('[data-test=TOS_InputCheckbox]').check(); - cy.switchToMetamaskNotification(); - cy.confirmMetamaskSignatureRequest(); - checkCurrentElement(1, true); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - }); -}); diff --git a/client/cypress/e2e/onboardingTOSAccepted.cy.ts b/client/cypress/e2e/onboardingTOSAccepted.cy.ts new file mode 100644 index 0000000000..e73556e8fd --- /dev/null +++ b/client/cypress/e2e/onboardingTOSAccepted.cy.ts @@ -0,0 +1,168 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import chaiColors from 'chai-colors'; + +import { navigateWithCheck } from 'cypress/utils/e2e'; +import { + beforeSetup, + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px, + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px, + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px, + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px, + checkChangeStepsWithArrowKeys, + checkProgressStepperSlimIsCurrentAndClickNext, + connectWallet, +} from 'cypress/utils/onboarding'; +import viewports from 'cypress/utils/viewports'; +import { HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import { getStepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; + +chai.use(chaiColors); + +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { + describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { + before(() => { + beforeSetup(); + }); + + beforeEach(() => { + cy.clearLocalStorage(); + connectWallet(true); + }); + + it('user is able to click through entire onboarding flow', () => { + for (let i = 1; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { + checkProgressStepperSlimIsCurrentAndClickNext(i); + } + + cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(getStepsDecisionWindowOpen('2', '16 Jan').length - 1) + .click(); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); + }); + + it('user is able to close the modal by clicking button in the top-right', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); + }); + + it('renders every time page is refreshed when "Always show Allocate onboarding" option is checked', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + navigateWithCheck(ROOT_ROUTES.settings.absolute); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').check().should('be.checked'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); + }); + + it('renders only once when "Always show Allocate onboarding" option is not checked', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + navigateWithCheck(ROOT_ROUTES.settings.absolute); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('not.be.checked'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + + it('user can change steps with arrow keys (left, right)', () => { + checkChangeStepsWithArrowKeys(true); + }); + + it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(true); + }); + + it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(true); + }); + + it('user can change steps by swiping on screen (difference more than or equal 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(true); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); + }); + + it('user is able to close the onboarding, and after disconnecting & connecting, onboarding does not show up again', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + connectWallet(true, false, true); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + + it('Onboarding stepper is visible after closing onboarding modal without going to the last step', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=OnboardingStepper]').should('be.visible'); + }); + + it('Onboarding stepper opens onboarding modal', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); + cy.get('[data-test=OnboardingStepper]').click(); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it(`Onboarding stepper is not visible if "${IS_ONBOARDING_DONE}" is set to "true"`, () => { + localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); + cy.get('[data-test=OnboardingStepper]').should('not.exist'); + }); + + if (isDesktop) { + it(`Onboarding stepper has tooltip`, () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=OnboardingStepper]').trigger('mouseover'); + cy.get('[data-test=OnboardingStepper__Tooltip__content]').should('be.visible'); + cy.get('[data-test=OnboardingStepper__Tooltip__content]') + .invoke('text') + .should('eq', 'Reopen onboarding'); + }); + } + + it('Onboarding stepper has right amount of steps and highlights correct amount of passed steps', () => { + cy.get('[data-test=ModalOnboarding__Button]').click(); + + cy.get(`[data-test*=OnboardingStepper__circle]`).should( + 'have.length', + getStepsDecisionWindowOpen('2', '16 Jan').length, + ); + + for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 0 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(1); + cy.get('[data-test=ModalOnboarding__Button]').click(); + for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 1 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(2); + cy.get('[data-test=ModalOnboarding__Button]').click(); + for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 2 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(3); + cy.get('[data-test=ModalOnboarding__Button]').click(); + + cy.get('[data-test=OnboardingStepper]').should('not.exist'); + }); + }); +}); diff --git a/client/cypress/e2e/onboardingTOSNotAccepted.cy.ts b/client/cypress/e2e/onboardingTOSNotAccepted.cy.ts new file mode 100644 index 0000000000..4b6ad23529 --- /dev/null +++ b/client/cypress/e2e/onboardingTOSNotAccepted.cy.ts @@ -0,0 +1,98 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import chaiColors from 'chai-colors'; + +import { + beforeSetup, + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px, + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px, + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px, + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px, + checkChangeStepsWithArrowKeys, + checkCurrentElement, + checkProgressStepperSlimIsCurrentAndClickNext, + connectWallet, +} from 'cypress/utils/onboarding'; +import viewports from 'cypress/utils/viewports'; +import { getStepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; + +chai.use(chaiColors); + +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { + describe(`onboarding (TOS not accepted): ${device}`, { viewportHeight, viewportWidth }, () => { + before(() => { + beforeSetup(); + }); + + beforeEach(() => { + cy.intercept( + { + method: 'POST', + url: '/user/*/tos', + }, + { body: { accepted: true }, statusCode: 200 }, + ); + connectWallet(false); + }); + + it('onboarding TOS step should be first and active', () => { + checkCurrentElement(0, true); + cy.get('[data-test=ModalOnboardingTOS]').should('be.visible'); + }); + + it('user is not able to click through entire onboarding flow', () => { + for (let i = 1; i < getStepsDecisionWindowOpen('2', '16 Jan').length; i++) { + checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); + } + }); + + it('user is not able to close the modal by clicking button in the top-right', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click({ force: true }); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it('renders every time page is refreshed', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + }); + + it('user cannot change steps with arrow keys (left, right)', () => { + checkChangeStepsWithArrowKeys(false); + }); + + it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(false); + }); + + it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { + checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(false); + }); + + it('user cannot change steps by swiping on screen (difference more than or equal 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(false); + }); + + it('user cannot change steps by swiping on screen (difference less than 5px)', () => { + checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(false); + }); + + it('TOS acceptance changes onboarding step to next step', () => { + checkCurrentElement(0, true); + cy.get('[data-test=TOS_InputCheckbox]').check(); + cy.switchToMetamaskNotification(); + cy.confirmMetamaskSignatureRequest(); + checkCurrentElement(1, true); + }); + + it('TOS acceptance allows the user to close the modal by clicking button in the top-right', () => { + checkCurrentElement(0, true); + cy.get('[data-test=TOS_InputCheckbox]').check(); + cy.switchToMetamaskNotification(); + cy.confirmMetamaskSignatureRequest(); + checkCurrentElement(1, true); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); + }); +}); diff --git a/client/cypress/utils/onboarding.ts b/client/cypress/utils/onboarding.ts new file mode 100644 index 0000000000..c06444816a --- /dev/null +++ b/client/cypress/utils/onboarding.ts @@ -0,0 +1,227 @@ +import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; + +import { mockCoinPricesServer, visitWithLoader } from './e2e'; + +import Chainable = Cypress.Chainable; + +export const connectWallet = ( + isTOSAccepted: boolean, + shouldVisit = true, + shouldReload = false, +): Chainable => { + cy.intercept('GET', '/user/*/tos', { body: { accepted: isTOSAccepted } }); + cy.disconnectMetamaskWalletFromAllDapps(); + if (shouldVisit) { + visitWithLoader(ROOT.absolute, ROOT_ROUTES.projects.absolute); + } + if (shouldReload) { + cy.reload(); + } + cy.get('[data-test=MainLayout__Button--connect]').click(); + cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); + cy.switchToMetamaskNotification(); + return cy.acceptMetamaskAccess(); +}; + +export const beforeSetup = (): void => { + mockCoinPricesServer(); + cy.setupMetamask(); + window.innerWidth = Cypress.config().viewportWidth; + window.innerHeight = Cypress.config().viewportHeight; +}; + +export const checkCurrentElement = (el: number, isCurrent: boolean): Chainable => { + return cy + .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(el) + .invoke('attr', 'data-iscurrent') + .should('eq', `${isCurrent}`); +}; + +export const checkProgressStepperSlimIsCurrentAndClickNext = ( + index: number, + isCurrent = true, +): Chainable => { + checkCurrentElement(index - 1, isCurrent); + return cy + .get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(index) + .click({ force: true }); +}; + +export const checkChangeStepsWithArrowKeys = (isTOSAccepted: boolean): void => { + checkCurrentElement(0, true); + + [ + { el: 1, key: 'ArrowRight' }, + { el: 2, key: 'ArrowRight' }, + // { el: 3, key: 'ArrowRight' }, + // { el: 3, key: 'ArrowRight' }, + // { el: 2, key: 'ArrowLeft' }, + { el: 1, key: 'ArrowLeft' }, + { el: 0, key: 'ArrowLeft' }, + { el: 0, key: 'ArrowLeft' }, + ].forEach(({ key, el }) => { + cy.get('body').trigger('keydown', { key }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; + +export const checkChangeStepsByClickingEdgeOfTheScreenUpTo25px = (isTOSAccepted: boolean): void => { + checkCurrentElement(0, true); + + cy.get('[data-test=ModalOnboarding]').then(element => { + const leftEdgeX = element.offsetParent().offset()?.left as number; + const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; + + [ + { clientX: rightEdgeX - 25, el: 1 }, + { clientX: rightEdgeX - 10, el: 2 }, + // { clientX: rightEdgeX - 5, el: 3 }, + // rightEdgeX === browser right frame + // { clientX: rightEdgeX - 1, el: 3 }, + // { clientX: leftEdgeX + 25, el: 2 }, + { clientX: leftEdgeX + 10, el: 1 }, + { clientX: leftEdgeX + 5, el: 0 }, + { clientX: leftEdgeX, el: 0 }, + ].forEach(({ clientX, el }) => { + cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); + }); +}; + +export const checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px = ( + isTOSAccepted: boolean, +): void => { + checkCurrentElement(0, true); + + cy.get('[data-test=ModalOnboarding]').then(element => { + const leftEdgeX = element.offsetParent().offset()?.left as number; + const rightEdgeX = (leftEdgeX as number) + element.innerWidth()!; + + [ + { clientX: rightEdgeX - 25, el: 1 }, + { clientX: rightEdgeX - 26, el: 1 }, + { clientX: leftEdgeX + 26, el: 1 }, + { clientX: leftEdgeX + 25, el: 0 }, + ].forEach(({ clientX, el }) => { + cy.get('[data-test=ModalOnboarding]').click(clientX, element.height()! / 2); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); + }); +}; + +export const checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px = ( + isTOSAccepted: boolean, +): void => { + checkCurrentElement(0, true); + + [ + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 2, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 2, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + // { + // el: 3, + // touchMoveClientX: window.innerWidth / 2 - 5, + // touchStartClientX: window.innerWidth / 2, + // }, + { + el: 2, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { + cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { + touches: [{ clientX: touchStartClientX }], + }); + cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { + touches: [{ clientX: touchMoveClientX }], + }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; + +export const checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px = ( + isTOSAccepted: boolean, +): void => { + checkCurrentElement(0, true); + + [ + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 5, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 - 4, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 1, + touchMoveClientX: window.innerWidth / 2 + 4, + touchStartClientX: window.innerWidth / 2, + }, + { + el: 0, + touchMoveClientX: window.innerWidth / 2 + 5, + touchStartClientX: window.innerWidth / 2, + }, + ].forEach(({ touchStartClientX, touchMoveClientX, el }) => { + cy.get('[data-test=ModalOnboarding]').trigger('touchstart', { + touches: [{ clientX: touchStartClientX }], + }); + cy.get('[data-test=ModalOnboarding]').trigger('touchmove', { + touches: [{ clientX: touchMoveClientX }], + }); + checkCurrentElement(el, isTOSAccepted || el === 0); + + if (!isTOSAccepted) { + checkCurrentElement(0, true); + } + }); +}; diff --git a/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx b/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx index 6684080c68..66f364eb1c 100644 --- a/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx +++ b/client/src/components/shared/ModalOnboarding/ModalOnboarding.tsx @@ -133,7 +133,7 @@ const ModalOnboarding: FC = () => { }; useEffect(() => { - if (!isConnected && !isUserTOSAccepted) { + if (!isConnected || !isUserTOSAccepted) { return; } From cec5b8907c263bf710cf2adbdd1f612ef081bc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 25 Apr 2024 13:25:24 +0200 Subject: [PATCH 09/24] oct-1396: e2e fix --- client/cypress/e2e/onboardingTOSAccepted.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/cypress/e2e/onboardingTOSAccepted.cy.ts b/client/cypress/e2e/onboardingTOSAccepted.cy.ts index e73556e8fd..716c4683ba 100644 --- a/client/cypress/e2e/onboardingTOSAccepted.cy.ts +++ b/client/cypress/e2e/onboardingTOSAccepted.cy.ts @@ -114,7 +114,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes localStorage.setItem(IS_ONBOARDING_DONE, 'true'); localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); cy.get('[data-test=OnboardingStepper]').should('not.exist'); }); From 95d6cd078be09f9cb4d91a29dc3db7f7f7c11042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 25 Apr 2024 14:42:23 +0200 Subject: [PATCH 10/24] oct-1396: onboarding steps e2e fix --- .../cypress/e2e/onboardingTOSAccepted.cy.ts | 106 +++++++++++------- .../e2e/onboardingTOSNotAccepted.cy.ts | 22 +++- 2 files changed, 83 insertions(+), 45 deletions(-) diff --git a/client/cypress/e2e/onboardingTOSAccepted.cy.ts b/client/cypress/e2e/onboardingTOSAccepted.cy.ts index 716c4683ba..b504a7866e 100644 --- a/client/cypress/e2e/onboardingTOSAccepted.cy.ts +++ b/client/cypress/e2e/onboardingTOSAccepted.cy.ts @@ -13,8 +13,12 @@ import { connectWallet, } from 'cypress/utils/onboarding'; import viewports from 'cypress/utils/viewports'; +import { QUERY_KEYS } from 'src/api/queryKeys'; import { HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; -import { getStepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { + getStepsDecisionWindowClosed, + getStepsDecisionWindowOpen, +} from 'src/hooks/helpers/useOnboardingSteps/steps'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; chai.use(chaiColors); @@ -31,16 +35,26 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('user is able to click through entire onboarding flow', () => { - for (let i = 1; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i); - } - - cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(getStepsDecisionWindowOpen('2', '16 Jan').length - 1) - .click(); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); + cy.window().then(win => { + const isDecisionWindowOpen = win.clientReactQuery.getQueryData( + QUERY_KEYS.isDecisionWindowOpen, + ); + + const onboardingSteps = isDecisionWindowOpen + ? getStepsDecisionWindowOpen('2', '16 Jan') + : getStepsDecisionWindowClosed('2', '16 Jan'); + + for (let i = 1; i < onboardingSteps.length - 1; i++) { + checkProgressStepperSlimIsCurrentAndClickNext(i); + } + + cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(onboardingSteps.length - 1) + .click(); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); + }); }); it('user is able to close the modal by clicking button in the top-right', () => { @@ -130,39 +144,49 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes } it('Onboarding stepper has right amount of steps and highlights correct amount of passed steps', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.window().then(win => { + const isDecisionWindowOpen = win.clientReactQuery.getQueryData( + QUERY_KEYS.isDecisionWindowOpen, + ); - cy.get(`[data-test*=OnboardingStepper__circle]`).should( - 'have.length', - getStepsDecisionWindowOpen('2', '16 Jan').length, - ); + const onboardingSteps = isDecisionWindowOpen + ? getStepsDecisionWindowOpen('2', '16 Jan') + : getStepsDecisionWindowClosed('2', '16 Jan'); - for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 0 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(1); - cy.get('[data-test=ModalOnboarding__Button]').click(); - for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 1 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(2); - cy.get('[data-test=ModalOnboarding__Button]').click(); - for (let i = 0; i < getStepsDecisionWindowOpen('2', '16 Jan').length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 2 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(3); - cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=OnboardingStepper]').should('not.exist'); + cy.get(`[data-test*=OnboardingStepper__circle]`).should( + 'have.length', + onboardingSteps.length, + ); + + for (let i = 0; i < onboardingSteps.length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 0 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(1); + cy.get('[data-test=ModalOnboarding__Button]').click(); + for (let i = 0; i < onboardingSteps.length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 1 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(2); + cy.get('[data-test=ModalOnboarding__Button]').click(); + for (let i = 0; i < onboardingSteps.length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 2 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(3); + cy.get('[data-test=ModalOnboarding__Button]').click(); + + cy.get('[data-test=OnboardingStepper]').should('not.exist'); + }); }); }); }); diff --git a/client/cypress/e2e/onboardingTOSNotAccepted.cy.ts b/client/cypress/e2e/onboardingTOSNotAccepted.cy.ts index 4b6ad23529..87914fc9a5 100644 --- a/client/cypress/e2e/onboardingTOSNotAccepted.cy.ts +++ b/client/cypress/e2e/onboardingTOSNotAccepted.cy.ts @@ -13,7 +13,11 @@ import { connectWallet, } from 'cypress/utils/onboarding'; import viewports from 'cypress/utils/viewports'; -import { getStepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { QUERY_KEYS } from 'src/api/queryKeys'; +import { + getStepsDecisionWindowClosed, + getStepsDecisionWindowOpen, +} from 'src/hooks/helpers/useOnboardingSteps/steps'; chai.use(chaiColors); @@ -40,9 +44,19 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => }); it('user is not able to click through entire onboarding flow', () => { - for (let i = 1; i < getStepsDecisionWindowOpen('2', '16 Jan').length; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); - } + cy.window().then(win => { + const isDecisionWindowOpen = win.clientReactQuery.getQueryData( + QUERY_KEYS.isDecisionWindowOpen, + ); + + const onboardingSteps = isDecisionWindowOpen + ? getStepsDecisionWindowOpen('2', '16 Jan') + : getStepsDecisionWindowClosed('2', '16 Jan'); + + for (let i = 1; i < onboardingSteps.length; i++) { + checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); + } + }); }); it('user is not able to close the modal by clicking button in the top-right', () => { From 28db5239e0008d1f9e368053d1836d4cc23df8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 25 Apr 2024 17:33:46 +0200 Subject: [PATCH 11/24] oct-1396: onboarding e2e - AW open --- .../cypress/e2e/onboardingTOSAccepted.cy.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/client/cypress/e2e/onboardingTOSAccepted.cy.ts b/client/cypress/e2e/onboardingTOSAccepted.cy.ts index b504a7866e..6367806756 100644 --- a/client/cypress/e2e/onboardingTOSAccepted.cy.ts +++ b/client/cypress/e2e/onboardingTOSAccepted.cy.ts @@ -2,6 +2,7 @@ import chaiColors from 'chai-colors'; import { navigateWithCheck } from 'cypress/utils/e2e'; +import { moveTime, setupAndMoveToPlayground } from 'cypress/utils/moveTime'; import { beforeSetup, checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px, @@ -24,6 +25,30 @@ import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; chai.use(chaiColors); Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { + describe('move time', () => { + before(() => { + /** + * Global Metamask setup done by Synpress is not always done. + * Since Synpress needs to have valid provider to fetch the data from contracts, + * setupMetamask is required in each test suite. + */ + cy.setupMetamask(); + }); + + it('allocation window is open, when it is not, move time', () => { + setupAndMoveToPlayground(); + + cy.window().then(async win => { + moveTime(win, 'nextEpochDecisionWindowOpen').then(() => { + const isDecisionWindowOpenAfter = win.clientReactQuery.getQueryData( + QUERY_KEYS.isDecisionWindowOpen, + ); + expect(isDecisionWindowOpenAfter).to.be.true; + }); + }); + }); + }); + describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { before(() => { beforeSetup(); From 915ab203213ed8ee2b813b8775a8a341503e5589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Fri, 26 Apr 2024 08:09:53 +0200 Subject: [PATCH 12/24] OCT-1396: move time onboarding once --- .../cypress/e2e/onboardingTOSAccepted.cy.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/client/cypress/e2e/onboardingTOSAccepted.cy.ts b/client/cypress/e2e/onboardingTOSAccepted.cy.ts index 6367806756..64f7d8567f 100644 --- a/client/cypress/e2e/onboardingTOSAccepted.cy.ts +++ b/client/cypress/e2e/onboardingTOSAccepted.cy.ts @@ -24,31 +24,31 @@ import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; chai.use(chaiColors); -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { - describe('move time', () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - it('allocation window is open, when it is not, move time', () => { - setupAndMoveToPlayground(); - - cy.window().then(async win => { - moveTime(win, 'nextEpochDecisionWindowOpen').then(() => { - const isDecisionWindowOpenAfter = win.clientReactQuery.getQueryData( - QUERY_KEYS.isDecisionWindowOpen, - ); - expect(isDecisionWindowOpenAfter).to.be.true; - }); +describe('move time', () => { + before(() => { + /** + * Global Metamask setup done by Synpress is not always done. + * Since Synpress needs to have valid provider to fetch the data from contracts, + * setupMetamask is required in each test suite. + */ + cy.setupMetamask(); + }); + + it('allocation window is open, when it is not, move time', () => { + setupAndMoveToPlayground(); + + cy.window().then(async win => { + moveTime(win, 'nextEpochDecisionWindowOpen').then(() => { + const isDecisionWindowOpenAfter = win.clientReactQuery.getQueryData( + QUERY_KEYS.isDecisionWindowOpen, + ); + expect(isDecisionWindowOpenAfter).to.be.true; }); }); }); +}); +Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { before(() => { beforeSetup(); From 466a058cae91f5b134c11caab75341b1d7071acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Fri, 26 Apr 2024 09:54:25 +0200 Subject: [PATCH 13/24] OCT-1396: ModalOnboarding - change not.be.visible to not.exist --- client/cypress/e2e/onboardingTOSAccepted.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/cypress/e2e/onboardingTOSAccepted.cy.ts b/client/cypress/e2e/onboardingTOSAccepted.cy.ts index 64f7d8567f..4f8ba2fb35 100644 --- a/client/cypress/e2e/onboardingTOSAccepted.cy.ts +++ b/client/cypress/e2e/onboardingTOSAccepted.cy.ts @@ -94,7 +94,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes navigateWithCheck(ROOT_ROUTES.settings.absolute); cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').check().should('be.checked'); cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); }); it('renders only once when "Always show Allocate onboarding" option is not checked', () => { @@ -144,7 +144,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes it('Onboarding stepper opens onboarding modal', () => { cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.be.visible'); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); cy.get('[data-test=OnboardingStepper]').click(); cy.get('[data-test=ModalOnboarding]').should('be.visible'); }); From 26347c32aebad269272720f13bdcaa04f55ac618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Fri, 26 Apr 2024 10:56:06 +0200 Subject: [PATCH 14/24] OCT-1396: e2e test 1 --- client/cypress/e2e/onboardingTOSAccepted.cy.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/cypress/e2e/onboardingTOSAccepted.cy.ts b/client/cypress/e2e/onboardingTOSAccepted.cy.ts index 4f8ba2fb35..986d56c0db 100644 --- a/client/cypress/e2e/onboardingTOSAccepted.cy.ts +++ b/client/cypress/e2e/onboardingTOSAccepted.cy.ts @@ -129,13 +129,13 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); }); - it('user is able to close the onboarding, and after disconnecting & connecting, onboarding does not show up again', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - connectWallet(true, false, true); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); + // it('user is able to close the onboarding, and after disconnecting & connecting, onboarding does not show up again', () => { + // cy.get('[data-test=ModalOnboarding]').should('be.visible'); + // cy.get('[data-test=ModalOnboarding__Button]').click(); + // cy.get('[data-test=ModalOnboarding]').should('not.exist'); + // connectWallet(true, false, true); + // cy.get('[data-test=ModalOnboarding]').should('not.exist'); + // }); it('Onboarding stepper is visible after closing onboarding modal without going to the last step', () => { cy.get('[data-test=ModalOnboarding__Button]').click(); From 3afdead7a012075adf3bf0550b404fa9bc3a5b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Fri, 26 Apr 2024 11:44:52 +0200 Subject: [PATCH 15/24] OCT-1396: e2e test 2 --- client/cypress/e2e/onboardingTOSAccepted.cy.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/cypress/e2e/onboardingTOSAccepted.cy.ts b/client/cypress/e2e/onboardingTOSAccepted.cy.ts index 986d56c0db..e7870b7e9e 100644 --- a/client/cypress/e2e/onboardingTOSAccepted.cy.ts +++ b/client/cypress/e2e/onboardingTOSAccepted.cy.ts @@ -129,13 +129,13 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); }); - // it('user is able to close the onboarding, and after disconnecting & connecting, onboarding does not show up again', () => { - // cy.get('[data-test=ModalOnboarding]').should('be.visible'); - // cy.get('[data-test=ModalOnboarding__Button]').click(); - // cy.get('[data-test=ModalOnboarding]').should('not.exist'); - // connectWallet(true, false, true); - // cy.get('[data-test=ModalOnboarding]').should('not.exist'); - // }); + it('user is able to close the onboarding, and after page reload, onboarding does not show up again', () => { + cy.get('[data-test=ModalOnboarding]').should('be.visible'); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.reload(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + }); it('Onboarding stepper is visible after closing onboarding modal without going to the last step', () => { cy.get('[data-test=ModalOnboarding__Button]').click(); From 8f88341f01eea5d01e4a313a26c6f19b8237a07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Fri, 26 Apr 2024 13:50:18 +0200 Subject: [PATCH 16/24] OCT-1396: e2e test 3 --- client/synpress.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/synpress.config.ts b/client/synpress.config.ts index 3d6311a099..a7a345ac9f 100644 --- a/client/synpress.config.ts +++ b/client/synpress.config.ts @@ -29,6 +29,8 @@ export default defineConfig({ }, supportFile: 'cypress/support/index.ts', }, + experimentalMemoryManagement: true, + numTestsKeptInMemory: 10, viewportHeight: 1080, viewportWidth: 1920, }); From 8d89954eb5813aa7261b66c9b5ad892d65677c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Fri, 26 Apr 2024 14:44:03 +0200 Subject: [PATCH 17/24] OCT-1396: e2e test 4 --- client/synpress.config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/synpress.config.ts b/client/synpress.config.ts index a7a345ac9f..326e07b471 100644 --- a/client/synpress.config.ts +++ b/client/synpress.config.ts @@ -29,8 +29,7 @@ export default defineConfig({ }, supportFile: 'cypress/support/index.ts', }, - experimentalMemoryManagement: true, - numTestsKeptInMemory: 10, + numTestsKeptInMemory: 5, viewportHeight: 1080, viewportWidth: 1920, }); From 9de7c70c48d86c48652ef5e2b4ec93d1bf2f9eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Mon, 29 Apr 2024 09:17:11 +0200 Subject: [PATCH 18/24] OCT-1396: e2e test 5 --- client/synpress.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/synpress.config.ts b/client/synpress.config.ts index 326e07b471..4a5f1c8f6a 100644 --- a/client/synpress.config.ts +++ b/client/synpress.config.ts @@ -29,7 +29,7 @@ export default defineConfig({ }, supportFile: 'cypress/support/index.ts', }, - numTestsKeptInMemory: 5, + numTestsKeptInMemory: 4, viewportHeight: 1080, viewportWidth: 1920, }); From a9d76832c04497c8ed0cce9b62b00f891fb2a300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Mon, 29 Apr 2024 13:09:10 +0200 Subject: [PATCH 19/24] OCT-1396: e2e test 6 --- client/cypress/utils/onboarding.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/cypress/utils/onboarding.ts b/client/cypress/utils/onboarding.ts index c06444816a..4c75fd5fed 100644 --- a/client/cypress/utils/onboarding.ts +++ b/client/cypress/utils/onboarding.ts @@ -17,7 +17,9 @@ export const connectWallet = ( if (shouldReload) { cy.reload(); } + cy.wait(500); cy.get('[data-test=MainLayout__Button--connect]').click(); + cy.wait(500); cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); cy.switchToMetamaskNotification(); return cy.acceptMetamaskAccess(); From 540b9eb4e09c69f06d3135e356df305b1863ac1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Mon, 29 Apr 2024 14:40:49 +0200 Subject: [PATCH 20/24] OCT-1396: e2e fix - connect wallet + wait --- client/cypress/e2e/earn.cy.ts | 28 +++++++++------------------- client/cypress/utils/e2e.ts | 2 ++ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/client/cypress/e2e/earn.cy.ts b/client/cypress/e2e/earn.cy.ts index 9a4e207148..756cf8cf63 100644 --- a/client/cypress/e2e/earn.cy.ts +++ b/client/cypress/e2e/earn.cy.ts @@ -1,4 +1,4 @@ -import { visitWithLoader, mockCoinPricesServer } from 'cypress/utils/e2e'; +import { visitWithLoader, mockCoinPricesServer, connectWallet } from 'cypress/utils/e2e'; import { moveTime } from 'cypress/utils/moveTime'; import viewports from 'cypress/utils/viewports'; import { @@ -8,16 +8,6 @@ import { } from 'src/constants/localStorageKeys'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; -import Chainable = Cypress.Chainable; - -const connectWallet = (): Chainable => { - cy.intercept('GET', '/user/*/tos', { body: { accepted: true } }); - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); - cy.acceptMetamaskAccess(); - return cy.switchToCypressWindow(); -}; - Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }, idx) => { describe(`earn: ${device}`, { viewportHeight, viewportWidth }, () => { before(() => { @@ -78,18 +68,18 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes } it('Wallet connected: "Lock GLM" / "Edit Locked GLM" button is active', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Button]').should('not.be.disabled'); }); it('Wallet connected: "Lock GLM" / "Edit Locked GLM" button opens "ModalGlmLock"', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Button]').click(); cy.get('[data-test=ModalGlmLock]').should('be.visible'); }); it('Wallet connected: "ModalGlmLock" has overflow', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Button]').click(); cy.get('[data-test=ModalGlmLock__overflow]').should('exist'); }); @@ -99,7 +89,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes * In EarnGlmLock there are multiple autofocus rules set. * This test checks if user is still able to type without any autofocus disruption. */ - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Button]').click(); cy.get('[data-test=ModalGlmLock]').should('be.visible'); cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').should('have.focus'); @@ -111,7 +101,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Wallet connected: "ModalGlmLock" - changing tabs keep focus on first input', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Button]').click(); cy.get('[data-test=ModalGlmLock]').should('be.visible'); cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').should('have.focus'); @@ -122,7 +112,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Wallet connected: Lock 1 GLM', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') .invoke('text') @@ -174,7 +164,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Wallet connected: Unlock 1 GLM', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') .invoke('text') @@ -221,7 +211,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Wallet connected: Effective deposit after locking 1000 GLM and moving epoch is equal to current deposit', () => { - connectWallet(); + connectWallet(true, false); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') .invoke('text') diff --git a/client/cypress/utils/e2e.ts b/client/cypress/utils/e2e.ts index 83e4938dea..077518d7ce 100644 --- a/client/cypress/utils/e2e.ts +++ b/client/cypress/utils/e2e.ts @@ -42,7 +42,9 @@ export const connectWallet = ( cy.intercept('GET', '/user/*/patron-mode', { body: { status: isPatronModeEnabled } }); cy.intercept('PATCH', '/user/*/patron-mode', { body: { status: !isPatronModeEnabled } }); cy.disconnectMetamaskWalletFromAllDapps(); + cy.wait(500); cy.get('[data-test=MainLayout__Button--connect]').click(); + cy.wait(500); cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); cy.switchToMetamaskNotification(); return cy.acceptMetamaskAccess(); From b049d6440a887304f2993fbda6492de1959711f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 2 May 2024 09:57:23 +0200 Subject: [PATCH 21/24] oct-1396: e2e projects.cy fix --- client/cypress/e2e/projects.cy.ts | 64 +++++++++++++++---------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/client/cypress/e2e/projects.cy.ts b/client/cypress/e2e/projects.cy.ts index 1773bc9376..bbf66fff10 100644 --- a/client/cypress/e2e/projects.cy.ts +++ b/client/cypress/e2e/projects.cy.ts @@ -169,40 +169,40 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => cy.get('[data-test=AllocationItem]').should('not.exist'); cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist'); }); + }); - it('ProjectsTimelineWidgetItem with href opens link when clicked without mouse movement', () => { - const milestones = getMilestones(); - cy.get('[data-test=ProjectsTimelineWidget]').should('be.visible'); - cy.get('[data-test=ProjectsTimelineWidgetItem]').should('have.length', milestones.length); - for (let i = 0; i < milestones.length; i++) { - if (milestones[i].href) { - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .within(() => { - cy.get('[data-test=ProjectsTimelineWidgetItem__Svg--arrowTopRight]').should( - 'be.visible', - ); - }); - - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .then(el => { - const { x } = el[0].getBoundingClientRect(); - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .trigger('mousedown') - .trigger('mouseup', { clientX: x + 10 }); - cy.location('pathname').should('eq', ROOT_ROUTES.projects.absolute); - - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .trigger('mousedown') - .trigger('mouseup'); - cy.location('pathname').should('not.eq', ROOT_ROUTES.projects.absolute); - }); - } + it('ProjectsTimelineWidgetItem with href opens link when clicked without mouse movement', () => { + const milestones = getMilestones(); + cy.get('[data-test=ProjectsTimelineWidget]').should('be.visible'); + cy.get('[data-test=ProjectsTimelineWidgetItem]').should('have.length', milestones.length); + for (let i = 0; i < milestones.length; i++) { + if (milestones[i].href) { + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .within(() => { + cy.get('[data-test=ProjectsTimelineWidgetItem__Svg--arrowTopRight]').should( + 'be.visible', + ); + }); + + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .then(el => { + const { x } = el[0].getBoundingClientRect(); + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .trigger('mousedown') + .trigger('mouseup', { clientX: x + 10 }); + cy.location('pathname').should('eq', ROOT_ROUTES.projects.absolute); + + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .trigger('mousedown') + .trigger('mouseup'); + cy.location('pathname').should('not.eq', ROOT_ROUTES.projects.absolute); + }); } - }); + } }); }); From 6c8e201a28df8d4eaf8b4bce8341ab7501a48033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Thu, 2 May 2024 13:23:32 +0200 Subject: [PATCH 22/24] oct-1396: e2e projects.cy test 1 --- client/cypress/e2e/projects.cy.ts | 70 +++++++++++++++---------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/client/cypress/e2e/projects.cy.ts b/client/cypress/e2e/projects.cy.ts index bbf66fff10..ba5670b819 100644 --- a/client/cypress/e2e/projects.cy.ts +++ b/client/cypress/e2e/projects.cy.ts @@ -10,7 +10,7 @@ import { import { getNamesOfProjects } from 'cypress/utils/projects'; import viewports from 'cypress/utils/viewports'; import { HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; -import getMilestones from 'src/constants/milestones'; +// import getMilestones from 'src/constants/milestones'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; @@ -164,46 +164,46 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => .trigger('pointermove', { pageX: x - 20 }) .trigger('pointerup'); cy.get('[data-test=AllocationItem__removeButton]').should('be.visible'); - cy.get('[data-test=AllocationItem__removeButton]').click(); + cy.get('[data-test=AllocationItem__removeButton]').click({ force: true }); cy.get('[data-test=AllocationItem__removeButton]').should('not.exist'); cy.get('[data-test=AllocationItem]').should('not.exist'); cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist'); }); }); - it('ProjectsTimelineWidgetItem with href opens link when clicked without mouse movement', () => { - const milestones = getMilestones(); - cy.get('[data-test=ProjectsTimelineWidget]').should('be.visible'); - cy.get('[data-test=ProjectsTimelineWidgetItem]').should('have.length', milestones.length); - for (let i = 0; i < milestones.length; i++) { - if (milestones[i].href) { - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .within(() => { - cy.get('[data-test=ProjectsTimelineWidgetItem__Svg--arrowTopRight]').should( - 'be.visible', - ); - }); - - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .then(el => { - const { x } = el[0].getBoundingClientRect(); - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .trigger('mousedown') - .trigger('mouseup', { clientX: x + 10 }); - cy.location('pathname').should('eq', ROOT_ROUTES.projects.absolute); - - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .trigger('mousedown') - .trigger('mouseup'); - cy.location('pathname').should('not.eq', ROOT_ROUTES.projects.absolute); - }); - } - } - }); + // it('ProjectsTimelineWidgetItem with href opens link when clicked without mouse movement', () => { + // const milestones = getMilestones(); + // cy.get('[data-test=ProjectsTimelineWidget]').should('be.visible'); + // cy.get('[data-test=ProjectsTimelineWidgetItem]').should('have.length', milestones.length); + // for (let i = 0; i < milestones.length; i++) { + // if (milestones[i].href) { + // cy.get('[data-test=ProjectsTimelineWidgetItem]') + // .eq(i) + // .within(() => { + // cy.get('[data-test=ProjectsTimelineWidgetItem__Svg--arrowTopRight]').should( + // 'be.visible', + // ); + // }); + + // cy.get('[data-test=ProjectsTimelineWidgetItem]') + // .eq(i) + // .then(el => { + // const { x } = el[0].getBoundingClientRect(); + // cy.get('[data-test=ProjectsTimelineWidgetItem]') + // .eq(i) + // .trigger('mousedown') + // .trigger('mouseup', { clientX: x + 10 }); + // cy.location('pathname').should('eq', ROOT_ROUTES.projects.absolute); + + // cy.get('[data-test=ProjectsTimelineWidgetItem]') + // .eq(i) + // .trigger('mousedown') + // .trigger('mouseup'); + // cy.location('pathname').should('not.eq', ROOT_ROUTES.projects.absolute); + // }); + // } + // } + // }); }); describe(`projects (patron mode): ${device}`, { viewportHeight, viewportWidth }, () => { From ad3441c5ec8f02d41b94d91d28db0a52d07e0575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Sun, 5 May 2024 22:42:33 +0200 Subject: [PATCH 23/24] oct-1396: e2e onboardingTOSAccepted fix --- client/cypress/e2e/onboardingTOSAccepted.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/cypress/e2e/onboardingTOSAccepted.cy.ts b/client/cypress/e2e/onboardingTOSAccepted.cy.ts index e7870b7e9e..738c2caefb 100644 --- a/client/cypress/e2e/onboardingTOSAccepted.cy.ts +++ b/client/cypress/e2e/onboardingTOSAccepted.cy.ts @@ -94,7 +94,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes navigateWithCheck(ROOT_ROUTES.settings.absolute); cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').check().should('be.checked'); cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.get('[data-test=ModalOnboarding]').should('be.visible'); }); it('renders only once when "Always show Allocate onboarding" option is not checked', () => { From a456021113a2dcacc0fc9ead0f6f7ce58a54cbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Wed, 8 May 2024 12:52:03 +0200 Subject: [PATCH 24/24] oct-1396: cr fixes --- .../cypress/e2e/onboardingTOSAccepted.cy.ts | 103 +++++++----------- client/cypress/e2e/projects.cy.ts | 73 +++++++------ client/cypress/utils/onboarding.ts | 12 -- .../AllocationItem/AllocationItem.tsx | 4 +- .../OnboardingStepper/OnboardingStepper.tsx | 16 ++- client/src/constants/milestones.ts | 3 +- client/src/store/onboarding/store.ts | 1 - 7 files changed, 95 insertions(+), 117 deletions(-) diff --git a/client/cypress/e2e/onboardingTOSAccepted.cy.ts b/client/cypress/e2e/onboardingTOSAccepted.cy.ts index 738c2caefb..88b42e9916 100644 --- a/client/cypress/e2e/onboardingTOSAccepted.cy.ts +++ b/client/cypress/e2e/onboardingTOSAccepted.cy.ts @@ -16,10 +16,7 @@ import { import viewports from 'cypress/utils/viewports'; import { QUERY_KEYS } from 'src/api/queryKeys'; import { HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; -import { - getStepsDecisionWindowClosed, - getStepsDecisionWindowOpen, -} from 'src/hooks/helpers/useOnboardingSteps/steps'; +import { getStepsDecisionWindowOpen } from 'src/hooks/helpers/useOnboardingSteps/steps'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; chai.use(chaiColors); @@ -60,26 +57,18 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('user is able to click through entire onboarding flow', () => { - cy.window().then(win => { - const isDecisionWindowOpen = win.clientReactQuery.getQueryData( - QUERY_KEYS.isDecisionWindowOpen, - ); - - const onboardingSteps = isDecisionWindowOpen - ? getStepsDecisionWindowOpen('2', '16 Jan') - : getStepsDecisionWindowClosed('2', '16 Jan'); + const onboardingSteps = getStepsDecisionWindowOpen('2', '16 Jan'); - for (let i = 1; i < onboardingSteps.length - 1; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i); - } + for (let i = 1; i < onboardingSteps.length - 1; i++) { + checkProgressStepperSlimIsCurrentAndClickNext(i); + } - cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(onboardingSteps.length - 1) - .click(); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); - }); + cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') + .eq(onboardingSteps.length - 1) + .click(); + cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding]').should('not.exist'); + cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); }); it('user is able to close the modal by clicking button in the top-right', () => { @@ -169,49 +158,41 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes } it('Onboarding stepper has right amount of steps and highlights correct amount of passed steps', () => { - cy.window().then(win => { - const isDecisionWindowOpen = win.clientReactQuery.getQueryData( - QUERY_KEYS.isDecisionWindowOpen, - ); - - const onboardingSteps = isDecisionWindowOpen - ? getStepsDecisionWindowOpen('2', '16 Jan') - : getStepsDecisionWindowClosed('2', '16 Jan'); + const onboardingSteps = getStepsDecisionWindowOpen('2', '16 Jan'); - cy.get('[data-test=ModalOnboarding__Button]').click(); + cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get(`[data-test*=OnboardingStepper__circle]`).should( - 'have.length', - onboardingSteps.length, - ); + cy.get(`[data-test*=OnboardingStepper__circle]`).should( + 'have.length', + onboardingSteps.length, + ); - for (let i = 0; i < onboardingSteps.length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 0 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(1); - cy.get('[data-test=ModalOnboarding__Button]').click(); - for (let i = 0; i < onboardingSteps.length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 1 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(2); - cy.get('[data-test=ModalOnboarding__Button]').click(); - for (let i = 0; i < onboardingSteps.length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 2 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(3); - cy.get('[data-test=ModalOnboarding__Button]').click(); + for (let i = 0; i < onboardingSteps.length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 0 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(1); + cy.get('[data-test=ModalOnboarding__Button]').click(); + for (let i = 0; i < onboardingSteps.length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 1 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(2); + cy.get('[data-test=ModalOnboarding__Button]').click(); + for (let i = 0; i < onboardingSteps.length - 1; i++) { + cy.get(`[data-test=OnboardingStepper__circle--${i}]`) + .then($el => $el.css('stroke')) + .should('be.colored', i > 2 ? '#ffffff' : '#2d9b87'); + } + cy.get('[data-test=OnboardingStepper]').click(); + checkProgressStepperSlimIsCurrentAndClickNext(3); + cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=OnboardingStepper]').should('not.exist'); - }); + cy.get('[data-test=OnboardingStepper]').should('not.exist'); }); }); }); diff --git a/client/cypress/e2e/projects.cy.ts b/client/cypress/e2e/projects.cy.ts index 87f81b7be9..474d90042b 100644 --- a/client/cypress/e2e/projects.cy.ts +++ b/client/cypress/e2e/projects.cy.ts @@ -10,7 +10,7 @@ import { import { getNamesOfProjects } from 'cypress/utils/projects'; import viewports from 'cypress/utils/viewports'; import { HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; -// import getMilestones from 'src/constants/milestones'; +import getMilestones from 'src/constants/milestones'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; @@ -160,48 +160,49 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => cy.get('[data-test=AllocationItem]') .trigger('pointerdown') .trigger('pointermove', { pageX: x - 20 }) - .trigger('pointerup'); + .trigger('pointerup', { pageX: x - 40 }); + cy.wait(500); cy.get('[data-test=AllocationItem__removeButton]').should('be.visible'); - cy.get('[data-test=AllocationItem__removeButton]').click({ force: true }); + cy.get('[data-test=AllocationItem__removeButton]').click(); cy.get('[data-test=AllocationItem__removeButton]').should('not.exist'); cy.get('[data-test=AllocationItem]').should('not.exist'); cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist'); }); }); - // it('ProjectsTimelineWidgetItem with href opens link when clicked without mouse movement', () => { - // const milestones = getMilestones(); - // cy.get('[data-test=ProjectsTimelineWidget]').should('be.visible'); - // cy.get('[data-test=ProjectsTimelineWidgetItem]').should('have.length', milestones.length); - // for (let i = 0; i < milestones.length; i++) { - // if (milestones[i].href) { - // cy.get('[data-test=ProjectsTimelineWidgetItem]') - // .eq(i) - // .within(() => { - // cy.get('[data-test=ProjectsTimelineWidgetItem__Svg--arrowTopRight]').should( - // 'be.visible', - // ); - // }); - - // cy.get('[data-test=ProjectsTimelineWidgetItem]') - // .eq(i) - // .then(el => { - // const { x } = el[0].getBoundingClientRect(); - // cy.get('[data-test=ProjectsTimelineWidgetItem]') - // .eq(i) - // .trigger('mousedown') - // .trigger('mouseup', { clientX: x + 10 }); - // cy.location('pathname').should('eq', ROOT_ROUTES.projects.absolute); - - // cy.get('[data-test=ProjectsTimelineWidgetItem]') - // .eq(i) - // .trigger('mousedown') - // .trigger('mouseup'); - // cy.location('pathname').should('not.eq', ROOT_ROUTES.projects.absolute); - // }); - // } - // } - // }); + it('ProjectsTimelineWidgetItem with href opens link when clicked without mouse movement', () => { + const milestones = getMilestones(); + cy.get('[data-test=ProjectsTimelineWidget]').should('be.visible'); + cy.get('[data-test=ProjectsTimelineWidgetItem]').should('have.length', milestones.length); + for (let i = 0; i < milestones.length; i++) { + if (milestones[i].href) { + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .within(() => { + cy.get('[data-test=ProjectsTimelineWidgetItem__Svg--arrowTopRight]').should( + 'be.visible', + ); + }); + + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .then(el => { + const { x } = el[0].getBoundingClientRect(); + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .trigger('mousedown') + .trigger('mouseup', { clientX: x + 10 }); + cy.location('pathname').should('eq', ROOT_ROUTES.projects.absolute); + + cy.get('[data-test=ProjectsTimelineWidgetItem]') + .eq(i) + .trigger('mousedown') + .trigger('mouseup'); + cy.location('pathname').should('not.eq', ROOT_ROUTES.projects.absolute); + }); + } + } + }); }); describe(`projects (patron mode): ${device}`, { viewportHeight, viewportWidth }, () => { diff --git a/client/cypress/utils/onboarding.ts b/client/cypress/utils/onboarding.ts index 4c75fd5fed..dc55ab919d 100644 --- a/client/cypress/utils/onboarding.ts +++ b/client/cypress/utils/onboarding.ts @@ -57,9 +57,6 @@ export const checkChangeStepsWithArrowKeys = (isTOSAccepted: boolean): void => { [ { el: 1, key: 'ArrowRight' }, { el: 2, key: 'ArrowRight' }, - // { el: 3, key: 'ArrowRight' }, - // { el: 3, key: 'ArrowRight' }, - // { el: 2, key: 'ArrowLeft' }, { el: 1, key: 'ArrowLeft' }, { el: 0, key: 'ArrowLeft' }, { el: 0, key: 'ArrowLeft' }, @@ -83,10 +80,6 @@ export const checkChangeStepsByClickingEdgeOfTheScreenUpTo25px = (isTOSAccepted: [ { clientX: rightEdgeX - 25, el: 1 }, { clientX: rightEdgeX - 10, el: 2 }, - // { clientX: rightEdgeX - 5, el: 3 }, - // rightEdgeX === browser right frame - // { clientX: rightEdgeX - 1, el: 3 }, - // { clientX: leftEdgeX + 25, el: 2 }, { clientX: leftEdgeX + 10, el: 1 }, { clientX: leftEdgeX + 5, el: 0 }, { clientX: leftEdgeX, el: 0 }, @@ -147,11 +140,6 @@ export const checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px = ( touchMoveClientX: window.innerWidth / 2 - 5, touchStartClientX: window.innerWidth / 2, }, - // { - // el: 3, - // touchMoveClientX: window.innerWidth / 2 - 5, - // touchStartClientX: window.innerWidth / 2, - // }, { el: 2, touchMoveClientX: window.innerWidth / 2 + 5, diff --git a/client/src/components/Allocation/AllocationItem/AllocationItem.tsx b/client/src/components/Allocation/AllocationItem/AllocationItem.tsx index 0601316df6..ad159b612f 100644 --- a/client/src/components/Allocation/AllocationItem/AllocationItem.tsx +++ b/client/src/components/Allocation/AllocationItem/AllocationItem.tsx @@ -204,12 +204,12 @@ const AllocationItem: FC = ({ animate( ref.current, // @ts-expect-error e is wrongly typed, doesn't see x property. - { x: e.x < startX ? constraints[0] : constraints[1] }, + { x: e.pageX < startX ? constraints[0] : constraints[1] }, { duration: 0.2 }, ); }} // @ts-expect-error e is wrongly typed, doesn't see x property. - onDragStart={e => setStartX(e.x)} + onDragStart={e => setStartX(e.pageX)} style={{ x }} > {(isLoading || isLoadingError) && } diff --git a/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx index 98226fbaf6..f48cdfbf25 100644 --- a/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx +++ b/client/src/components/shared/OnboardingStepper/OnboardingStepper.tsx @@ -30,6 +30,18 @@ const OnboardingStepper = (): ReactNode => { const viewBox = '0 0 56 56'; const numberOfSteps = stepsToUse.length; + const animationProps = isDesktop + ? { + animate: { bottom: 48, opacity: 1, right: 48 }, + exit: { bottom: 24, opacity: 0, right: 48 }, + initial: { bottom: 24, opacity: 0, right: 48 }, + } + : { + animate: { bottom: 116, opacity: 1, right: 24 }, + exit: { bottom: 92, opacity: 0, right: 24 }, + initial: { bottom: 92, opacity: 0, right: 24 }, + }; + const svgNumber = useMemo(() => { if (lastSeenStep === 1) { return one; @@ -46,13 +58,11 @@ const OnboardingStepper = (): ReactNode => { return ( setIsOnboardingModalOpen(true)} whileHover={{ scale: 1.1 }} + {...animationProps} > ({ setIsOnboardingModalOpen: payload => { set(state => ({ data: { ...state.data, isOnboardingModalOpen: payload } })); }, - // eslint-disable-next-line @typescript-eslint/naming-convention setLastSeenStep: payload => { localStorage.setItem(LAST_SEEN_STEP, JSON.stringify(payload)); set(state => ({ data: { ...state.data, lastSeenStep: payload } }));