���� JFIF    �� �        "" $(4,$&1'-=-157:::#+?D?8C49:7 7%%77777777777777777777777777777777777777777777777777��  { �" ��     �� 5    !1AQa"q�2��BR��#b�������  ��  ��   ? ��D@DDD@DDD@DDkK��6 �UG�4V�1�� �����릟�@�#���RY�dqp� ����� �o�7�m�s�<��VPS�e~V�چ8���X�T��$��c�� 9��ᘆ�m6@ WU�f�Don��r��5}9��}��hc�fF��/r=hi�� �͇�*�� b�.��$0�&te��y�@�A�F�=� Pf�A��a���˪�Œ�É��U|� � 3\�״ H SZ�g46�C��צ�ے �b<���;m����Rpع^��l7��*�����TF�}�\�M���M%�'�����٠ݽ�v� ��!-�����?�N!La��A+[`#���M����'�~oR�?��v^)��=��h����A��X�.���˃����^Ə��ܯsO"B�c>; �e�4��5�k��/CB��.  �J?��;�҈�������������������~�<�VZ�ꭼ2/)Í”jC���ע�V�G�!���!�F������\�� Kj�R�oc�h���:Þ I��1"2�q×°8��Р@ז���_C0�ր��A��lQ��@纼�!7��F�� �]�sZ B�62r�v�z~�K�7�c��5�.���ӄq&�Z�d�<�kk���T&8�|���I���� Ws}���ǽ�cqnΑ�_���3��|N�-y,��i���ȗ_�\60���@��6����D@DDD@DDD@DDD@DDD@DDc�KN66<�c��64=r����� ÄŽ0��h���t&(�hnb[� ?��^��\��â|�,�/h�\��R��5�? �0�!צ܉-����G����٬��Q�zA���1�����V��� �:R���`�$��ik��H����D4�����#dk����� h�}����7���w%�������*o8wG�LycuT�.���ܯ7��I��u^���)��/c�,s�Nq�ۺ�;�ך�YH2���.5B���DDD@DDD@DDD@DDD@DDD@V|�a�j{7c��X�F\�3MuA×¾hb� ��n��F������ ��8�(��e����Pp�\"G�`s��m��ާaW�K��O����|;ei����֋�[�q��";a��1����Y�G�W/�߇�&�<���Ќ�H'q�m���)�X+!���=�m�ۚ丷~6a^X�)���,�>#&6G���Y��{����"" """ """ """ """ ""��at\/�a�8 �yp%�lhl�n����)���i�t��B�������������?��modskinlienminh.com - WSOX ENC screenshot.png000064400000224252146726766340007463 0ustar00PNG  IHDRqQ&iCCPkCGColorSpaceGenericRGB8U]hU>sg#$Sl4t? % V46nI6"dΘ83OEP|1Ŀ (>/ % (>P苦;3ie|{g蹪X-2s=+WQ+]L6O w[C{_F qb Uvz?Zb1@/zcs>~if,ӈUSjF 1_Mjbuݠpamhmçϙ>a\+5%QKFkm}ۖ?ޚD\!~6,-7SثŜvķ5Z;[rmS5{yDyH}r9|-ăFAJjI.[/]mK 7KRDrYQO-Q||6 (0 MXd(@h2_f<:”_δ*d>e\c?~,7?& ك^2Iq2"y@g|U\ @IDATxVE, K H ]R!"((! m |\ޘ3{9)U υB$@$@$@$@$@$ D~:Xy     L$@$@$@$@$@$@$@}HHHHHHC       (t+e7      r>@$@$@$@$@$@$@! "@Rv      *$@$@$@$@$@$@ (t+e7      r>@$@$@$@$@$@$@! "@Rv      *$@$@$@$@$@$@ (t+e7      Ç=&77ԗ%    :BArKk:(1_Z}Y1赲K*'T/BBv۲snYdl޲U._ޢU)^Xv$@$@$@$@$@$@AAnCrM5oeˌY2l}]'vl+g[wTRJܸq% }MzME*3eC7a.]1CzMN|"Xwf6Wʒ%̞9]AƎ ^f]Hy\SϱcO,Y$Q:ɓ'bh,S$Iy\~Cǻs>z"OȒ>}zyfVaI$N>d}3ŋbV +夹2IZG۫Y̥ԙZ1ĉJ[N eͮlݺ}&cS~j)(st8N] ,'!k׮ja+Vj%oMݍbK5z)RX[*oV(ǰ` A8*>vgyrVY+T(gӴrWY!˖C Sbhs6Z1k]u8pPFc}     pBLqVNPĉcۉ:/ $Ŋhŋ>l-f7{(1ۥVŪpݼi )S|! ҃9|~$9ŵt5W7'c(mݻuV@ڮ}Gm%PG|zʲd2m۶m]cPRI%3f͖o'O˖[Q>xb)Xb6UTS d\)+|ªv+23o PU1p#`Vyi15KڋyX4I((P!N֖P, 2eJRTI}w*Uk#]lRRTG$@$@$@$@$n!&n ع[O7gďeb\ uE:h NN[K/nY*W."<1a.!}1jWVX]~:eJ%cQ 6ɤCJ5e 4bga%\Wbjek1y`#9J8uަUI Oyv4r= IHHHH6b9lc=OsMC8رV5ÔIe:?w+u->w sTB6m"9,V8g_P:ܻwZ%Er T;wޕYFBXӆ(ȹ$E Ǣ-P.|忳b\}Õ<{zEݚ9[8\uikը.K͓L,c惎K{4 E9 E6h4+WyŴyC+mK8er/uBܻժ8\K.]y8_l)?e6oJIFX1VKuh }<~g_4'MT=cZj5ҽWojֹ1#/>M@cPh:ŋȜyK2Aͽ.([ٳ7vmlDSHHHHHVjG㦲YYi%O\~gQvpϿoѕS U>$v& b5X:w }Њc='5*p9Kh'SQ֬]' RʕIxdXeR z5ӮvSE\ ee_D 2QZrui;AZPc,.{teseE][ɱ y^|רUYP YNcXM|nIHHHH>b9R$TKמzQȑ}^ZU)l7\ V4RL)?ccl!T'nbZidEO"uNQSwX!V>g< Vܢ"q˨^ʽXUsCRLkAAo{H)ȑҥٳ[OY :H6nP)mw buUSl܇wl}     pB ]Od #;w$A|gpkdZ[FmXnܼ!X%q^ ÇryBZ+^rMz0ҝ`ǏHwR{IhQ]>{Z +%yd~,-[;8IA7?yG!7qK$@$@$@$@$~42܊dŊUż՜9wwӀ /dɒz?W!;5S[?a>F}OlcV qEm @"ÐqfEB2.Ϧk"'g<,]&_PM%:o#r0ܒ yҐ} yZ4jGKӒ$qbD)xD$@$@$@$@$ DH07O$@$@$@$@$@M;#     fb!IHHHHH d P9 Y̝HHHHH*ᢙXH     YTC/s'     pAah&HHHHHBÐIHHHHH \r.$     %@0d2w     fb!IHHHHH d P9 Y̝HHHHH*ᢙXH     YTC/s'     pAah&HHHHHBÐIHHHHH \r.$     %@0d2w     fb!IHHHHH d P9 Y̝HHHHH*ᢙXH     YTC/s'     pAah&HHHHHBÐIHHHHH \r.$     %%dw2HHHHHH P"G 0T'OȝwUQ&"     pO N8%JUp_bj70 w'N-o<"     dIHHHHH 8P9 ̃HHHHH9p^xQS޻Λ!iY `j HHHHHB@TWY+)" ʛrJ~޽C1W979s۔׮__f̔-lnEB@t2ʞ={f uʨ1_ʸ j6LO$@$@$@$@$JrvziQs[lޱM&N/դG1ܵm'NInx x<܊w ={̙=Cc Ӄ#+A$@$@$@$@$ rGYK*).SIHe_sf-%*lY,Zm4hDǁm]Vx ǵ'Ι|کܺuJy':ol8uR )Wpgp|X8?._I[V]ٲu H:'m/Rn~TnN{q[kO81 dYt9FR6t'    x=•rN5kBR t|MV>z(k+;v6[IebITA> KݔvO fu]GcX$ɓ;>t\zUbD!3gVgׯ  WX C$@$@$@$@$DymW =똣*e -YB.+eeт7OnJAC∓e˖aC H굫8Qb}]Ge2t@Ic7IҽgZڵg=wNϝ-P 3f[Jر,$R+V(ҤI-E ҧʖ)%I$?>n~cɞ#LQ־Q#^*:şy i &ٲك^/NAYj%CV)=*ժT*Kv)Uݷ_n\.5kT{%:jJO͛oėV _HaYhLyHHHHH D +aq4 X<ɱc$VX+g+Z'sKsrĽtu]y]^¥t:Sy"FV4P8Kt"})|$E>x v˩Rԧǎ]YG1ֲ+J˖-dΔ镬ʔ.f:d25G.LyHHHHH D +0YdƟV,<1bRČS>y1'*6~]GS(+djIrU'ӧtbƌ᧌T=~^Ҷ'2|BÏ?كml™,iRiSnZt yYh{ *SU5tR$aݱcԬ^UmL^ܒ RV\^-a7ߊo,rMV<-j8nI‚*ts.>6ryR{SsC?ǮPb+K(\37nlEüo[w. SV}C;GӦ*y3f>\|yיժWp-]b_XRJemJAxHHHHH_•ruQYuvW6|tARZVw",S2bh)Xn"ekZ+;pPZn'4 VO[kO;)KXɓV.dy U|}Ji{=ZtWA> u0o*&z2jk"iW,7x)WCm\]HHHHHL+f,-ZRpO,YRONi& aB,eēbtѢڨʅbkƋ̈I0rwSd2oF q{a B \+Ԇ'g/LW%/($@$@$@$@$@aðFᮄ-O!    ?•rxԇ,) |] u5C$@$@$@$@$@aoҰ $@$@$@$@$@$@Ba`EIHHHHH lrڃ!     P!@0T$@$@$@$@$@$P9 [Ґ @r*yQ     [`iHHHHHH TP9 ( -TV{4$@$@$@$@$@$* v^HHHHH*a=X     TC;/J$@$@$@$@$@að, *%     EajHHHHHBP΋ @" o .-X{ɪUV)>}*WCsF Zi׮L>=0Iy䉮˗_ƍ: uD?qC7kLa!p%ݯpO/^ΝJ Ԕ)S{*;Zwڐ]po!6mҵYd|AYPB.$@$@@S{ehN[l-[KZϟ>L^jǀO>3VL}nr1+<0;l><0IYv7y%n=o߾2h ի4nXڴi#Ϟ=q'N(+p"82'o޼V1΄߿_GH&W_}Rه|rƈC/_~z7N770alۺM{G+o#׮_ԯ__ tHk_96{li۶3w U }!Hc?A<J @"w59rHh]/(Zgw:oyE T9s$jԨrJY:uqF;dᢠYB_<>yXNGǏ/tѣGuvOŎ;oaܮ]VF )~ApˣGɓrKoFDVđ#Gjjժ/6C/[_SF;^˷o_n^H !\Ə/uԑ… \13R*U5X&M$JwyG5j$'NG[V͘1C*<Λk.pE+W[:eݻCY-ҼysJvl2} u2dTVMs_e̙2rHɓ;s>FzVD ӷܺuKƄՈ Kqz+8~/>z,8 >{֚Ǐˆ ƍ-4lPŋ֪.1k׮-#FQஊv<'Ș1ce-ho߾zgΙv.\YN@Μ9\'KXኻuVQXSw6;E>ʋ0X`͆L{=bSJ =1`ݯӧeCEo(v:Hƌa___W>be~ʕ+w5~(<:t`Yݏ`&J*?'os]W־oVsywYG&fzۮR`|]kw_>ÐwIVs7:Ö$[V#%O:ەYa! D rX*L@K >Wcurҷt6ݰk׮O?i?ԐywMj28H]?K.K qaݚ\n*pcʗ/@9[bwܹR`A}2BQTXcIfu +_tYYBk`$Aَ7h~0GiӦ.]S'4xV.+={nW%K K(]P/'O\2 }⩟?z-8qhexf#b A%$IĞ|R@y뭷tyRJb'M (eQJ(Ί%2 })]t =i1cǎ rAýM /[.+WDRy7n-Zȴi{Tgu{_@1nh 2gDҥe^D?4r]?<%Ax:}ٳZ:"ݝ x{J+@PAaxjS?ྍ{qR]oz_=a`rFp/Q\Bٿt!X?|G02&Z‹+<'%(9B i+2[x:~lqw&m}~r%xBggFwj036{|o~p=Lչ<#9֪IYY[Vx(x5/,)ސekJ7S_3RCuYUmd=w)k{2bniV*PA.WasU$IydY?rty|X"Lm_A.ܼ+glQ(ew]ҽjYYuc'RE}'VBB_-?_,}jI1ţU䙣YpH(a A*BbŴe)SF2|aAʝ[+ʋ| X(\ɒ&Zla1[~UGf/2IY`ч{T_,?7k,m˒%`]X1H"o&P"Pһu릓̕S֭u(GsANj(%SL-Z4I,~Qe3$N8Z s}kDMҧOOCQ% ^Ax/a!e!\ɝiA.xfup^h"f̘Z9twJo8`h9(g oJ|E/|V 6\G5e<\mg l]jBaiev(]~qLƈ&?7/%Q"GbI~\uqaێ_YmI4t/)7)k˰:/~u3GEqe2"%Y<GR%-ت[Z)ѣJKR6k 8k_X{q/FܵɑGtvvJ9tݬYjAB0?xiҤF [Ƌ+W\5}leb'QDvEO] 7&_^7^E [H fwq$ʕUy n;~E%[ʓ7cRl䴲Ȝ\+c OatN$B*ϱb7VwૃsIlEK8z>ѶS\Hvnuƿt0_# [)_U~'Cq8xܽ}_2+\lU b?} M/G~Y6~4V!C<(X'Nm\Mз9b)&мu o;DC'K.oݖje?iP!\@Vvɝ++@J<@ sð;( |-SW⮮*X` 7P2RvsdJ-,c *s̒7O^⺰̛upPhO fZ>Xxgk7 Z!&M Jя?fps'A*ҡ=@`}sFV^r\h!js{]9"Gr4\Œ~:xb&p4rI| \<ݏ&~Ho@7U&).Xz< |~n<7ɕW  }@bŊ:0wfۘ[,x?c /TBP[<"•tM&#' Đl#j$U82g1meQC˷$~hRďy_ɮ\R!{N_gLeћwXb ,ovשkR⅂uuԉdmh92;,O_ ƿ/A7bkc#I@J˸$@$  ka=K$,S}7p?|V2,7X MSf,\q`yc)Vn?ybRW#X& )-gy^XL&(75uhA9/` {p类Pb^(!mn΂r``+\bp!BkZ6mhk(p = m1o{+,]yT8a9c-~XRʊ5p %a3a@z E *]A{^iؠ\pQκ㉙3ET= ޾r~G_(\mPPA=b xyӮzZ9V4,XK?*>}^o1_;1.pG:;Ooy!7Șcx xo`ijsXukyd˼=ٻGdzgVW=~`'uLWgd3ʗ/͟^+K,&sB 6C޸+IDPnU=g'eRX}YR|'S];>?)Ɩu5ѝ[~@l=qYYFwK<$n(H#P _^;rͭ6^/D #ΗNgd\RUjqg >tb! `&.Cc VV) ~1W KCFBѳv5NuX`XbǎmuFaÇ7pB4Paa X'Jd|6@._j:f{% [s%Vʕ*A$|B9\P>s qck [9sy&"&8#y3e;pC]\A ׷L.ACw;Bw1P4+zg<(@֜gk֪_d@5ZXsarmn&h^իVk2P1[y޴}s={|O` 汢iFϷD{/?LPy+ XPۚ5jhMo"qng2r{f1++c "pt?uU^n3ƞgz!#=[F;72WyxL9L{:cެȊ,/`3x!-  ?x^s//ҴiSI G?iʋ7ܷx{Ez{_q`늴FQJo۟-H54CWNnV|!MybG">('D9?hѬ)uIƐ!vJn9lf|a3V-UWy{̲8W44Us + /nܑ᪞6eIrftTT+VUJ]~iYFJ(CՂ8XfT"V0OkQZ\O2X[N]>)C )SSVf! &I<'gjI'U:AQrֈ0(jWYQBU*Wъ΂kRY<9npuSouE1b`Z!X]} D@Ǐ6oat՗ςꈶ[qWvUOXq7k }PmQtaw:ۮU]QX]G}r?^`yjkFx83Ǟ7'x\}2}ztfhW} J]yc11_Haƽ kVOa7F(=ʫﰱ'‰\M*XPz.F#;HVf,*2ܼܷU}$MDym;*%;j$gj>毛^..y$Gp<ʁI+f,X]@ksh@ )w5Psל9l=,|?dRPMr8 @?u JYOOya`Jk.|jQ q ::Y (]+Oq ޘyA r8IOXإt' i[z]wXi€B$&XXb7iHXתOKNTPT~Rŋ%5r2H@P,T5 (:R%xY      #@0$@$@$@$@$@$fP9 3M @rzye     3`AHHHHHH P9 =2 TLS $@$@$@$@$@$z{^HHHHH *a)X     =TC=L$@$@$@$@$@a0, *ǞW&     0Cai HHHHHBc+ @!@04 B$@$@$@$@$@GaIHHHHH rf!     #@0$@$@$@$@$@$f;p… a`h޽{j*UOիСCܨQd…Vx`vڵk'ӧOLRiW_A@IDATӧČS_srر ]ɓ'DM8Q{ߛR~}x˨|z!}KΝVZRn]9q89W_ ΀Gf-m0[{w57o lOa^`U:FsJ^+eJ |\ᨥ5s _:%Oy%mL|4p S}RnJyn}LO?^:~0e@v'MTx>r~'FVI3%}_ $H@쿜׵kȑ#a_|2i$ɐ>Co Gɓ'W.өS'W<{L'ݻu͚eK%Zh$;yܹs<C lsʈ ~"$esf;{?ɂ'IcR:[JEɔu37JHˮJd# 'rḣ~(Ǐ:uH…O2UJ+5u nPJ*%4jȲ=GZ3fСCysk`ʕuYwڥp],VEei޼eZ||Y N dٲe8dVNk믿̙3e䨑'w`}[DҧouF&L >V꭮}Aᣇ&Ko[,{F\~~uL|l#G,y䕉N ͝J2XaႺuVCԋSvmA{ĭQfҶm[?K.} 1.q۴i~s%O:U*T倁t"Cul,V\i31\z3{_B}Iy롯7nvOFqٛ8O3pmk?9pM M-_ҥKn#F0t?smb +GaDxBFx~73;{ֳ࣏>/bLzOL)'-hݵmO܆)ە+WsS!+opsm_R%?2-{K#'~񻅾c}be{iIVS׫{r[e?wCgLo?yEvG_rLi9yTJ1?O6$s%\Jo?P|yܹs`:3Ou;!W/F-?T_~]tז}s /A+~%姟~* zK}T&O6QDm1t $0U+~;[lٲ$IۼE 6m5k4kL>9c`)ʼyJ=;E&P7n,iӦ.]Kʒ%K50W hWϓQJd=Xס : ^$2dҥKynPcJ„ <#yyq/Q\珲1 7{؇g /gCwo}qC?GsƳY0=믿7|S?RLQܳu~x)㣕ӊ+jWx{OxK~uU:!C-J 57U'zI/=p\~\zK;WJO$I1 Y+OZn/RYHVR'E=kHuzIgCE2K|:}feYG^2|*Uվ4,IzQ+cԊy2@5t-IQOEu-ϙJv +9R%OnsU$IydR#|"Kce s}!onŊՆ8qbI4]V+&}ۢe )_>\Ygkլ%XK-Z~z)Tl۶M|uE;Dq`ʟ?_~ikӵ\wn۷9KNgϮcre Z״~ڞI聗X_̀/ڷo?xoQG5_̇Eɓ'’mmERPA)V9~y>TZU*L=}~>=4mDڣbaYŲ蠸1JKśR5O:YOe%{Oi{N R])pΒ VtI fu8eً#Q 9z3lPNTz˯ؒPxFQ I| mTX`+)_~@V:Pq\:V#͋E#MՂ4(oXU߈UF{G#F#Ե3%8)ӗz"&! @Wau1oX!@;۠A$W\zpoae\I 5H*Vo{;:c,PV:fQ|1H`B`5ԯW_ 5:upt>E16qm= @BFĉ1,-0>l?En בl`)4mCZbNe}y nB<!Ç빭Jb ?XJtX4(KsZyӵM=Ԫe+?QV4iL\9sYa%?cnG7ժVG]JOPz? 8v m۾Rx3D}lA]|Ë2hvMJPvĵ ^1JჇ&J/'z".(FkO}e=7Sn!(xg x9~h,YΓRM?t '>KOvB.޺3$[)>?B+)إ_J>dn?MJㅄ髷`Z14QB6)c`&CHdB9MrS(\> grE2# cGoUߜ2fNɪ_*%ܖ) _ zu릭;C94pA-ܔx;؝`b,oP1 ʐzj}l'K.oݖx f ;a;e+Εl - P%L?K, SW⮮ɒ:,wA U q}OmigW9n0un#㶈P|⼳M>3f0`En|JT!u`%9b4mT+modmxтz={V[3 P"  q 7g)yb%M`ړH_܃Q C^mh P + 0fΒY5?`v2Fm$ywݱiw'n&{K4%r0qV`7<ȩS̮ɾZ1~}}wgBQn1?k>2r$ri j:v${ 2zAyԒ<^L9a=xf^8^"hIoE[Gav*VGHbQ|9u] we>^Pl$pGSwyFxv`(SPB#.x% lQ.sZi9Xg4=8ٽXTǛ``XO` 򅢁EH6m-1 ._-=OMḞ|b,ނx9 Eg _Je^[OܜBAݱ`d<6h(/x_S lxk7wck湛z9gs޾E:g ^F= ~0G}y9f 6r7-U>!N ̦CZ)_:gp1T8Ɯ/^:_3R"*"jm{))Wʱ껅;z;' q-٤[+y<S/oǏ)1mʄey,}objaHj BnxQHH ¥rh,y}:uhHX0`@@0G5}(0=z&? U;pl-vz 6| 4G?z P oaa>`סCQ0Oʕ*k%lڥf͚m FlqU]aE;nvKb4̙Szeye W^X <%|"=?,8-.XWsEUVMB+eby]|0 ˮ,M%H@O'(8U`V֮z  !/,FSr=x9g=kH%/A g npQƋ ]˙s{_J (DO?l!S/}R lmr;3i@ VK(1PkgEb!AI7n\]$wB!!D~b"X?PDYj8ama"'nsgd8e5Bm:ǷS l< qB.ù?;Ǹ21ĢhxW1튗xx#}Eo V 6w/>O$ϓ|A1ɐ<D=YmY.s9ܻ+H3ʹ꭫?E3&YSI9}/ɓ&LhUF&:(ϐEk*+"+%ՈꋮĜT1$]קROY ,& Ns(wO,ՒQaJrfjepRW8.{Β}hW2a3!/($@$XԊAɳ'U: ΂0("fe``hӇkrY<9npb׬"1cQ0{-#ad&fѣ9~/bM`5UA>wF\3W}??e 4ykOrU{{cr:F[ (6_SZW\E(3CWm~썛=.1u9SB8/&\<ʟXFh~ΙRG||TYD{,^cv\3 @ oX1ciϭ?H`fd$;yta X. rs@x}^8 @"@0 7*aaҥJk70P+BDkAd$@$DsڱVm͋IH7*Xz     AQ4B XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XZ(C$@$@$@$@$@È9 XX{! 'v:C  $={̞y GT=a @T$RHْ @xsO̅HIa 1 DQF(>Qz "铧r %@- èQR9dw  "`!=% roTH\II& '@g+ x'J3b      robVHHHHHݻq_˟t;wȓO}$u$@$@$@$@$@$ ;hҒ:]F?5l,w;l۾CF-st[n͛7d tr_UD(&Nz%VH {^IEVz-+ᣇR{YC$@$@$@$@$@vnAsNJ{dRZM}j^`O(cČ#H-]J?{\iݪuwΞ>{O?SO;u6٣sHHHHH,rhJޢGҮMk:eԮUS^|\vUJ.']2Q#cav:Iμb|J?n6N]{SriYK6KҬekVyJXty9_eѢR|96t'rtSVRFPO?9- @U6m}'r 9{ŋsO?YrĎS<$͔=ٳ:Stz(k֕VKq$S\Y{'KeMRREA ԼFob゚*&O|㾔(>>:޽RIYYW(E8w\2e -ZIx$I$ž}u˗z e k-[ɯ3gɕ3GZ(Csfټq4mN;mty%RI)z֭Kg,*8~FNnN@ AYAJF$$Ar;l0 <3}ޙ[M/8-|*slR?gƂ8o"3qglPt{Y kTM,\ƍdf2ɝ;q=b\Mqݨk,![wl7۪_T1[ x&P~I&`4ׯ]>ҥJkʗ+keԪVBySs㴺Bh{En /9wNʤ}B I8\ڡ~7&Jdջ/wSm[m3X⟗LKٲ vÆf[|Y guii1bD+̞M` h~~*mt8&jԠsUhRin_66_|U Μ9[Zj# (O<5%~$O[[Rp!իAď?HҤ@?NC$@$@$@$@$@PWRY2V[$Il3e$)ۂXxm.m|'ƳdFEM҈!-ѣG1K*e )RZX+#G˭[5 Mb5zDQ+ 8,QtefKҐG$@$@$@$@W~^Z\Ik}tg nT`.A2Bw"b);ªXBY.7|8ߘ%Jh4SL!7n2a)]8d`{ݦ};|gy})H/_/jic3->5VX9w-,߶Nď/sgϐdI3kqUv 1$HHHHHu˷9/_>nᣇ:՛]vM~\jx\T\#GG_E nUwHQ5+Wa~}zIu.'$]t)b@GqW< Z2zLΞ9+3f `I /*э6;.5-~8q\ֻ~8>f͙(.VŻeHHHHH 5oSNZ7Rx1c @#@0Mx`-y~B$@$@$@$@$x[iؚJHHHHH^-ώ%I L@($@$@og WJ$|Tό%H Laj, UTs$ HB$@$@$@Bk=d"9      pCw, x*2 Cʡ;XHHHHH<CHHHHHH!@z,K$@$@$@$@$@BʡL$A$@$@$@$@$@r=%     !@C&      wP9t˒ r!a ;CeIHHHHHCP90HHHHHHTݡDz$@$@$@$@$@$!zDr$@$@$@$@$@$*cY     T=d"9      pCw, x*2 Cʡ;XHHHHH<CHHHHHH!@z,K$@$@$@$@$@BʡL$A$@$@$@$@$@r=%     !@C&      wP9t˒ r!a ;CeIHHHHHCP90HHHHHHTݡDz$@$@$@$@$@$!zDr$@$@$@$@$@$*cY     T=d"9      pCw, x*2 Cʡ;XHHHHH<CHHHHHH!@z,K$@$@$@$@$@BʡL$A$@$@$@$@$@r=%     !f+WɈQc{.n 2i.f1d/3fϴj? 'jmwHiC[x bswUN<%;w"%JK}Ν&aå`hkÏ؍7|չRxU4lݶܿߞn3J2fO/_ʔL۰=ǖ-_ak5 ת#mwe5kh#nϨ;>zud1}l)QuN R9ܰq<|P*+++U۷n;Ć!cƎWHѥ*fk׭ce޽+7EKc.ry)[>rTuh@y>|73fӥ=dv;w^=~l/zUsgc76O~.XXZnkO:m~iPC9uj~:>|`;)LUk?cABv? 9svWMY2Ek򷾒$NX5L'    p@Z+T wɢKKe 乷M9bp]%ϞycX$z}g;M\J2{Qƍ{Rf 3w.SY{^е)S 9˲e?!:BoĎ+ʈ5~ڶ[7o&ڌVŊ,G4> *Jjm U*WIG2gVb}#Lʖ6 UeMF)ڹIo׶ %,ڶi%U1XJc{>ݺ vN ,`J`mUNx"*쑣T>|L&… PGgRڪ-ݠXψ-\S1 !N|;MZyx뚿JThp !De;CXʒˌ> x`̘1})qi$r$`|RKd@6>g;]g. w'H(5{, VmQeu A y]Zx*#?ɣt Yp. m/B>C$5aoj9b $H{ ga{EPHH$@϶I :#PevHB(|tS2v<CZBH#Q9 HxH2*o8#F+% BneOl]cHHHHHH\r  @X"@Ѝ^l5F~X ).:tcI|Dr_b\WDiI2*WQVYk_}|[v1˺kyM&kՕ gean{2BT ͥ ڬ,zu bUM Ν* @\*|k͒=;wMڞLǎ MmnlߪJ=m޳`b+"2zxqtܭ<*Vf@:h?GVzOI}#p#5j f eq:1p}1_Js&7C~Ο˱>gs~3V իW+LhgY[?vsNm;ȷߵ5VǛnq~Unҳ9ֵG_٭sKʯ߸W^䛡!GR sVվX_|{Ͽ4}Eǹiv:r)ٱKc-SˏI?@%@-Z4]:ʭ۷eeA& [ _ 9sɀ%Ar)ڭV*+?HnZ/3MZÇhj\WY Q,YR6նn(Sx1YhܾuKjHϙ)kV.ܹsU\- ͢߿iKϕӱ;Fڼy$ vd@IDATW?)] ,7[V-_"&3VlGo'I?mlÆ21jPeo9ɔ9,\nEMK:_S&$(o{vȎma("}, 1QBKC>p.zկ/Էq~2:v0EBq&ʇ19uLiٴnڱ^x<+u{; m*I2VPVaJHō ̜3|CZBy}KolYȲ$|2t`_U@KK?Hԩd*u/IP >=HlY%Q5K&S]ne#H"u?T$@$@$@$"Br2e I.3+W1 >iq3gb#>k58.U`5nPHjW]ײg&cǖreʘkĸN_Hr!R0~V?.^Ac@OYlf⟗LWTXQvYp!ŴNr@IǵP7O:q k,K%_ -Lp4B RWS۷w]sUByss3k1Gx,‹COfޡ.(15Li[&.!Ueu[li}M9cV%%>ɢΏqc" U*-w\LVS8XZbz?˞!;1\gF}5!y+o,[O2KU$A2m,;g2`0Ue$1B<[ `0+jSܭtkU]KdhȞ$_:&٨~\ 1S+),lm;wM[NjRxQo9uM9   xR9t5ĉ$Xg:w`,fXIߩ n={5CWտ_9u $ӪM{BL͛556mjb8qb> ,Zu$Ic0gH/35xM_Rs|s/$٧T)R:f 9uUIpWu+cܨ)RsyrȬ˘CI렰ŰnRX&O5[ AZfͺ*3+߶nX{MoXQpBans Tvh(IJ[F<\V֗rX 0 Vl$@$@$@$6WnWWW2Ӄ " .U0nV *`C|)\zh)EtيҡcgYtQP&C ٲAXtҔRZ ߶6}!H$AȩS1rtMDY}W>dUk%!LђeW~-tZGjDH3 2dٳd& ޢ2QD'*nvɌ2((z} ի:nR\%yDii&y{k*!/V`s筑zVm7o&Sa:lN'(J`uM42cccƌa&]:f1-%~x?b oUZ-Ap(rRWeQO:-rK#*.]3gϩK_/^\S nL@VFc~@m$u]~sMJF$@$@$@PZցp Юֵubn&¨*-2eI|uop?CeZ$F~ߦtKP(j[=E劦pֺ:A5ׯ(=Ԃ A`,W… B %m[g,qzDr$Q$KTϘ)^S~_qk8ɯks꺖Fƍe\Z: _Rd cYEM˪kM@-(w : ڱ w-vrFQzFέ*H'~ylUe]{!uaR'<1WE-cOW/ euI2[# ZЮ7    u˷ىj~-=ԩR񘊛8 DtWzmdRUxӧO%oe1h*FP: E²=urA?Ձ^~]ď(tD[?ݻgtA!m].sv GձNWsپsΐ)b$_ʙ;mܿ@bŊN,r>N뺫S,cKAs)|H#;"BuN:qHJ]tYuϨh ;Ϋ%z7HP5ZH(JPCb&"6+'|r!:;`]ceWsXM`*.\%Vq6U+y/M8;ozĈwL$@$@$& L2HD~`kj &IXHHHH"@"vI^:_a"   '*#     pMClB$(/ !7Õ 3c S8G/K$@$@$@að:7 C 5_&#F[iXHHHHH6R^*uֿ13m?~⍵qt9k #R7l$]'Na$@$@$@$@$@@S_ҽWP2|E*T*ҧi>X,YW{q_q~ܞgtѳw_y=qn5j #էhgɐaM~Gc~4nܼ!pޫw?YUԾݳwһo0iT{̘5[}pv_ EJ,sKdŦ5>WDO}Ad)Jre      |Ӭl޼EG.9sm۶M˞[3hU$4<$˱ǥZ:&oҥCM|ߦϵ|9ke)$IiU#{9wԮ96nH#4Y.]ZvumoŒOwyyWcO>3bD&55VX~8|ʢ(g gG[ԨVU*Hʔ)lRa-'Y 6Kv?*ӤIϟ~.X@-x~R%yzz+Z|MHӏ]s\9%r()re}]1oR 9ʕ*_J/*3}?FDb s3>eMA xPz{F5@LQTArO)ղ!m5"F }ڈ5c1t֗xqUw4EjO|a)Z83_iA3f {ȑ#Vp̸d_3&LО.\8Ǐ}Bȑ"ۃ` ڝ1c)k&ce5g^^]3q.^LZ4k*ܑuZ%'2XعSSZфEB$@$@$@$@$YBr5'f\',#E  mu٭\t9iPlٺMl- ԓȤSi}xӬE+Aҿ5ƍ7_KhFWwӮ]:NDX_'ȑ9g7^ЩF (VTFV&߾sf]1ji(ny6Kz(S~Sת]. ׵Ձ9MW<VɄ I}|Yhq Q۬zkf;׈"J=߿o\B-jφ I *&η+ԱȞL.,^8Om(ud[i3ӯaA     PC Ժ( %4o'!     0GarHHHHHr @#@0M9L$@$@$@$@$@ P9τGHHHHHH r榜&     g#$@$@$@$@$@$P9 sS T3     s)IHHHHH?* 9Tܔs$@$@$@$@$@$CLxHHHHH*an9`     Oʡ&yDVFn߾fMFqAZd\kwS{oN˟,YWڹ drU_=Ch;w.],6liq9&Vu  'ÿ[}H#+T|Eˮ{La}2hP9~>mr!9q뀁e Aw`q1 esɲe 5^LGk={BjY{&tOʷi4UeI?"m;trǏEKܽ{OV P4BB޻'W*۶|n%+Tav&6o%KWؿ+n^}CGΊx .'Oy [,9;A-\vFvz3# e. M͍Ur%)Y\;P ׬#wldɒ٥sGiРIҹkwI&TX፴W:a٣J`e%ZhBKҰaɛmMn[O2K/*J=__ _ɜ %ϧ$J(nw={$Fn嬂@ծaܵG>(}kgys_!t`A^L^X6o)*ۼyw[j+n>H"mk!K4en޾-SLUו5rM)V4dIҫW_ٷsF ( 7Jl=O9L 'N+fL)W9Β)-m9+c._~[ď'U3+V1wŋW2-Kf,Kd^uLOsySI*4iP%L ͚3إ|aziӲnkmT*_F6n&GPLc%a„RBYɝ3)ҵG_Q|75F/$~) >bnZ)/^õQ6n*zvVwX*Ç/~VZ~>nt:vJqHϾ^6u*W\L2M`1}5JT9&LaKH Ȇ[L>/\4F-˚%Ƀ&LjpnԨVuj$zӰe&?n(cg;oSg/^v{4_T`oelyH <`Q߇F_JLڽ{ե{mDQrd*2 &HH Mc9.Cۢ udҋgϼ+WC{9ǰ+\bq0ɷrX,M#FirҦzBTVTU֮[/cŔtjSԥ)HE_/Y@,J(FqG-oӧO[vZnky._I֨5'lZ&7ij"E$ljc?7n\m38B}eTy{ -T/ܰvw)A1P/1MZrDz!,Ґ d칂Зʑyp?%>.X?. eO񈑣L7n>(kc}/q2ħ|2xb+ο+Wojs1N_ c^|ibkkuoQr\=wΜzC"0;ywJEW?/(ZX'N$c_׬ꜿJ_yT+/+ , I'*4g2ml+YF7J&ATRo`s^"Naj RD1{9 TXJ/IiݾY'YU!;0eګuȷ[nuJ7\=zlҗ5zPUAٳzCx 7o6e먒sJVۛ8Q"8(>RVކEi6dΨmǛ7cp6Y[uZ[(M%6_+2\/_ics;Ws ïsꇵ ;AFi-ZYg>t:_[?k, lך-zÒ+zX$ZԨudLq .?]4ihz[ؤ 7  .._֛?5%XYZP|yrVKyuIiTA2KH@&nNԪQUo֔m;=;M|# w@c6oMtRҩC;I?_XHb\X֦te_w׀Rz!=XȡcGmUqܼe^x^0Ee 7]zg2P-uj4yz MM;_5n$=^UV(+Pbfyepɒ*MV:c,|kq4vͷ-8oBY$^ޱYG6}šªHyZIƵ5YGj֮'&M'](O{}/i6knަUK)Q?cǎ#?i{aYhq<0Uk6|Ə)|1chU˖)%}* oN8^J,a.`> SmV& o瓟'Me1]^=zՋ[OGmO:"/[Xo<Ї23@mnlqBV_UZdI%U$ʖ*a>BMj ʹc ERUQIL8pS +ƚjn***1xX@r V3Yti3&ٹk)U=s.缩(7XaWf_}Q>a+~O~l"kH,@Zݼz^|0y9/a,]ܠ~~EeKAo</p N9r{.\P6 V1>9+vئ ?(;g\9k\Rh)Xs]Yc,.p ex ^b+0A(xPFvbzEz߲m ƒ?ޗrh:y",QW\zPR!.PX^*Z2m.hgΜ5 B|5厝p&Om2ed^DqcΟ3uQmsMFaUwI +tu7rsn/[Xjc%F]2֪}Wp罄Z%Ybɦ] n!!~ m̙=]9$.&Ғ]KZ@8ҧMkTfrkćtZP}zQn !Gh90 I\_W >9?Z!;Wƪ % "l% [(CX+c^ xAuuZ-R?Kw e\ߐmP[ s7Uuv1Z1"`yvV}Ɗ0wD5-+|#FtfZ Y}T)SJ,@ rӄI׀]YwJ

ʅ?[VJ/zk֨f^_}Xn:u*SۍrS.{o҂G/ ;Xɓ&Tz1Kۘ}%wRUW@ZYvu"n߹K* k vm]X# d)f 7r8Q]O!X^V&xru ̤2NVj@U~XYA@=} X\( ^vS ,k#mVlmNsh 9p,N!5K/]:|^?f<7 Xy/=_PwrQ{~VUW@7]mj2Hю?^PQ|f(@6oah3B\quGhyIk-166|OO׌ Ze5?|8{p]oOv>Z @J\E;CZ?gM /]ӈuT-Zܒ x2׷Q[kYvYv=6:d]KVzRH]Oq1y ę@,Y4e`$۶ E!52c*LuGlΉ6;/) A|]YG@QٳU8:wӀchEUt_+^];;վb!:(3f2ǪUo~B5c JJ~Ν(>>2쳼3f͖e+JevE>+A褐:i_~5Pyo7o7Hlֲ 6B:ZMm/0>ةq"wHXkuA^.`0}:sA4&,N5(y@W1jܽ<}q꺌8~n0h,nDdE$%W_eZiPȬX/YGm2_x aCRknP.jicX4XL{"MzUF ;A +9u4Q`r ~\ܣQQ}^m5zLW:sW`~q/7n6ߘ gAk3ۺ;#EwrHo7!(ݳ !~xLt_W?F ( v) ZC38[@HyvcȻ8/,Oy辈;k|c7c ot/kY{9umD;FYDEUD4 'U!\͞!+W4/" KψaefZ )\7up#qwQG!;3PcJ~4wGn6y^0wsHa\N$C4U7sa#UYZ}B/Yj:|5rI:djEc euaK~fB^ F}.QeV/Lۀl]s D҃hOB]fs s̮Qw(ߊc;aZ`l}̹O6~S4f*Io`G9q[I֎ղc_8oq^Ti,.X|_۶jn \]nf'.] A=!Ρv[+?uȱլWp>,.8~sTGd;o8Wty؆!r_Sz(S~ԝ>uڬs@9ܼuS\ne r+z;wJd]%~g籶eE[(>L޽g\fBjڸR,C@vumP^}օ50\W֧$\PY{=\>1Π%$8qBY j;Yn5<:+"u:ˇgU܅ ~\G5kTq=ƹ\@:9S1`}Mظw9$~>Y[x.3gyʾTXy% 8VntEvB?~פ+| R Ag~״>gyFH(eu֖cp6~(~X@T^s Y9zXJ-,G%Fحlf5 A!'v*:Kǂu;Dpz_۬(q f,ՅpG;wܩeQ 37 |vj?(|@hP=MTBw3$@$!Maj5?}̬!,O`? Ս.Zkj[C6eM.iSArt$@$ZPĘ]%PKP;hvH2el<;n= G$@$@$@$@$@$t+ $f!Lw }3\Y+ @ P9 >3 0Er$@$@$@$V P9 3q@0z 1 x90HHHHHHTݡDz$@$@$@$@$@$!zDr$@$@$@$@$@$*cY     T=d"9      pCw, x*2 Cʡ;XHHHHH<CHHHHHH!@z,K$@$@$@$@$@BʡL$A$@$@$@$@$@r=%   b@IDAT  !@C&      wP9t˒ r!a ;CeIHHHHHCP90HHHHHHTݡDz$@$@$@$@$@$!zDr$@$@$@$@$@$*cY     T=d"9      pCw, x*2 Cʡ;XHHHHH<CHHHHHH!@z,K$@$@$@$@$@BʡL$A$@$@$@$@$@r=%     !@C&      wP9t˒ r!a ;CeIHHHHHCP90HHHHHHTݡDz$@$@$@$@$@$!zDr$@$@$@$@$@$*cY     T=d"9      pCw, x*2 Cʡ;XHHHHH<* uˣGܞ%˖ˈQc.5fLe&<~XN8).σZ,Xܹ+,}˟.ɹB媂qf:ӿNcHHyFQmM!zGzDzoR VA"MDJo" w!3Jvowo=3K$@$@ >'+Ԑ+ZnA,F,[B6o".\A~̙3OF\zeȗ'ێ+WJ$"RAc V[J ̍-iGFFJMվ rȟw/iӖx0"L=vA43 <'ۇ5O%00@N8)/[ォ _p\vM>ëL"|WY۴(~; +ViJo7{.RگS~z2u{7s{jeg_%o<}K:wn/TX. H{|Dze:I>Rv7p^;:>c>Lo?̑Vm#G+5wô[02vkE@@C?M[<Lٰ&N?$@$@$@$@$@9Mɕ3tA&N"vEQE5,$s,:D$'^CfKR%ffzٽc% ٵ{O6eed J`~fr)4T[ak*4ow1={Zl{zlҲfr~Gʲتu;XiA13*wtn%KhwA bIpp2I&ޗ$II>qtm.#Gs-L!wXXD܉QF:ܼuSq9… w7ҿQ_??A]F)J*u*I6mt^u^ע|~5Y*]'ůut)Qr%z/_^I,9߷uC$@$@$@$@$=%s*X 6բY[:7a1CzV 'z>ubպu+ٱuݽ] v ~Rc\e?.e;Mk?+)RP@)Ri-vlBٵ}So͚rHY5aD־7f5ҰA}nZ2i'W.k2;`l] ja#bT\IM7X ?wyE}t_8Zh?86?1FMˆdέZH߰Q)@$@$@$@$@$p?8};\.eVzۤSnFlٲIR:z2eJ,lp=W._Ky,m)~z31Y5o(tVVCc`Ct)fy~2۠ԩSKt?Gϝ;o˰>"`a\ٳgu4XYMZXT烄ڵaY+kWeV T*h?BzuС4P*\z*2 O?#n̓;}|B WI*z [rۥYۼew2Q M $9#T+3G:vls ՛6nKV^8RDሻ2U*[{M(Y^jl Vn[k0۱-iҤ]\'    p sG/^VZU&*Sʝv5b\?8{+_U3BVc,*//_Nڷk+SOIL*ΧN MǮqBxBG8>6Bͼ 2vܗzbu;FW.Uj*q&æҤQ}*5N55m޵Sgܧ цIcJgn9X+( [… 7.Ք['D5f~͖5,uΤ5匚@Uk(Q_n)]Lq!q c eqHHHHϺXR`.C`ғӿ^={کB (M%דS@XZVڵm#0!1uc>LvڳGw[>H*uj׊o;e:ęef?5.q.IHHHE=ʚDFE! y>ʢuYΝL2EW^OrbMk$>ߗԤ6IƘ1z\ ҟ耠rۃ=ȶ ˕+ٖ-$Os(]NXz`^9 D0bEI.Sʎ5OQQQeU>7oT%sLp{EƭZ,o`J[;RG$@$@$@$ypr +C 3el͉0D>[}iCH.r.qpo#NUsZ* +KhlE:.bVM<'৬苸Z^uhKoKc     Oxġ'aJmԷ[YFڥ0JXO     (y,JԴq#F cHg'y M&     Fe$@$@$@$@$@$;(}XS     xh(ZfL$@$@$@$@$@C0}5z̧Ҷ}'HHHHHH 8`m;7nm wڷw}޾n&^Y|l޼E.\Y3?     x|Sʗpnfuv&o/DEEɖ-8ʔM앥 k$O$v̏HHHHH)ٲeܹs%C Cƍ8b|r\}c2v:qW4':|yʭ۷nRlYɘ1̞;_:}Z xS%햊xArdT: aPl*YJn  0UTZT-kV@͛\H%Tيuy摔*I4ZSHn_O&7,HHHHH9Pj!I-59 D ,fEz_H%qB5qdԈazEY ) {޽حz 8 f ~ר^lʕ+…Eȸ CZ#>ؾ֥5oZ{lҼ#*Ő {>)K,aoUժf'Eu×0T}2bђ$I]'~z߽8uCDQ{Uп``5r}΃HHHHHHBCCG"" a7$ wJpGUc9.+W.˳-[NbfiJ47 ę1%((R3 i߁<8J;2!2xbmN- nJaP3S     8xlayvZekwޣaq#1XgiҼV+=Yv|+{ 6hH{n|''O\RL)R?E    #sBɑԻo?}괌Ygm߱S9p8v6 G1cݦ; ,/ -ÞZٺmq%((H%?E    #Oq}zZ 0IOƍRuڻfzɞ#߰Q:{ L!&O}<83HBRcUM&^9^V.͗Ξ-NgyuA^8V 7jR/I*,\X> cApǮҹS} 1J .HHHHHQYqXd9_߬g`6blBqm:~XOc?{}Vx?]B\&=ae2qXộ_饫?-]UT󸊦תY]&\ABZn4mH1> S5jljY3N& iҤ޴sn\n^6QÇe%o ?nDW;yꔔ,YBW&9^*Yu{ܿW_vɆ䝷߼/6dΒEvج׮]S0رmcuRgFO4!^^۽]>v:9L̴q5yjeB~޽wfMd;o}1Cz$@$@$@$@$ 8رx_h!VvmZzy\Y *V( /s;}& z7c2nMD95}/^*F)zC:hCRp!ɗ/ZF[S*BMS\4W".gblV!U%u:^zuС'y|le+jw;wN- =v\Qb/Ctzef{c.% ˗ ӧƌW͚5$[V5hXuá:}zH.H1R++%Ɨ" Zr…y؈ђ-[6)]?$@$@$@$@$(>BFB67A<[ߩ\ZjA<<%,0% :;wf (#g}yrS78Yk9|P8PzسӬ6n$=_4a]3$z,zݷPCáF hNeKųiobŊjW̱"9sߤIC c+akٲ'&_|B\~tqt]ͣoذ^jL a1 رs}sZxeӉ՟'ӫT^ROJH7dИa,k&̛n;y\vC+=;iStRg#' @| U=2%GH:$5ʯJDabW&1q:DKP Sgll+5V9?)^kt>&";7μS D5c埓'+53|e]4M4L3w=n"eլ@J,a_0ktЋZΝKEObPRJ n/(&~{(+Ui*W+(j{ڮj4ec|9X{ub4۷k+lyzx$@$@$@$@$|rBDBfR5W֡V-Kd>v@Cx`aLcҥֽW{e~  %Sd UET F|n]O6Suc1l(`{5ϴg-ߟ{V"=ʖW;%nJm DŽ,U8  a:k4+ٳg1GY9Sog嬧&ұ_YPߥ̕+̋gۺ߰6lX_O3GY_-oZݻ1 265V1 U?MF-8^[}xص;H}HHHHbQj^ 婀O0`pM0lDcKW^O`"!5o?)тEEۯ].A:&id#ߩYe1cjݘ;'E256L\cҮS. ׋=쟰n& oV?e|9ĖΝW4x va5glZOs39O_Bڷv>Plyp? 8v$Ny;^KjN\DI K( j|(e1b;:mOau%M&_[cg^̲A 9wz]REN3v_3B<^& rTdXc "  xlšuU3vT"jUOc(w[sz[[k>,bdVbT8X0K t;HHHH[[V75$@$@$@$@$@$ jw+ i$@$@$@$@$@$@G0X2'     Y>u8 $cɜHHHHHHg Plױ$@$@$@$@$@$x(%s"     %@q]NJ @8L<̉HHHHH|šv+N$@$@$@$@$@G0X2'     Y>u8 $cɜHHHHHHg Plױ$@$@$@$@$@$x(%s"     %@q]NJ @8L<̉HHHHH|šv+N$@$@$@$@$@G0X2'     Y>u8 $cɜHHHHHHg Plױ$@$@$@$@$@$x(%s"     %@q]NJ @8L<̉HHHHH|šv+N$@$@$@$@$@G0X2'     Y>u8 $cɜHHHHHHg Plױ$@$@$@$@$@$x(%s"     %@q]NJ @8L<̉HHHHH|šv+N$@$@$@$@$@G0X2'     Y>u8 $cɜHHHHHHg Plױ$@$@$@$@$@$x|J;~L~g~ˢŋڶmz2rȄf/\ m۶ZQ+#GHnW_}%of%$ABxOyqM{wʪUz[be=yJbhG}$˗/͛'~˴̗ݻ2NbĹz:uJ~0T_"""#qoX/GB p|mڴ)Fr\bGĈv#xcd?NސsvHQ$"2*sfV$@ K֭]'SNUs^&ID/˕+'%>-!{쑀͊inăGTBeѣG_l5EH>e &}arS y'tqy72AE[9]|.j) 1I&RvXkq'&ӧw9sɛ7oͲHǸxlW|CO?޽[2flWOߝ;w>Bڵk'eʔq'6&ֹX1L0A+/`zrk&[d5|ĸj-[oI&9vZ\n:/;w,ɒ%3gmsmk9swncWJT-CJyMR2,SW$BvWkMo&%%/~H V>%aI2$XpCbmPR%ˇ|;ЬN .ǏlV2%yd^|D)СCRBDdl1cȆ dɒ/oD+Qx|CO?iSBڵK ~%4X'ֹkA qܬixdtx~~'\^xrڴitǎRn]{6ph߾"Xkn -JXLRK8z쨼+ 66aĈ2l0SΟ?/իW׮* [nr %!Kj-Fp7v~aYre1c,YDuaiժγc2p@y\y]FHH :Dc4}$W^\8qt1^zɩ+;vXmM3|R -\?oyamܸ~)Kg~W+oظQp\?\~Irw&?-%pU{po`4Ǥy)cqҥK&8pqtE XQ'cb;u9J7Wtƍ-[pya92v=`qNk ;9wAԣ-w\ӄe˖sB:p~b5;25eZ8q,|01Cq=1m>|p@ܱ{m6mEkO?2d ~rp VsG?[:-/3g䰴D7}tm}1Elqߴ,p0uk }`s !x#-΍خ&_,#>mww;ZBĊ+bDy~u>ww~{[9mWրɓI6ӨS\c\b⺄k"ǿ9b ^tCͤ!x*\y1rݭ4mKȬI7_OMX)#~/>Z%Y۽n!#~D{ֿY&]f\YPv]5i >j[wu?Ey:Y~;}YmWmF/y\s^옕I:}WHHبWHՐgiQ41n߶v nxUTY͒ {gKb&ppڱ}~zɛ yww? /Q~1 AFq ]x 쯄nre(*YnfW^E3,EBYcU{J*]V /_^7Ǹg'7|}LBS C/hO|懱jSN'dqcSoVj-͚Ieqx?~fn~H_I6[1k ^*ܹ޻I=p"Я(M .SJ׮]5 ߗ… :eʔI&uTYʺu&p5AnH댩5OO@B|%BŜ{k֬e=2l0my,#dݱ &8ά]B{Y-jשy5acy ڈ1\B =E9ri΋{LѢEebW|q@ozs; *gEGܹ^u?i{l罳;O,-]|Rxq}M0/ijdРA];"9Tf-d4RǽWս/p` 1طoTnk!g˗O5j!8I4Fy~FYqawJ'3Kt)tS-8ݑJm;\Y,-3ghvrZGzT/$֕J} SO^'/^SyۆD7Hu|YȳJu2pi)cjפ,|+[27/גw!~o,5i䧁$_Tzی2˒ Uo|'OgK+7P ?ڌ K#"#۔M D}-\O>p9ŅOlO:~H7ؗ'Oٺm(X@ 7 Khw9k,7a9ʜ9 ]Ț5d``".3<#IK)5k]/ꍷg7>-"BD ^F CdɒnEEx0|־d 7u<,@t@ apq鏺 _2 iҤu 4}z&͸d>Hu_"nfuW_-hpllQ !N13gpsܵ^ztRm Bp!,ϰ{i Bm(߶fzqZha۵UX@?~o2<8[؃zět8ƺĹ!?v>]TV8Ÿc2?-u],c8y F5;gn޼* bL5b/[/Yu<<,#FtIDZ2,{qDg/p\z7ؠ~Q]Ôm6}4(ڹKV\yuo'ձFeYѳ_SL!o xC Lkx!}b{1rή+ּⳎss喧~ZO41ebC Q뢹ň~ĥ8qpw[낗kҥK!alӶ>OB\$B J*qK넖?8pA\3%}s {}Ê{+h/Cgbb*,qaܹVU*"-+sݤٴtd$SⲓpЪҽrAOːy;ebJ(1֭ETx& kYF/,9^RVJH'by*KjIή,79E޵dNwMjA_n%:?kg{i2{2wFyA1eNj>-oڪۮyW`̓ I ROB 0e [2a-pC8m߶]Y i V!AT~[:nxyѬLRl`1V4<"WK VGw}7K3ޠ`oܕ fcX)a3 ;D9wDrY2.wL񀩭G$IDAT.gXCmtU̵ q^C\=c TTi{Tn7u؆6 E0B0UT7Wt81bRݹf*h[qmyZbYW:/^Fh&3#.]^qTu7u6`=9sxщ .X: qkykMuWC\p.Z%K/yYY0YR~xpt9m;c(3}S)v7:D?nU im3C,䤲:"WЄ Ţr3Yw͛578(P2 צNbzOgה5)aN' 8ՇSb s w,^J9sZ6y +x?<\q659 4 !<V/Zٖ1`L80=BZn/%u,0oqsLhk::,\c3giWZum7mkjpenL^&O~Y[٬Y2ۄ,>ތÊ m>` InaɄoƜp4}fh?w2oǢa|3pu FB~lwLD[Moizg}3LX]Pͺ9b#Ėgd`1f&Lwr\O8=M_:OڨýcuI Zw_j߆8Fk)3"uy\7.:cm˲1[-{6FÕc%X^v3]Wܴ8ϖ&es(̔2훎$Q"MπشG}A}Z8[Ϫ{=qA$ &4`8eY?\ ^lNhLO^M+b},>z "oB5jf 7Ӎ%:5qC4c{Lyxă!q3I;5l[`aD[RPkk`nZx3o!("VZr߰VTyZO55X1./o{Gu{Ju)Q﫱`ٓq}o mnpwt~4YZıjڎ{ 'Lxyv#v.ǧ x;Q W牳^ ф1Ȏa18Fj*K)Uv 8g/$opF_c\UalVϢ/yl WC\;PW\ /4煀)j+a䀚oҲTe|rŤ3:(ΉRoW+U[ݐ?&yo˾\縌_{H{YMjYV`L\LT$2U>SM{Cxz"̔Z* ՛u>kkj3fb9qh#s )ނWRU?&'pfeCZ|,ŋYUZ˶; Mp=kؠ~# 13y "6gތ5ooVx@ 35jHO<\yLfil[`"&1㦬Me׶M[m-2cA.pr6y9' xmVwaXYa%ަθYg♺ x 1i݆B*#_rLkYn` 1+y`tjc[tLFxc7%\1~ ƛoxXәukֶ7x #\?燇{&}zk\1ff`";Qd3ww0[leKe+xQ©ɥ̱uqp¸c?k=1m?ܡ`ǹ4K[*_ \p݃8qe 1\c,*=<lg] w5UK8V? uƷkN|Qݺvٸ;Q';n3lZ7Ǵ}uw,ݝ' }\p LQO ˯>xNFٮfp /p?{+s=~Ag3bRÀp΀FDawfK3dª(ی[ K5˒elӨ̜*`,G v_N)WPZCZOgߞ-s6)㻇hWkg)exU 4OYI+&#'>sRmr$gTRݹzkj⛞5t?r?a ?gdZ)w"3͓X=M}̇x@q QҤIc<|$[ ?X^p#JH^x Zڄ`Du8ɺ:CxIv1ގc|1F O|;%Ic db0 ׇU $+%!ä)ce[  q~|ݝq]plbsȔkm+禎?,xpCm\2 ( ae;IP8_e-8:ƏogcرL\p<8uqwX OVXwwp箎7?ʆ 3&N\8P{uk^\{9\iSlb5kgIr~3f7m5GuL{ɓM2q|[&wp,;ttV3YX. N9dI%i`=\ѓ_#n!M;1ONH_GB&NFiK ![0&]p4}E l̛twwGY74O|a2%XleħuϱLg]sv8Kj[l}uw~<'̈́0I5uswn/ yA[=.7m*$xpno]ܝ':8̺yl[\6um۽5q !}.\Uc:|SCL$3_lH8 C{ꈉlVpUw ÇEz-ϲL3k"\@ypi)N2cℎOMn|Ǘ<'VJҸxN)\CuG?/ps5W||HX)q 4Ä46VHHHHHH84$$     ǘcl: $@$@$@$@$@$8|;M'     CАHHHHHcq$@$@$@$@$@$`P\ cL1|6HHHHH CCK     x P>ƝϦ !@qhHpI$@$@$@$@$@1Ǹt     0( .IHHHHH1&ɶ;~̓ųl      hAI $@$@$@$@$@$@^BC?G^ g5HHHHHHzFa ]4$@$@$@$@$@$<|xa$@$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ @q,HHHHHšC$@$@$@$@$@ 2E.YDFܵog HRdse<[N$@$@$@$@$H F= {q:YqL+*Lg$etFԵF$@$@$@$@$$1e[HMam?V7ևHHHHH8Hxh cN@     LJØ-%     '@q] '@q     zŃЉ 燉S<1)fTEq,LC$@$@$@$@$ >#p.Q7Kݻ,X&Gׄ %Q4t"AA:DEjQi.tE}j[˨ڕ$A$@$@$@$@$@$>V y$uIz+"zԐ%FK3-8B(Cڔ~ajY%yW"]W.P߳lmJ@$@$@$@$@$@@L^*ˁu-R ~MJʐg+Ir #]>-/_}HT,$KI"PcjB)P_EPD15rgemrlrN5,-K%?Q3:tAea yePRe0eŚ2Ytl.y|VMn7ZIު>7a٧L*G+tpõՖIHHHH׋C=;eP r)oFھN ܳnR^ƮUY$r9KTSȰk"wLj[D)TX!Eb\U! *:Sh nߒOe{i(nkiRIɘ.XBaYTĨWmu BR:\\SH)[jK5Pt"M?UyϠrM"U{Un~I">meލ?U_PJU"-vťΑHHHHHHFš2H`l󌮵_ݺr=Y,S_o*!K&I@,J>/{/u%EP7gpOHEEȓOe/yD3N_(*[I YSȢ/iRH;8w[-ynIhO ~YK#%rgyb[ȐեXRa7ҾV1>D:)Kw-ȫSV_T. \}]dJ#xTٯqi:zl9jXR4.+R%3Wnȫ߬Tiu? @LIo\5G/IȶOK%i,tH4xd61WҺ|AV*L[OM!fR4;Cj%"Ȅes~R9tꂌRK8F5Q0ͼ[9+Coޒ&7nQin!$=&n*>%51 w[iغ WN_g/˩KץwULIDL4P$O*senՊH̩XrfH%_-c 3T²͔#g.׽ꋪu"    (~ڕȔ5%o2k-u_\62)D)7~;\z*.[o4+P[ r2UE-:|H6GXwFeìJIzPɲQ_\2frqf?yy95𪴨\X)qkJ2rj_JBCɑ/+(٥,W#ȱr)\y[oc+~4D=l6z|X:ګ~IzzG**36w^+goؓ޼mY%>*D/[i:5w\!    xtޭ6F0VJlielҸd>BEu j6|6eζ#jBJTݕe*Jɕ16/ @?DA$*͋a9je'՘ǡ+տHt Wќh`,|LAm۶ ! =-I: +}'=lX oI2|vרK 6˔U_ԓ s^ H}W~%u %?Y/;YKS 7JgmD`y2{r JiWب& 3tڌTFK?^&IksYC;jǪd 0j߆Ha5JQ"PolD zb+d\V65m"8 Ƣ%ϞgLmPGկ]"ѩUjfm HggB哎5$>- 8nq]Ki+K%c\$*W@eE&I&`wpAY/\!]j|!T'O!̣wZK,ir]iXv{M V}rϘ5N$rgo4Zu-1s5K"7ľM Ϋ2U%TzJU*{rM-Oͬqp+u)ováԘBMf2wCyY9{Og'詋j,eޗ6i{d "O} aڌ528HV a* jwkKe0q/HB955 Pq+O'گ%O%廥Q|ri2_FlaeSB +q"]Cf)XhnuLw9dOJo\!%DUl}T/R3I[2給2su w#:Kst,Q6S_&v&|ӱii>Qx^yCÄ-to'db::uJ;tV9uQMҪLT.IHHHHH@ QsbjBM´jѿi\ {\yC Côz5WLĚ%0PrzGM&*;̟.\.Qx%T0_z%L.+˟p ]D!_$mrEW>V>{uFdDǦVy)$QV|CblsԭM̶3o(F^<'*^uʩeuk22NYF˼3S:N\*Y`;nV^83EHHHHH?E'm9I^8kJ(^%+ r'U.2Eo)ɀa`3mշ -۴LIK%%V0:Oq$AZaRq{)U|{TVkj,FOYO4g&wUYQ~w8DexDeiTy&=}*?SVe¤2]#׬vW}&G&9p1"f[Y{$@$@$@$@$@$>!QQ`SBP(=.poeeՙ{bGB8 i^Y a%E$@$@$@$@$|S.D9"&JVGDk3"    x(9D,cz&b̊HHHHHq"l JD]NֈHHHH"C?gNR >}! /     LJJ[T.(7.wO>0F[ [zv$@$@$@$@$@Jc!!)$jك ω)IHHHH|_hh(@$@$@$@$@$@$Ǹt     0( .IHHHHH1&@qw>N$@$@$@$@$@š!% <(gIHHHHH84$$     ǘ.LIENDB`readme.txt000064400000031777146726766340006606 0ustar00=== Klaviyo === Contributors: klaviyo, bialecki, bawhalley Tags: analytics, email, marketing, klaviyo, woocommerce Requires at least: 4.4 Tested up to: 6.5 Stable tag: 3.3.5 Easily integrate Klaviyo with your WooCommerce stores. Makes it simple to send abandoned cart emails, add a newsletter sign up to your site and more. == Description == [Klaviyo](https://www.klaviyo.com) is a unified customer platform that gives your online brand direct ownership of your consumer data and interactions, empowering you to turn transactions with customers into productive long-term relationships—at scale. Because the Klaviyo database integrates seamlessly with your tech stack, you can get the full story on every customer that visits, and then—from the same platform—use those insights to automate personalized email and SMS communications that make people feel seen. With Klaviyo, it’s easy to talk to every customer like you know them, and grow your business—on your own terms. Talk to customers like you know them. Because you do. ####Sync all your store data with a single click Our seamless one-click ecommerce integration allows you to sync all your historical and real-time data, so you can stay on top of every single interaction people have with your brand. ####Automations help you make money while you sleep Dozens of built-in automations are fully customizable, like welcome emails, happy birthday, or abandon cart. Each can have any mix of emails and texts. So while you’re dreaming up your next big idea, customers are automatically getting timely, actionable info. ####Smarter targeting. Deeper personalization. Bigger payoff. Drive more sales with powerful personalization. Whether you’re sending a drip campaign, a transactional email, or a special holiday campaign, ultra-relevant content can help you boost engagement—and earn more revenue. ####Get more answers out of your data Data is powerful—but only if you can find it, understand it, and act on it. With Klaviyo, performance is clear. Pre-built reports answer the marketing questions that matter most. Go beyond vanity metrics and understand what’s driving sales. (Really.) ####Benchmarks See how your performance stacks up against peers. With Klaviyo, you get relevant, real-life benchmarks based on real-time data from 90,000+ brands. See how you compare to businesses of your size and scope, and to your overall industry. You always know what to do to improve. == Installation == Integrating Klaviyo and your WooCommerce store is a simple process: 1. Install/activate Klaviyo's plugin. 2. Click on Klaviyo in your left-menu, then click Connect Account. 3. Click through two screens to approve access and finish setting up your integration. For detailed instructions on integrating Klaviyo and WooCommerce please visit our [Help Center](https://help.klaviyo.com/hc/en-us/articles/115005255808-Integrate-with-WooCommerce). == Changelog == = 3.3.5 2024-03-25 = * Added - Notifications for common setup issues from settings page. = 3.3.4 2024-02-02 = * Fixed - Fix deprecation warning for dynamic properties. = 3.3.3 2023-11-15 = * Fixed - Fix newsletter checkboxes validation. * Fixed - Move Added to Cart 'value' property up a level. = 3.3.2 2023-11-09 = * Fixed - Fixed tracking on checkout pages that show the shipping form by default = 3.3.1 2023-10-02 = * Fixed - Update events call payload to have brand parity with V1/V2 APIs = 3.3.0 2023-09-27 = * Added - New Klaviyo onsite object * Added - New X-Klaviyo-User-Agent to headers to collect plugin usage meta data * Added - Added support for V3 APIs /client/events and /client/profiles * Added - Added user tracking support on both shortcode and block checkout page types * Removed - Support for V2 APIs: /track and /identify * Removed - Removed _learnq onsite object in favor of klaviyo object = 3.2.2 2023-08-22 = * Changed - Klaviyo settings link nested under Marketing tab if WooCommerce is installed. * Deprecated - Deprecate getVersion method in favor of get_version to meet WP convention. * Removed - Role creation and removal during install/uninstall. * Fixed - Only include WooCommerce Checkout Block compatibility if StoreApi is supported. = 3.2.1 2023-07-06 = * Fixed - Add `paginate` arg to order query to access total order count. = 3.2.0 2023-06-27 = * Added - Checkout block integration to support consent at checkout for email and sms. = 3.1.0 2023-06-20 = * Changed - Update Klaviyo API orders routes to use HPOS compatible query methods. = 3.0.13 2023-05-15 = * Changed - Added utm parameters to Create Account url on plugin settings page. = 3.0.12 2023-04-21 = * Added - `kl_added_to_cart` hook which enables developers to edit the `$added_to_cart` payload * Added - `kl_started_checkout` hook which enables developers to edit the `$started_checkout_data` payload * Added - `kl_cart_rebuild` hook which enables developers to add/remove products from the cart before it is rebuild * Added - `kl_cart_rebuild_complete` hook which allows developers to easily trigger a callback on successful cart rebuild * Added - `kl_checkout_item` hook which allows you to add additional properties to the line items that make up Started Checkout Event = 3.0.11 2023-03-27 = * Changed - Bump WP tested up to 6.2 * Fixed - Rename function to avoid conflicts = 3.0.10 2023-03-14 = * Changed - Replaced deprecated FILTER_SANITIZE_STRING functionality = 3.0.9 2023-03-09 = * Added - Added `Stable tag` readme.txt management to changelogger. = 3.0.8 2023-03-03 = * Changed - Add filter `wck_should_add_started_checkout` for adding started checkout logic on pages. * Security - Modified the `get_klaviyo_option` method to prevent script injection on the admin page. = 3.0.7 2023-01-24 = * Changed - Updated "Tested up to" from 6.0.1 => 6.1.1 = 3.0.6 2023-01-03 = * Fixed - Undefined categories in cart rebuild. * Fixed - Added ProductID to viewed product events for better integration with product recommenders. = 3.0.5 2022-12-08 = * Fixed - Prevent automatic integration removal. * Removed - Sending webhook on plugin deactivation. = 3.0.4 2022-10-20 = * Update Removed product description from the kl_build_add_to_cart_data method to reduce the size of the payload. * Update Started Checkout events not working with TT2 theme * Update Use POST instead of GET when sending through Added to Cart Event. = 3.0.3 2022-04-12 = * Update - Query only for product post_type at klaviyo/v1/products resource. * Update - Use get_home_url() for url query param in auth kickoff request. = 3.0.2 2022-03-28 = * Update - Assets for brand refresh. * Fix - Undefined index warnings in cart build. = 3.0.1 2022-02-07 = * Fix - Remove redirect after update/install. = 3.0.0 2022-02-07 = * Add - Options endpoint supporting GET/POST requests. * Add - Improved validation function for custom endpoints. * Add - `is_most_recent_version` key to the response from the /klaviyo/v1/version endpoint detailing whether plugin update is available. * Add - Webhook service for outgoing requests to Klaviyo's webhook endpoint. * Add - Redirect to Klaviyo settings page after activation. * Add - Deactivation logic removing options, webhooks and sending request to Klaviyo to keep integration state aligned. * Add - WCK_Options class to handle deprecated options and adjusting via filter. * Add - `disable` endpoint to handle plugin data cleanup and deactivation when removed in Klaviyo. * Update - Updated plugin settings page allowing for management of settings in Klaviyo. Maintain original for non-WooCommerce sites. * Update - Use __DIR__ to define KLAVIYO_PATH constant for test compatibility. * Fix - PHP Notices on admin page when initial options are not set. * Deprecate - Removed 'klaviyo_popup' and 'admin_settings_message' from `klaviyo_settings` option. = 2.5.5 2021-12-09 = * Update - Support for Synching Product Variations. = 2.5.4 2021-11-10 = * Update - Default SMS consent disclosure text = 2.5.3 2021-10-27 = * Fixed - Over representation of cart value in Added to Cart events. = 2.5.2 2021-08-10 = * Add - Support for Chained Products * Deprecation - Displaying Email checkbox on checkout pages based on ListId set in Plugin settings. This will be displayed using the Email checkbox setting on the Plugin settings page, as done for SMS checkout checkbox = 2.5.1 2021-07-23 = * Update - Adjusted priority of kl_added_to_cart_event hook to allow for line item calculations. = 2.5.0 2021-07-12 = * Add - Added to Cart event. = 2.4.2 2021-06-16 = * Add - Use exchange_id for "Started Checkout" if available * Update - Lowered priority of consent checkboxes to address conflicts with some checkout plugins = 2.4.1 2021-04-14 = * Fix - Address console error faced while displaying deprecation notice on plugin settings page. = 2.4.0 2021-03-17 = * Add - Class to handle Plugins screen update messages. * Add - Collecting SMS consent at checkout. * Update - Refactor adding checkout checkbox to allow for re-ordering in form. * Update - Plugin settings form redesigned to be more intuitive. * Update - Enqueue Identify script before Viewed Product script. * Update - Moving to webhooks to collect Email and SMS consent. * Fix - Remove unnecessary wp_reset_query call in Klaviyo analytics. * Fix - Move _learnq assignment outside of conditional in identify javascript. * Fix - Assign commenter email value for localization. = 2.3.6 2020-10-27 = * Fix - Remove escaping backslashes from Started Checkout title property = 2.3.5 2020-10-19 = * Fix - Remove escaping backslashes from Viewed Product title property = 2.3.4 2020-10-01 = * Fix - Remove unused import. = 2.3.3 2020-09-25 = * Fix - Cart state issue with rebuild when composite products are present = 2.3.2 2020-09-11 = * Fix - Encode non-ascii started checkout event data * Fix - Handle checkout without Klaviyo cookie = 2.3.1 2020-09-08 = * Fix - Update to fix fatal error for websites not using WooCommerce plugin = 2.3.0 2020-09-07 = * Update - Removing all external javascripts from the Checkout page = 2.2.6 2020-09-04 = * Fix - Update to add permission callback for all custom endpoints (Wordpress 5.5) = 2.2.5 2020-08-20 = * Fix - Rename undefined variable = 2.2.4 2020-08-05 = * Tweak - Update to be more defensive around global server variables = 2.2.3 2020-06-23 = * Fix - Identify call in checkout billing fields = 2.2.2 2020-06-11 = * Fix - Check for checkout variable * Fix - Resolve register_rest_route_warning * Dev - Increase max WP version to 5.4.2 * Dev - Increase max WC version to 4.2.0 = 2.2.1 2020-05-26 = * Tweak - Small update to legacy signup form widget = 2.2.0 2020-05-14 = * Fix - Custom order and product count method = 2.1.9 2020-05-12 = * Fix - Security fix = 2.1.8 2020-04-24 = * Dev - Refactor API code for unit tests = 2.1.7 2020-01-28 = * Add new authentication for api = 2.1.6 2020-01-27 = * Fix - Revert authentication patch * Fix - Making sure characters are encoded correctly on signup success = 2.1.5 2020-01-22 = * Fix - Improve authentication for custom api endpoints = 2.1.4 2019-12-04 = * Fix - Check index is set for subscribe checkbox during checkout * Fix - Move klaviyo.js script to highest priority in footer and add missing single quotes around src = 2.1.3 = * Fix - Deactivate old Klaviyo plugins if active * Fix - Check if Klaviyo Settings index exists * Fix - Pluck product categories only if array = 2.1.2 = * Add support for latest api version (v3) = 2.1.1 = * Check for existing Klaviyo plugins avoiding incompatibility = 2.1.0 = * Move all javascript to external files * Compatible with just WP = 2.0.7 = * Add widget for Klaviyo's built-in signup forms = 2.0.6 = * Be able to customize CSS for forms * Fix issue with button text display = 2.0.5 = * Remove signupform js as it's included in klaviyo.js = 2.0.4 = * Add klaviyo.js = 2.0.3 = * Escape quotes in product titles = 2.0.2 = * Use new endpoint for checkout subscriptions = 2.0.1 = * Compatibility for PHP 7.2 and remove PHP warnings * Add persistent cart URL for rebuilding abandoned carts * Add support for composite product cart rebuild = 2.0 = * Bundles the Wordpress and Woocommerce plugin together as one. * An option to Add a checkbox at the end of the billing form that can be configured to sync with a specified Klaviyo list. The text can be configured in the settings. Currently set to off by default. * Install the Klaviyo pop-up code by clicking a checkbox in the admin UI * Automatically adds the viewed product snippet to product pages. * Adds product categories which can be segmented to the started checkout metric. * Removes the old unused code and functions. * Updates all deprecated WC and Wordpress functions/methods. * Removes the description tag from the checkout started event. * Captures first and last names to the started check out metric. = 1.3.3 = * Updating docs. = 1.3.2 = * Tested for support for Wordpress 4.8. = 1.3 = * Added HTTPS support for embedded form. * Updated logo branding. * Updated links. * Updated previously deprecated functions. = 1.2.0 = * Updating to allow embedding an email sign up form. = 1.1.2 = * Updating docs. = 1.1.1 = * Fixing documentation a bit and one bug fix. = 1.1 = * Adding in automatic tracking of users if they log in or post a comment. = 1.0 = * Initial version includes/class-wck-webhook-service.php000064400000005751146726766340014101 0ustar00 array( 'X-WC-Webhook-Topic' => self::TOPIC_RESOURCE_CUSTOM . '/' . $topic_event, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( $data ), ) ); // Klaviyo's webhook endpoints almost always return 200 with a body of "1"/"0" corresponding to success/failure. // It's possible to get a 503 response in the case of a larger issue unrelated to content, formatting, etc. or a // timeout if the request takes longer than 5 seconds. if ( is_wp_error( $response ) || '1' !== $response['body'] || 200 !== $response['response']['code'] ) { // TODO: It'd be nice to eventually log this failure. return; } return $response; } /** * Send webhook with topic 'custom/options'. Data contains all options under 'klaviyo_settings', * the plugin version and if it's the most recent plugin version. * * Set email/sms list ID values to null if no ID set. * * @param boolean $is_updating Whether the plugin is being updated. */ public function send_options_webhook( $is_updating = false ) { $data = array_merge( WCK_API::build_version_payload( $is_updating ), WCK()->options->get_all_options() ); if ( ! isset( $data['klaviyo_sms_list_id'] ) || '' === $data['klaviyo_sms_list_id'] ) { $data['klaviyo_sms_list_id'] = null; } if ( ! isset( $data['klaviyo_newsletter_list_id'] ) || '' === $data['klaviyo_newsletter_list_id'] ) { $data['klaviyo_newsletter_list_id'] = null; } return $this->send_webhook( self::TOPIC_EVENT_OPTIONS, $data ); } } includes/class-wck-options.php000064400000006613146726766340012476 0ustar00 true, 'klaviyo_popup' => true, ); /** * WordPress option key for plugin settings. * * @var string */ const KLAVIYO_SETTINGS = 'klaviyo_settings'; /** * Klaviyo plugin options array. * * @var bool|mixed|void $_options */ private $_options; /** * Constructor */ public function __construct() { $this->_options = get_option( self::KLAVIYO_SETTINGS ); // Add hooks when settings are updated to ensure instance variable is up to date if no page refresh. add_action( 'update_option_' . self::KLAVIYO_SETTINGS, array( $this, 'refresh_klaviyo_settings' ), 15, 2 ); add_action( 'add_option_' . self::KLAVIYO_SETTINGS, array( $this, 'refresh_klaviyo_settings' ), 15, 2 ); } /** * Gets the specific setting value from the klaviyo_settings option. Allows for deprecation of settings by first * checking if the setting exists in the db, if not will check if the setting is deprecated and will expose a filter * so customers can still adjust this value if absolutely necessary. * * @param string $option Name of sub-setting which is a key on the klaviyo_settings array. * @param mixed|string|void $default Default value to return if option value isn't present. * @return mixed|string|void */ public function get_klaviyo_option( $option, $default = '' ) { $option_value = $default; if ( isset( $this->_options[ $option ] ) ) { $option_value = $this->_options[ $option ]; } elseif ( array_key_exists( $option, self::DEPRECATED_SETTINGS ) ) { /** * Allows for "setting" Klaviyo option if deprecated. * * @since 3.0.0 */ $option_value = apply_filters( 'wck_option_' . $option, self::DEPRECATED_SETTINGS[ $option ] ); } return htmlspecialchars( $option_value ); } /** * Refresh the instance variable when the 'klaviyo_settings' WordPress option is updated. This is hooked into the * `update_option_klaviyo_settings` hook which is dynamically called in WordPress' update_option function. * * @param array $old_value Unused - this is passed along in the hook. * @param array $new_value The new value to save to the instance variable. */ public function refresh_klaviyo_settings( $old_value, $new_value ) { $this->_options = $new_value; } /** * Gets all the settings values from the klaviyo_settings option. If not set returns an empty array. * * @return array */ public function get_all_options() { return $this->_options ? $this->_options : array(); } } includes/wck-added-to-cart.php000064400000014747146726766340012317 0ustar00 false, 'headers' => array( 'Content-Type' => 'application/json', 'X-Klaviyo-User-Agent' => kl_get_plugin_usage_meta_data(), 'revision' => '2023-08-15', ), 'body' => wp_json_encode( $payload ), ); } /** * Set wck_cart data then build the Added Item and return the array * of the full cart data. * * @param object $added_product Added product data. * @param int $quantity Quantity of item added to cart. * @param object $cart Cart data. * @return array */ function kl_build_add_to_cart_data( $added_product, $quantity, $cart ) { $wck_cart = wck_build_cart_data( $cart ); $added_product_id = $added_product->get_id(); $added_to_cart = array( 'value' => (float) $cart->total, 'AddedItemCategories' => (array) kl_strip_explode( wc_get_product_category_list( $added_product_id ) ), 'AddedItemImageURL' => (string) wp_get_attachment_url( get_post_thumbnail_id( $added_product_id ) ), 'AddedItemPrice' => (float) $added_product->get_price(), 'AddedItemQuantity' => (int) $quantity, 'AddedItemProductID' => (int) $added_product_id, 'AddedItemProductName' => (string) $added_product->get_name(), 'AddedItemSKU' => (string) $added_product->get_sku(), 'AddedItemTags' => (array) kl_strip_explode( wc_get_product_tag_list( $added_product_id ) ), 'AddedItemURL' => (string) $added_product->get_permalink(), 'ItemNames' => (array) $wck_cart['ItemNames'], 'Categories' => isset( $wck_cart['Categories'] ) ? (array) $wck_cart['Categories'] : array(), 'ItemCount' => (int) $wck_cart['Quantity'], 'Tags' => isset( $wck_cart['Tags'] ) ? (array) $wck_cart['Tags'] : array(), 'extra' => $wck_cart['$extra'], ); /** * Allow developers to customise the payload before it is sent to Klaviyo. * * The `kl_added_to_cart` filter allows you to add additional properties to the [Added to Cart] * (https://help.klaviyo.com/hc/en-us/articles/360030732832#added-to-cart2) event. * * This example below shows you how to add custom fields to the event * * add_filter('kl_added_to_cart','kl_modify_added_to_cart', 1, 4); * * function kl_modify_added_to_cart($added_to_cart, $added_product, $quantity, $wck_cart) { * $product_lead_time = get_field('leadtime',$added_product->get_id()); * $product_designer = get_field('designer', $added_product->get_id()); * $added_to_cart['LeadTime'] = $product_lead_time; * $added_to_cart['Designer'] = $product_designer; * return $added_to_cart; * } * * @since 3.0.12 * * @param array $added_to_cart The Klaviyo added to cart payload * @param WC_Product $added_product The product being added to the cart * @param integer $quantity The quantity of the item being added * @param array $wck_cart The entire Klaviyo cart object. */ $added_to_cart = apply_filters( 'kl_added_to_cart', $added_to_cart, $added_product, $quantity, $wck_cart ); return $added_to_cart; } /** * Check that the Public API token is set, build Added to Cart event payload, * create an options request array using kl_added_to_cart_options function and * send the request. * * @param array $customer_identify Identifies the customer based on email or exchange_id. * @param array $data Cart and AddedItem data. * @returns null */ function kl_track_request( $customer_identify, $data ) { $public_api_key = WCK()->options->get_klaviyo_option( 'klaviyo_public_api_key' ); if ( ! $public_api_key ) { return; } $metric_data = array( 'data' => array( 'type' => 'metric', 'attributes' => array( 'name' => 'Added to Cart', ), ), ); $profile_data = array( 'data' => array( 'type' => 'profile', 'attributes' => $customer_identify, ), ); // 'value' should be a key under 'attributes' $value = $data['value']; unset($data['value']); $attributes = array( 'properties' => $data, 'metric' => $metric_data, 'profile' => $profile_data, 'value' => $value, ); $atc_data = array( 'data' => array( 'type' => 'event', 'attributes' => $attributes, ), ); $url = 'https://a.klaviyo.com/client/events/?company_id=' . $public_api_key; $options = kl_added_to_cart_options( $atc_data ); wp_remote_post( $url, $options ); } /** * Set customer identity, call kl_build_add_to_cart_data and then call kl_track_request * to trigger the event. * * @param string $cart_item_key Unique key for item in cart. * @param int $product_id ID of item added to cart. * @param int $quantity Quantity of item added to cart. * @returns null */ function kl_added_to_cart_event( $cart_item_key, $product_id, $quantity ) { if ( ! isset( $_COOKIE['__kla_id'] ) ) { return; } $kl_cookie = sanitize_text_field( wp_unslash( $_COOKIE['__kla_id'] ) ); $kl_decoded_cookie = json_decode( base64_decode( $kl_cookie ), true ); $has_exchange_id = isset( $kl_decoded_cookie['$exchange_id'] ); $has_email = isset( $kl_decoded_cookie['email'] ); $customer_identify = array(); if ( $has_exchange_id ) { $customer_identify['_kx'] = $kl_decoded_cookie['$exchange_id']; } if ( $has_email ) { $customer_identify['email'] = $kl_decoded_cookie['email']; } if (!$has_exchange_id && !$has_email) { return; } $added_product = wc_get_product( $product_id ); if ( ! $added_product instanceof WC_Product ) { return; } kl_track_request( $customer_identify, kl_build_add_to_cart_data( $added_product, $quantity, WC()->cart ) ); } includes/blocks/StoreApi.php000064400000017137146726766340012124 0ustar00settings = get_option( 'klaviyo_settings' ); if ( ! $this->settings ) { return; } /** * Compatibility issue separate from consent at checkout, see callback method docstring for more info. We are * using this hook based on its occurring specifically from the Checkout Block and corresponding to the older * woocommerce_checkout_order_processed hook and indicates and order is processed and ready for payment. */ add_action( 'woocommerce_store_api_checkout_order_processed', array( $this, 'update_order_created_at' ), 10, 1 ); // Only run the logic if we have one of the newsletter checkboxes enabled. if ( ( isset($this->settings['klaviyo_sms_subscribe_checkbox']) && $this->settings['klaviyo_sms_subscribe_checkbox'] ) || ( isset($this->settings['klaviyo_subscribe_checkbox']) && $this->settings['klaviyo_subscribe_checkbox'] ) ) { // React to consent opt-in from Store API (Checkout block) and schedule an API call to Klaviyo. add_action( 'woocommerce_store_api_checkout_update_order_from_request', array( $this, 'optin_customer_from_store_api' ), 10, 2 ); // Triggers an API call to Klaviyo. add_action( 'klaviyo_schedule_consent_event', array( $this, 'send_consent_event' ), 10, 5 ); // Register Store API schema and validation. add_action( 'init', array( $this, 'register_store_api_routes' ) ); } } /** * We need to reset the order's created_at property when the order is placed to counteract Block Checkout creating * the shop_order record at the start of checkout as a draft. Legacy checkouts created a shop_order record when a * customer clicks "Place Order". The earlier created_at date causes Placed Order and Ordered Product events in * Klaviyo to eventually be recorded before Started Checkout events in a profile's activity feed. This can impact * Abandoned Cart flows negatively. * * @param \WC_Order $order Order object. * @return void */ public function update_order_created_at( $order ) { $unix_now = time(); $order->set_date_created( $unix_now ); // to ensure modified isn't before. $order->set_date_modified( $unix_now ); $order->save(); } /** * Register Store API schema and validation. * We're not returning anything to the Checkout request, that's why we only have schema_callback. We need to react to POST requests. * * @return void * @since 0.1.0 * @see https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/rest-api/extend-rest-api-add-data.md */ public function register_store_api_routes() { $args = array( 'endpoint' => \Automattic\WooCommerce\StoreApi\Schemas\V1\CheckoutSchema::IDENTIFIER, 'namespace' => 'klaviyo', 'schema_callback' => function () { return array( 'newsletter' => array( 'description' => __( 'Subscribe to email newsletter.', 'klaviyo-checkout-block' ), 'type' => array( 'boolean', 'null' ), 'context' => array(), 'arg_options' => $this->optional_boolean_arg_options(), ), 'sms' => array( 'description' => __( 'Subscribe to sms marketing newsletter.', 'klaviyo-checkout-block' ), 'type' => array( 'boolean', 'null' ), 'context' => array(), 'arg_options' => $this->optional_boolean_arg_options(), ), ); }, ); woocommerce_store_api_register_endpoint_data( $args ); } /** * React to consent opt-in from Store API (Checkout block) and schedule an API call to Klaviyo. * * Request payload structure: * https://github.com/woocommerce/woocommerce-blocks/blob/trunk/src/StoreApi/docs/checkout.md#process-order-and-payment * * @param \WC_Order $order Order object. * @param \WP_REST_Request $request Request object. * @return void * @since 0.1.0 */ public function optin_customer_from_store_api( $order, $request ) { $billing_address = $request->billing_address; $request_email = $billing_address['email'] ?? null; $request_phone = $billing_address['phone'] ?? null; $request_country = $billing_address['country'] ?? null; $email = $order->get_billing_email() ?? $request_email; $phone = $order->get_billing_phone() ?? $request_phone; $country = $order->get_billing_country() ?? $request_country; // Structure of extension data defined in schema_callback above. $consent_to_sms = $request['extensions']['klaviyo']['sms']; $consent_to_newsletter = $request['extensions']['klaviyo']['newsletter']; if ( $consent_to_sms || $consent_to_newsletter ) { as_enqueue_async_action( 'klaviyo_schedule_consent_event', array( $email, $phone, $country, $consent_to_sms, $consent_to_newsletter ), 'klaviyo' ); } } /** * Triggers an API call to Klaviyo. * * @param string $email Customer email. * @param string $phone Customer billing phone. * @param string $country Customer billing country. * @param boolean $consent_to_sms If the customer consented to sms. * @param boolean $consent_to_newsletter If the customer consented to newsletter. * @return void * @since 0.1.0 */ public function send_consent_event( $email, $phone, $country, $consent_to_sms, $consent_to_newsletter ) { $url = $this->api_url . 'webhook/integration/woocommerce?c=' . $this->settings['klaviyo_public_api_key']; $body = array( 'data' => array(), ); if ( $consent_to_sms ) { array_push( $body['data'], array( 'customer' => array( 'email' => $email, 'country' => $country, 'phone' => $phone, ), 'consent' => true, 'updated_at' => gmdate( DATE_ATOM, date_timestamp_get( date_create() ) ), 'consent_type' => 'sms', 'group_id' => $this->settings['klaviyo_sms_list_id'], ) ); } if ( $consent_to_newsletter ) { array_push( $body['data'], array( 'customer' => array( 'email' => $email, 'phone' => $phone, ), 'consent' => true, 'updated_at' => gmdate( DATE_ATOM, date_timestamp_get( date_create() ) ), 'consent_type' => 'email', 'group_id' => $this->settings['klaviyo_newsletter_list_id'], ) ); } wp_remote_post( $url, array( 'method' => 'POST', 'httpversion' => '1.0', 'blocking' => false, 'headers' => array( 'X-WC-Webhook-Topic' => 'custom/consent', 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( $body ), 'data_format' => 'body', ) ); } /** * Validate if the value is a boolean or null. Make sure we accept null as false. * * @return array Array of options for the argument. See https://developer.wordpress.org/reference/functions/register_rest_route/#arguments * @since 0.1.0 */ protected function optional_boolean_arg_options() { return array( 'validate_callback' => function ( $value ) { if ( ! is_null( $value ) && ! is_bool( $value ) ) { return new \WP_Error( 'api-error', 'value of type ' . gettype( $value ) . ' was posted to the klaviyo opt-in callback' ); } return true; }, 'sanitize_callback' => function ( $value ) { if ( is_bool( $value ) ) { return $value; } return false; }, ); } } includes/blocks/CheckoutIntegration.php000064400000011643146726766340014343 0ustar00settings = get_option( 'klaviyo_settings' ); } /** * The name of the integration. * * @return string */ public function get_name() { return 'klaviyo_checkout_block'; } /** * Block scripts and settings are already loaded */ public function initialize() { $script_path = '/blocks/build/events.js'; $script_url = plugins_url( $script_path, __DIR__ ); $script_asset_path = dirname( __DIR__ ) . '/blocks/build/events.asset.php'; $script_asset = file_exists( $script_asset_path ) ? require $script_asset_path : array( 'dependencies' => array(), 'version' => $this->get_file_version( $script_path ), ); wp_register_script( 'klaviyo-checkout-block-event-script', $script_url, array_merge( $script_asset['dependencies'], array( 'wck_started_checkout' ) ), $script_asset['version'], true ); } /** * Returns an array of script handles to enqueue in the frontend context. * * Note: first element in array matches namespace/block-name from block * name in block.json e.g. klaviyo/klaviyo-checkout-block matches to * klaviyo-klaviyo-checkout-block. Mismatch gets caught and very hard to * identify. * * @return string[] */ public function get_script_handles() { return array( 'klaviyo-klaviyo-checkout-block-view-script', 'klaviyo-checkout-block-event-script' ); } /** * Returns an array of script handles to enqueue in the editor context. * * Note: first element in array matches namespace/block-name from block * name in block.json e.g. klaviyo/klaviyo-checkout-block matches to * klaviyo-klaviyo-checkout-block. Mismatch gets caught and very hard to * identify. * * @return string[] */ public function get_editor_script_handles() { return array( 'klaviyo-klaviyo-checkout-block-editor-script' ); } /** * An array of key, value pairs of data made available to the block on the client side. * * @return array */ public function get_script_data() { return array( 'newsletterText' => $this->get_newsletter_text(), 'smsConsentText' => $this->get_sms_consent_text(), 'smsConsentDisclosureText' => $this->get_sms_consent_disclosure_text(), 'smsEnabled' => $this->get_sms_enabled(), 'newsletterEnabled' => $this->get_newsletter_enabled(), ); } /** * Gets newsletter consent text from settings. * * @return mixed */ public function get_newsletter_text() { return $this->settings['klaviyo_newsletter_text'] ?? __( 'Sign me up to receive email updates and news (optional)', 'klaviyo-checkout-block' ); } /** * Get sms consent text from settings. * * @return mixed */ public function get_sms_consent_text() { return $this->settings['klaviyo_sms_consent_text'] ?? __( 'Sign me up to receive SMS updates and news (optional)', 'klaviyo-checkout-block' ); } /** * Get sms consent disclosure text from settings. * * @return mixed */ public function get_sms_consent_disclosure_text() { return $this->settings['klaviyo_sms_consent_disclosure_text'] ?? __( 'By checking this box and entering your phone number above, you consent to receive marketing text messages (such as [promotion codes] and [cart reminders]) from [company name] at the number provided, including messages sent by autodialer. Consent is not a condition of any purchase. Message and data rates may apply. Message frequency varies. You can unsubscribe at any time by replying STOP or clicking the unsubscribe link (where available) in one of our messages. View our Privacy Policy [link] and Terms of Service [link]', 'klaviyo-checkout-block' ); } /** * Whether sms consent collection is enabled in the integration settings. * * @return false|mixed */ public function get_sms_enabled() { return $this->settings['klaviyo_sms_subscribe_checkbox'] ?? false; } /** * Whether email consent collection is enabled in the integration settings. * * @return false|mixed */ public function get_newsletter_enabled() { return $this->settings['klaviyo_subscribe_checkbox'] ?? false; } /** * Get the file modified time as a cache buster if we're in dev mode. * * @param string $file Local path to the file. * @return string The cache buster value to use for the given file. */ protected function get_file_version( $file ) { if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $file ) ) { return filemtime( $file ); } return \WooCommerceKlaviyo::get_version(); } } includes/blocks/build/events.js000064400000002423146726766340012616 0ustar00(()=>{"use strict";const e=window.wp.data,t=window.wp.url,c=window.wp.hooks,o=(e,t,{leading:c}={})=>{let o;return(...i)=>{!o&&c&&e(...i),clearTimeout(o),o=setTimeout((()=>e(...i)),t)}},i=o((e=>setKlaviyoCookie(e)),1e3),a=o(((e,t)=>makePublicAPIcall(e,t)),1e3),l=o((()=>s.trackStartedCheckout()),1e3),s=window.WCK||{};s.trackStartedCheckout=function(){var e={};if(kl_checkout.email)e.email=kl_checkout.email;else{if(!kl_checkout.exchange_id)return;e._kx=kl_checkout.exchange_id}makePublicAPIcall("client/events/",buildEventRequestPayload(e,kl_checkout.event_data,{name:"Started Checkout",service:"woocommerce"}))},window.addEventListener("load",(function(){const o={};(0,c.addAction)("experimental__woocommerce_blocks-checkout-set-email-address","klaviyo-checkout-block",(()=>{const{billingAddress:{email:c}}=(0,e.select)("wc/store/cart").getCustomerData();(0,t.isEmail)(c)&&(o.email=c,identify_object.properties=o,kl_checkout.email=c,l())})),(0,c.addAction)("experimental__woocommerce_blocks-checkout-set-billing-address","klaviyo-checkout-block",(()=>{const{billingAddress:{first_name:t,last_name:c}}=(0,e.select)("wc/store/cart").getCustomerData();t&&(o.first_name=t),c&&(o.last_name=c),identify_object.properties=o,i(o),a("client/profiles/",buildProfileRequestPayload(identify_object))}))}))})();includes/blocks/build/index.js000064400000002237146726766340012424 0ustar00(()=>{"use strict";const e=window.wp.blocks,t=window.wp.element,n=window.wp.blockEditor,l=window.wp.components,o=window.wc.wcSettings,c=window.wc.blocksCheckout,{newsletterText:s,smsConsentText:a,smsConsentDisclosureText:r,smsEnabled:i,newsletterEnabled:u}=(0,o.getSetting)("klaviyo_checkout_block_data"),w=({checkoutExtensionData:e={}})=>{const[n,l]=(0,t.useState)(!1),[o,w]=(0,t.useState)(!1),{setExtensionData:k}=e;return(0,t.useEffect)((()=>{k&&i&&k("klaviyo","sms",o)}),[o,k]),(0,t.useEffect)((()=>{k&&u&&k("klaviyo","newsletter",n)}),[n,k]),u||i?(0,t.createElement)(t.Fragment,null,u&&(0,t.createElement)(c.CheckboxControl,{checked:n,onChange:l},(0,t.createElement)("span",null,s)),i&&(0,t.createElement)(t.Fragment,null,(0,t.createElement)(c.CheckboxControl,{checked:o,onChange:w},(0,t.createElement)("span",null,a)),o&&(0,t.createElement)("p",null,r))):null},k=JSON.parse('{"name":"klaviyo/klaviyo-checkout-block"}');(0,e.registerBlockType)(k.name,{edit:function(){return(0,t.createElement)("div",{...(0,n.useBlockProps)()},(0,t.createElement)(l.Disabled,null,(0,t.createElement)(w,null)))},save:function(){return(0,t.createElement)("div",{...n.useBlockProps.save()})}})})();includes/blocks/build/frontend.asset.php000064400000000205146726766340014416 0ustar00 array('wc-blocks-checkout', 'wc-settings', 'wp-element'), 'version' => 'da5b6663cd5eeea5285c'); includes/blocks/build/frontend.js000064400000002745146726766340013140 0ustar00(()=>{"use strict";const e=window.wc.blocksCheckout,t=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"klaviyo/klaviyo-checkout-block","version":"0.1.0","title":"Klaviyo","category":"woocommerce","description":"Klaviyo\'s marketing consent checkbox.","supports":{"align":false,"html":false,"multiple":false,"reusable":false,"inserter":false,"lock":false},"attributes":{"lock":{"type":"object","default":{"remove":true,"move":true}}},"parent":["woocommerce/checkout-contact-information-block"],"textdomain":"klaviyo-checkout-block","editorScript":"file:./index.js","viewScript":"file:./frontend.js","editorStyle":"file:./index.css","style":"file:./style-index.css"}'),o=window.wp.element,n=window.wc.wcSettings,{newsletterText:l,smsConsentText:c,smsConsentDisclosureText:s,smsEnabled:a,newsletterEnabled:r}=(0,n.getSetting)("klaviyo_checkout_block_data");(0,e.registerCheckoutBlock)({metadata:t,component:({checkoutExtensionData:t={}})=>{const[n,i]=(0,o.useState)(!1),[m,k]=(0,o.useState)(!1),{setExtensionData:u}=t;return(0,o.useEffect)((()=>{u&&a&&u("klaviyo","sms",m)}),[m,u]),(0,o.useEffect)((()=>{u&&r&&u("klaviyo","newsletter",n)}),[n,u]),r||a?(0,o.createElement)(o.Fragment,null,r&&(0,o.createElement)(e.CheckboxControl,{checked:n,onChange:i},(0,o.createElement)("span",null,l)),a&&(0,o.createElement)(o.Fragment,null,(0,o.createElement)(e.CheckboxControl,{checked:m,onChange:k},(0,o.createElement)("span",null,c)),m&&(0,o.createElement)("p",null,s))):null}})})();includes/blocks/build/index.asset.php000064400000000266146726766340013715 0ustar00 array('wc-blocks-checkout', 'wc-settings', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element'), 'version' => 'a1881f51035560664b30'); includes/blocks/build/block.json000064400000001441146726766340012740 0ustar00{ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "klaviyo/klaviyo-checkout-block", "version": "0.1.0", "title": "Klaviyo", "category": "woocommerce", "description": "Klaviyo's marketing consent checkbox.", "supports": { "align": false, "html": false, "multiple": false, "reusable": false, "inserter": false, "lock": false }, "attributes": { "lock": { "type": "object", "default": { "remove": true, "move": true } } }, "parent": [ "woocommerce/checkout-contact-information-block" ], "textdomain": "klaviyo-checkout-block", "editorScript": "file:./index.js", "viewScript": "file:./frontend.js", "editorStyle": "file:./index.css", "style": "file:./style-index.css" }includes/blocks/build/events.asset.php000064400000000163146726766340014106 0ustar00 array('wp-data', 'wp-hooks', 'wp-url'), 'version' => 'b34e1258069f9fa972a4'); includes/js/wck-viewed-product.js000064400000004143146726766340013076 0ustar00/** * WCK Viewed Product * * Incoming product object * @typedef {Object} item * @property {string} title - Product name * @property {int} product_id - Parent product ID * @property {int} variant_id - Product ID * @property {string} url - Product permalink * @property {string} image_url - Product image url * @property {float} price - Product price * @property {array} categories - Product categories (array of strings) * * Unfortunately wp_localize_script converts all variables to strings :( so we * will have to re-parse ints and floats. * See note in - https://codex.wordpress.org/Function_Reference/wp_localize_script * */ function trackViewedProduct() { var klaviyo = window.klaviyo || []; var viewed_item = { 'Title': item.title, 'ItemId': parseInt(item.product_id), 'ProductID': parseInt(item.product_id), 'variantId': parseInt(item.variant_id), 'Categories': item.categories, 'ImageUrl': item.image_url, 'Url': item.url, 'Metadata': { 'Price': parseFloat(item.price), } }; var track_viewed_item = { 'Title': item.title, 'ItemId': parseInt(item.product_id), 'variantId': parseInt(item.variant_id), 'Categories': item.categories, 'ImageUrl': item.image_url, 'Url': item.url, 'Metadata': { 'Price': parseFloat(item.price), } }; klaviyo.push(['track', 'Viewed Product', viewed_item]); klaviyo.push(['trackViewedItem', track_viewed_item]); }; window.addEventListener("load", function() { !function(){if(!window.klaviyo){window._klOnsite=window._klOnsite||[];try{window.klaviyo=new Proxy({},{get:function(n,i){return"push"===i?function(){var n;(n=window._klOnsite).push.apply(n,arguments)}:function(){for(var n=arguments.length,o=new Array(n),w=0;w identifyUser("first_name", node)); }); lastNameNodes.each(function(){ var node = jQuery(this); node.change(() => identifyUser("last_name", node)); }); } window.addEventListener("load", function () { // Custom checkouts/payment platforms may still load this file but won't // fire woocommerce_after_checkout_form hook to load checkout data. if (typeof kl_checkout === 'undefined') { return; } var WCK = WCK || {}; WCK.trackStartedCheckout = function () { var metric_attributes = { 'name': 'Started Checkout', 'service': 'woocommerce' } var customer_properties = {} if (kl_checkout.email) { customer_properties['email'] = kl_checkout.email; } else if (kl_checkout.exchange_id) { customer_properties['_kx'] = kl_checkout.exchange_id; } else { return; } makePublicAPIcall('client/events/', buildEventRequestPayload(customer_properties, kl_checkout.event_data, metric_attributes)); }; var klCookie = getKlaviyoCookie(); // Priority of emails for syncing Started Checkout event: Logged-in user, // cookied exchange ID, cookied email, billing email address if (kl_checkout.email !== "") { identify_object.properties = { 'email': kl_checkout.email }; makePublicAPIcall('client/profiles/', buildProfileRequestPayload(identify_object)); setKlaviyoCookie(identify_object.properties); WCK.trackStartedCheckout(); } else if (klCookie && JSON.parse(klCookie).$exchange_id !== undefined) { kl_checkout.exchange_id = JSON.parse(klCookie).$exchange_id; WCK.trackStartedCheckout(); } else if (klCookie && JSON.parse(klCookie).email !== undefined) { kl_checkout.email = JSON.parse(klCookie).email; WCK.trackStartedCheckout(); } else { if (jQuery) { var { firstNameNodes, lastNameNodes, emailNodes } = getTrackingNodes(); emailNodes.change(function () { var elem = jQuery(this), email = jQuery.trim(elem.val()); if (email && /@/.test(email)) { var params = { "email": email }; if (firstNameNodes.length > 0) { // Values come from first visible input node in the DOM params["first_name"] = firstNameNodes.val(); } if (lastNameNodes.length > 0) { params["last_name"] = lastNameNodes.val(); } setKlaviyoCookie(params); kl_checkout.email = params.email; identify_object.properties = params; makePublicAPIcall('client/profiles/', buildProfileRequestPayload((identify_object))); WCK.trackStartedCheckout(); } }); // Save billing fields klIdentifyBillingField(); } } }); includes/class-wck-install.php000064400000007360146726766340012451 0ustar00check_version(); } /** * Check version of plugin against that saved in the DB to identify update. * * @return void */ public function check_version() { if ( ! defined( 'IFRAME_REQUEST' ) && ( get_option( 'woocommerce_klaviyo_version' ) !== WooCommerceKlaviyo::get_version() ) ) { $this->install(); // Send options and version number to Klaviyo. $this->post_update_to_klaviyo(); /** * Fires when Klaviyo plugin is updated. * * @since 2.0.0 */ do_action( 'woocommerce_klaviyo_updated' ); } } /** * Send options and plugin version information to Klaviyo during the plugin update. Remove * the site transient so we start checking for a new version of Klaviyo again. */ protected function post_update_to_klaviyo() { // Send options to Klaviyo. WCK()->webhook_service->send_options_webhook( true ); // Remove transient so we start checking 'set_site_transient_update_plugins' again. delete_site_transient( 'is_klaviyo_plugin_outdated' ); } /** * Install WCK */ public function install() { // Update version. update_option( 'woocommerce_klaviyo_version', WooCommerceKlaviyo::get_version() ); // Flush rules after install. flush_rewrite_rules(); } /** * Called from WCK_Api via the `disable` route. Deactivate Klaviyo plugin via builtin function so hooks fire. */ public function deactivate_klaviyo() { deactivate_plugins( KLAVIYO_BASENAME ); } /** * Handle cleanup of the plugin. * Delete options and remove WooCommerce webhooks. */ public function cleanup_klaviyo() { // We can't remove webhooks without WooCommerce. No need to remove the integration app-side. if ( is_plugin_active( 'woocommerce/woocommerce.php' ) ) { // Remove WooCommerce webhooks. self::remove_klaviyo_webhooks(); } // Lastly, delete Klaviyo-related options. delete_option( 'klaviyo_settings' ); delete_option( 'woocommerce_klaviyo_version' ); delete_site_transient( 'is_klaviyo_plugin_outdated' ); } /** * Remove Klaviyo related webhooks. The only way to identify these are through the delivery url so check for the * Woocommerce webhook path. */ private static function remove_klaviyo_webhooks() { $webhook_data_store = WC_Data_Store::load( 'webhook' ); $webhooks_by_status = $webhook_data_store->get_count_webhooks_by_status(); // $webhooks_by_status returns an associative array with a count of webhooks in each status. $count = array_sum( $webhooks_by_status ); if ( 0 === $count ) { return; } // We can only get IDs and there's not a way to search by delivery url which is the only way to identify // a webhook created by Klaviyo. We'll have to iterate no matter what so might as well get them all. $webhook_ids = $webhook_data_store->get_webhooks_ids(); foreach ( $webhook_ids as $webhook_id ) { $webhook = wc_get_webhook( $webhook_id ); if ( ! $webhook ) { continue; } if ( false !== strpos( $webhook->get_delivery_url(), '/api/webhook/integration/woocommerce' ) ) { $webhook_data_store->delete( $webhook ); } } } } endif; includes/wck-checkout-block.php000064400000001610146726766340012565 0ustar00register( new WCK\Blocks\CheckoutIntegration() ); }, 10, 1 ); includes/wck-cart-functions.php000064400000026260146726766340012637 0ustar00 $v ) { $item = $v['item']; $container_id = $item['container_id']; if ( isset( $item['attributes'] ) ) { $container[ $container_id ] = array( 'product_id' => $item['product_id'], 'quantity' => $item['quantity'], 'variation_id' => $item['variation_id'], 'attributes' => $item['attributes'], ); } else { $container[ $container_id ] = array( 'product_id' => $item['product_id'], 'quantity' => $item['quantity'], ); } } $added = WC_CP()->cart->add_composite_to_cart( $v['composite_id'], $v['composite_quantity'], $container ); } } /** * Gets email of current user or commenter. * * @param WP_User $current_user The current WordPress user. * @return mixed|string */ function get_email( $current_user ) { $email = ''; if ( $current_user->user_email ) { $email = $current_user->user_email; } else { // See if current user is a commenter. $commenter = wp_get_current_commenter(); if ( $commenter['comment_author_email'] ) { $email = $commenter['comment_author_email']; } } return $email; } /** * Rebuild cart from Abandoned Checkout query param. * * @return void */ function wck_rebuild_cart() { // Exit if in back-end. if ( is_admin() ) { return; } global $woocommerce; // Exit if not on cart page or no wck_rebuild_cart parameter. $current_url = build_current_url(); $utm_wck_rebuild_cart = isset( $_GET['wck_rebuild_cart'] ) ? sanitize_text_field( wp_unslash( $_GET['wck_rebuild_cart'] ) ) : ''; if ( wc_get_cart_url() !== $current_url[0] || '' === $utm_wck_rebuild_cart ) { return; } // Rebuild cart. $woocommerce->cart->empty_cart( true ); $woocommerce->cart->get_cart(); $kl_cart = json_decode( base64_decode( $utm_wck_rebuild_cart ), true ); /** * Allow developers to customise the payload before the cart is rebuilt. * * @since 3.0.12 * * @param mixed $kl_cart The Klaviyo added to cart payload */ $kl_cart = apply_filters( 'kl_cart_rebuild', $kl_cart ); $composite_products = $kl_cart['composite']; $normal_products = $kl_cart['normal_products']; foreach ( $normal_products as $product ) { $cart_key = $woocommerce->cart->add_to_cart( $product['product_id'], $product['quantity'], $product['variation_id'], $product['variation'] ); } if ( class_exists( 'WC_Composite_Products' ) ) { add_composite_products_cart( $composite_products ); } /** * Allow developers to call a callback on cart rebuild. * * The `kl_cart_rebuild_complete` allows you to perform an action after Klaviyo has recovered and rebuilt the customers cart. * * @since 3.0.12 * * @param mixed $kl_cart The Klaviyo added to cart payload */ do_action( 'kl_cart_rebuild_complete', $kl_cart ); $carturl = wc_get_cart_url(); if ( wc_get_cart_url() === $current_url[0] ) { header( 'Refresh:0; url=' . $carturl ); } } /** * Construct current url. * * @return false|string[] */ function build_current_url() { $server_protocol = isset( $_SERVER['HTTPS'] ) ? 'https' : 'http'; $server_host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; $server_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; return explode( '?', $server_protocol . '://' . $server_host . $server_uri ); } /** * Insert tracking code for tracking started checkout. * * @return void */ function wck_started_checkout_tracking() { global $current_user; wp_reset_query(); wp_get_current_user(); $cart = WC()->cart; $event_data = wck_build_cart_data( $cart ); if ( empty( $event_data['$extra']['Items'] ) ) { return; } $event_data['$service'] = 'woocommerce'; // Remove top level properties to maintain consistent Started Checkout event data in 2.5.0. unset( $event_data['Tags'] ); unset( $event_data['Quantity'] ); $email = get_email( $current_user ); /** Adding apply_filter hook to modify the $event_data array which is passed to wck-started-checkout.js * * The `kl_started_checkout` filter allows you to add additional top level properties to the * [Started Checkout](https://help.klaviyo.com/hc/en-us/articles/360030732832#started-checkout1) event. * * The example below will add a "ReferralCode" property to the Started Checkout event * * add_filter('kl_started_checkout','kl_modify_started_checkout', 1, 2); * * function kl_modify_started_checkout($checkout_data, $cart){ * $referrer = htmlspecialchars($_COOKIE['referral_code']); * if(isset($referrer)){ * $referrer = "Direct"; * } * $checkout_data['ReferralCode'] = $referrer; * return $checkout_data; * } * * @since 3.0.12 * * @param array $event_data * @param WC_Cart $cart */ $event_data = apply_filters( 'kl_started_checkout', $event_data, $cart ); $started_checkout_data = array( 'email' => $email, 'event_data' => $event_data, ); // Pass Started Checkout event data to javascript attaching to 'wck_started_checkout' handle. wp_localize_script( 'wck_started_checkout', 'kl_checkout', $started_checkout_data ); } // Load javascript file for Started Checkout events. add_action( 'wp_enqueue_scripts', 'load_started_checkout' ); /** * Check if page is a checkout page, if so load the Started Checkout javascript file. */ function load_started_checkout() { require_once __DIR__ . '/class-wck-api.php'; /** * Override whether a page is a checkout page for purposes of whether to load the started checkout js. * * @since 3.0.8 */ $should_add_started_checkout = apply_filters( 'wck_should_add_started_checkout', is_checkout() ); if ( $should_add_started_checkout ) { $token = WCK()->options->get_klaviyo_option( 'klaviyo_public_api_key' ); wp_enqueue_script( 'wck_started_checkout', plugins_url( '/js/wck-started-checkout.js', __FILE__ ), null, WCK_API::VERSION, true ); wp_localize_script( 'wck_started_checkout', 'public_key', array( 'token' => $token ) ); wp_localize_script( 'wck_started_checkout', 'plugin_meta_data', array( 'data' => kl_get_plugin_usage_meta_data() )); // Build started checkout event data and add inline script to html. wck_started_checkout_tracking(); } } add_action( 'wp_loaded', 'wck_rebuild_cart' ); /** * Add checkbox to subscribe profiles to email list during checkout. * * @param array[] $fields Checkout form fields. * @return array[] $fields */ function kl_checkbox_custom_checkout_field( $fields ) { $klaviyo_settings = get_option( 'klaviyo_settings' ); $fields['billing']['kl_newsletter_checkbox'] = array( 'type' => 'checkbox', 'class' => array( 'kl_newsletter_checkbox_field' ), 'label' => $klaviyo_settings['klaviyo_newsletter_text'], 'value' => true, 'default' => 0, 'required' => false, ); return $fields; } /** * Add checkbox to subscribe profiles to SMS list during checkout. * * @param array[] $fields Checkout form fields. * @return array[] $fields */ function kl_sms_consent_checkout_field( $fields ) { $klaviyo_settings = get_option( 'klaviyo_settings' ); $fields['billing']['kl_sms_consent_checkbox'] = array( 'type' => 'checkbox', 'class' => array( 'kl_sms_consent_checkbox_field' ), 'label' => $klaviyo_settings['klaviyo_sms_consent_text'], 'value' => true, 'default' => 0, 'required' => false, ); return $fields; } /** * Echo compliance text. * * @return void */ function kl_sms_compliance_text() { $klaviyo_settings = get_option( 'klaviyo_settings' ); echo esc_html($klaviyo_settings['klaviyo_sms_consent_disclosure_text']); } /** * Send consent settings to Klaviyo. * * @return void */ function kl_add_to_list() { // This method is called from within WC_Checkout::process_checkout where nonce validation is done. Ignoring here. // phpcs:disable WordPress.Security.NonceVerification.Missing $klaviyo_settings = get_option( 'klaviyo_settings' ); $email = isset( $_POST['billing_email'] ) ? sanitize_text_field( wp_unslash( $_POST['billing_email'] ) ) : null; $phone = isset( $_POST['billing_phone'] ) ? sanitize_text_field( wp_unslash( $_POST['billing_phone'] ) ) : null; $country = isset( $_POST['billing_country'] ) ? sanitize_text_field( wp_unslash( $_POST['billing_country'] ) ) : null; $url = 'https://a.klaviyo.com/api/webhook/integration/woocommerce?c=' . $klaviyo_settings['klaviyo_public_api_key']; $body = array( 'data' => array(), ); if ( isset( $_POST['kl_sms_consent_checkbox'] ) && sanitize_text_field( wp_unslash( $_POST['kl_sms_consent_checkbox'] ) ) ) { array_push( $body['data'], array( 'customer' => array( 'email' => $email, 'country' => $country, 'phone' => $phone, ), 'consent' => true, 'updated_at' => gmdate( DATE_ATOM, date_timestamp_get( date_create() ) ), 'consent_type' => 'sms', 'group_id' => $klaviyo_settings['klaviyo_sms_list_id'], ) ); } if ( isset( $_POST['kl_newsletter_checkbox'] ) && sanitize_text_field( wp_unslash( $_POST['kl_newsletter_checkbox'] ) ) ) { array_push( $body['data'], array( 'customer' => array( 'email' => $email, 'phone' => $phone, ), 'consent' => true, 'updated_at' => gmdate( DATE_ATOM, date_timestamp_get( date_create() ) ), 'consent_type' => 'email', 'group_id' => $klaviyo_settings['klaviyo_newsletter_list_id'], ) ); } // phpcs:enable WordPress.Security.NonceVerification.Missing wp_remote_post( $url, array( 'method' => 'POST', 'httpversion' => '1.0', 'blocking' => false, 'headers' => array( 'X-WC-Webhook-Topic' => 'custom/consent', 'Content-Type' => 'application/json', ), 'body' => json_encode( $body ), 'data_format' => 'body', ) ); } $klaviyo_settings = get_option( 'klaviyo_settings' ); if ( isset( $klaviyo_settings['klaviyo_subscribe_checkbox'] ) && $klaviyo_settings['klaviyo_subscribe_checkbox'] && ! empty( $klaviyo_settings['klaviyo_newsletter_list_id'] ) ) { // Add the checkbox field. add_filter( 'woocommerce_checkout_fields', 'kl_checkbox_custom_checkout_field', 11 ); // Post list request to Klaviyo. add_action( 'woocommerce_checkout_update_order_meta', 'kl_add_to_list' ); } if ( isset( $klaviyo_settings['klaviyo_sms_subscribe_checkbox'] ) && $klaviyo_settings['klaviyo_sms_subscribe_checkbox'] && ! empty( $klaviyo_settings['klaviyo_sms_list_id'] ) ) { // Add the checkbox field. add_filter( 'woocommerce_checkout_fields', 'kl_sms_consent_checkout_field', 11 ); // Add data compliance messaging to checkout page. add_filter( 'woocommerce_after_checkout_billing_form', 'kl_sms_compliance_text' ); // Post SMS request to Klaviyo. add_action( 'woocommerce_checkout_update_order_meta', 'kl_add_to_list' ); } includes/wck-viewed-product.php000064400000002703146726766340012635 0ustar00get_parent_id(); if ( $product->get_parent_id() === 0 ) { $parent_product_id = $product->get_id(); } $categories_array = get_the_terms( $product->get_id(), 'product_cat' ); if ( false === $categories_array ) { $categories_array = array(); } $categories = (array) wp_list_pluck( $categories_array, 'name' ); $item = array( 'title' => (string) $product->get_name(), 'product_id' => (int) $parent_product_id, 'variant_id' => (int) $product->get_id(), 'url' => (string) get_permalink( $product->get_id() ), 'image_url' => (string) wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ), 'price' => (float) $product->get_price(), 'categories' => $categories, ); wp_enqueue_script( 'wck_viewed_product', plugins_url( '/js/wck-viewed-product.js', __FILE__ ), null, WCK_API::VERSION, true ); wp_localize_script( 'wck_viewed_product', 'item', $item ); } } includes/wck-cart-rebuild.php000064400000017212146726766340012252 0ustar00 $item['product_id'], 'quantity' => $item['quantity'], 'variation_id' => $item['variation_id'], 'variation' => $item['variation'], ); } /** * Helper function for composite products. * * @param array $container_ids container ids. * @param array $values values. * @return array Composite product. */ function add_encoded_composite( $container_ids, $values ) { $composite_product = array(); foreach ( $container_ids as $container_id => $container_values ) { $components = array(); if ( isset( $container_values['attributes'] ) ) { $components = array( 'composite_id' => $container_values['composite_id'], 'composite_quantity' => $values['quantity'], 'item' => array( 'product_id' => $container_values['product_id'], 'quantity' => $container_values['quantity'], 'container_id' => $container_id, 'attributes' => $container_values['attributes'], 'variation_id' => isset( $container_values['variation_id'] ) ? $container_values['variation_id'] : null, ), ); } else { $components = array( 'composite_id' => $container_values['composite_id'], 'composite_quantity' => $values['quantity'], 'item' => array( 'product_id' => $container_values['product_id'], 'quantity' => $container_values['quantity'], 'container_id' => $container_id, ), ); } array_push( $composite_product, $components ); } return $composite_product; } /** * Build the wck_cart and return the event_data. * * @param WC_Cart $cart The woocommerce cart. * @return array Normalized event data */ function wck_build_cart_data( $cart ) { $event_data = array( 'CurrencySymbol' => get_woocommerce_currency_symbol(), 'Currency' => get_woocommerce_currency(), '$value' => $cart->total, '$extra' => array( 'Items' => array(), 'SubTotal' => $cart->subtotal, 'ShippingTotal' => $cart->shipping_total, 'TaxTotal' => $cart->tax_total, 'GrandTotal' => $cart->total, ), ); $wck_cart = array(); $composite_products = array(); $normal_products = array(); $all_categories = array(); $item_names = array(); $all_tags = array(); $item_count = 0; foreach ( $cart->get_cart() as $cart_item_key => $values ) { $product = $values['data']; $parent_product_id = $product->get_parent_id(); if ( $product->get_parent_id() == 0 ) { $parent_product_id = $product->get_id(); } $categories = array(); $categories_array = get_the_terms( $parent_product_id, 'product_cat' ); if ( $categories_array && ! is_wp_error( $categories_array ) ) { $categories = wp_list_pluck( $categories_array, 'name' ); foreach ( $categories as $category ) { array_push( $all_categories, $category ); } } $tags_array = get_the_terms( $parent_product_id, 'product_tag' ); if ( $tags_array && ! is_wp_error( $tags_array ) ) { $tags = wp_list_pluck( $tags_array, 'name' ); foreach ( $tags as $tag ) { array_push( $all_tags, $tag ); } } $is_composite_child = false; $is_chained_product = is_chained_product( $values ); if ( class_exists( 'WC_Composite_Products' ) ) { $product_encoded = json_encode( $product ); $is_composite_child = wc_cp_is_composited_cart_item( $values ); $container = wc_cp_get_composited_cart_item_container( $values ); if ( $product->get_type() == 'composite' ) { $composite_product = array(); foreach ( wc_cp_get_composited_cart_items( $values ) as $key => $val ) { $composite_product = add_encoded_composite( $val['composite_data'], $values ); break; } array_push( $composite_products, $composite_product ); } elseif ( ! $is_composite_child && ! $is_chained_product ) { $normal_products[ $cart_item_key ] = normalize_normal_product( $values ); } } elseif ( ! $is_chained_product ) { $normal_products[ $cart_item_key ] = normalize_normal_product( $values ); } $image = wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ); if ( false == $image ) { $image = wp_get_attachment_url( get_post_thumbnail_id( $parent_product_id ) ); } /** * Somehow price/tax values can be missing on a product. Defaulting to null * in these cases to differentiate from a zero value. */ $line_total = $values['line_total'] ?? null; $line_tax = $values['line_tax'] ?? null; $item = array( 'Quantity' => $values['quantity'], 'ProductID' => $parent_product_id, 'VariantID' => $product->get_id(), 'Name' => $product->get_name(), 'URL' => $product->get_permalink(), 'Images' => array( array( 'URL' => $image, ), ), 'Categories' => $categories, // Default value is an empty array. See WC_Cart::add_to_cart arg, $variation. 'Variation' => $values['variation'] ?? array(), 'SubTotal' => $values['line_subtotal'] ?? null, 'Total' => $values['line_subtotal_tax'] ?? null, 'LineTotal' => $line_total, 'Tax' => $line_tax, // Ensure both values are non-null e.g. zero is fine here. 'TotalWithTax' => isset($line_total, $line_tax) ? $line_total + $line_tax : null, ); /** * Allow developers to customise the payload before it is sent to Klaviyo. * * The `kl_checkout_item` filter allows you to add additional properties to the line items that make up the * [Started Checkout](https://help.klaviyo.com/hc/en-us/articles/360030732832#started-checkout1) event. * * The example below shows you how to add custom fields to the line items on the Started Checkout event * * add_filter('kl_checkout_item','kl_modify_checkout_item',1,2); * * function kl_modify_checkout_item($line_item, $product) { * $product_lead_time = get_field('leadtime',$product->get_id()); * $product_designer = get_field('designer', $product->get_id()); * $line_item['LeadTime'] = $product_lead_time; * $line_item['Designer'] = $product_designer; * return $line_item; * } * * @since 3.0.12 * * @param array $item The item in the cart * @param WC_Product $product The product being added to the cart */ $event_data['$extra']['Items'][] = apply_filters( 'kl_checkout_item', $item, $product ); $item_count += $values['quantity']; $all_categories = array_values( array_unique( $all_categories ) ); $event_data['Categories'] = $all_categories; $all_tags = array_values( array_unique( $all_tags ) ); $event_data['Tags'] = $all_tags; array_push( $item_names, $product->get_name() ); } $event_data['Quantity'] = $item_count; $event_data['ItemNames'] = $item_names; $wck_cart['composite'] = $composite_products; $wck_cart['normal_products'] = $normal_products; $event_data['$extra']['CartRebuildKey'] = base64_encode( json_encode( $wck_cart ) ); return $event_data; } /** * Check if product instance of WooCommerce Chained Products plugin: https://woocommerce.com/products/chained-products/ * * @param object $cart_item_properties cart properties key/values. * @return boolean */ function is_chained_product( $cart_item_properties ) { if ( class_exists( 'WC_Chained_Products' ) && ! empty( $cart_item_properties['chained_item_of'] ) ) { return true; } return false; } includes/class-wck-api.php000064400000046100146726766340011547 0ustar00 array( 'GET' ), self::PERMISSION_WRITE => array( 'POST' ), self::PERMISSION_READ_WRITE => array( 'GET', 'POST' ), ); /** * Check if there is a new version of the Klaviyo plugin available for download. WordPress stores this info in the * database as an object with the following properties: * - last_checked (int) Unix timestamp when request was last made to WordPress server. * - response (array) Contains plugin data with updates available stored by key e.g.'klaviyo/klaviyo.php'. * - no_update (array) Contains plugin data for plugins without updates stored by key e.g. 'klaviyo/klaviyo.php'. * - translations (array) Not relevant to us here. * * The response and no_update arrays are mutually exclusive so we can see if Klaviyo's plugin has been checked for * and if it's in the response array. * * See wp_update_plugins function in `wordpress/wp-includes/update.php` for more information on how this is set. * * @param stdClass $plugins_transient Optional arg if the transient value is already in scope e.g. during update check. * @return bool */ public static function is_most_recent_version( stdClass $plugins_transient = null ) { if ( ! $plugins_transient ) { $plugins_transient = get_site_transient( 'update_plugins' ); } // True if response property isn't set, we don't want to alert on a false positive here. if ( ! isset( $plugins_transient->response ) ) { return true; } // True if Klaviyo plugin is not in the response array meaning no update available. return ! array_key_exists( KLAVIYO_BASENAME, $plugins_transient->response ); } /** * Build payload for version endpoint and webhooks. * * @param bool $is_updating Short circuit checking version if plugin is being updated, we know it's most recent. * @return array */ public static function build_version_payload( $is_updating = false ) { return array( 'plugin_version' => self::VERSION, 'is_most_recent_version' => $is_updating || self::is_most_recent_version(), ); } } /** * Iterate over query to fetch all resource IDs. * * @param WP_Query $loop The query object. * @return array */ function count_loop( WP_Query $loop ) { $loop_ids = array(); while ( $loop->have_posts() ) { $loop->the_post(); $loop_id = get_the_ID(); array_push( $loop_ids, $loop_id ); } return $loop_ids; } /** * Legacy validation function. * * @param WP_REST_Request $request Incoming request object. * @return array */ function validate_request( $request ) { $consumer_key = $request->get_param( 'consumer_key' ); $consumer_secret = $request->get_param( 'consumer_secret' ); if ( empty( $consumer_key ) || empty( $consumer_secret ) ) { return validation_response( true, WCK_API::STATUS_CODE_BAD_REQUEST, WCK_API::ERROR_KEYS_NOT_PASSED, false ); } global $wpdb; // this is stored as a hash so we need to query on the hash. $key = hash_hmac( 'sha256', $consumer_key, 'wc-api' ); $user = $wpdb->get_row( $wpdb->prepare( " SELECT consumer_key, consumer_secret FROM {$wpdb->prefix}woocommerce_api_keys WHERE consumer_key = %s ", $key ) ); if ( $user->consumer_secret === $consumer_secret ) { return validation_response( false, WCK_API::STATUS_CODE_HTTP_OK, null, true ); } return validation_response( true, WCK_API::STATUS_CODE_AUTHORIZATION_ERROR, WCK_API::ERROR_CONSUMER_KEY_NOT_FOUND, false ); } /** * Validate incoming requests to custom endpoints. * * @param WP_REST_Request $request Incoming request object. * @return bool|WP_Error True if validation succeeds, otherwise WP_Error to be handled by rest server. */ function validate_request_v2( WP_REST_Request $request ) { $consumer_key = $request->get_param( 'consumer_key' ); $consumer_secret = $request->get_param( 'consumer_secret' ); if ( empty( $consumer_key ) || empty( $consumer_secret ) ) { return new WP_Error( 'klaviyo_missing_key_secret', 'One or more of consumer key and secret are missing.', array( 'status' => WCK_API::STATUS_CODE_AUTHENTICATION_ERROR ) ); } global $wpdb; // this is stored as a hash so we need to query on the hash. $key = hash_hmac( 'sha256', $consumer_key, 'wc-api' ); $user = $wpdb->get_row( $wpdb->prepare( " SELECT consumer_key, consumer_secret, permissions FROM {$wpdb->prefix}woocommerce_api_keys WHERE consumer_key = %s ", $key ) ); // User query lookup on consumer key can return null or false. if ( ! $user ) { return new WP_Error( 'klaviyo_cannot_authentication', 'Cannot authenticate with provided credentials.', array( 'status' => 401 ) ); } // User does not have proper permissions. if ( ! in_array( $request->get_method(), WCK_API::PERMISSION_METHOD_MAP[ $user->permissions ], true ) ) { return new WP_Error( 'klaviyo_improper_permissions', 'Improper permissions to access this resource.', array( 'status' => WCK_API::STATUS_CODE_AUTHORIZATION_ERROR ) ); } // Success! if ( $user->consumer_secret === $consumer_secret ) { return true; } // Consumer secret didn't match or some other issue authenticating. return new WP_Error( 'klaviyo_invalid_authentication', 'Invalid authentication.', array( 'status' => WCK_API::STATUS_CODE_AUTHENTICATION_ERROR ) ); } /** * Helper method to build response. * * @param boolean $error Whether there's an error during validation. * @param string $code HTTP status code. * @param string $reason Reason for error. * @param boolean $success Whether validation was successful. * @return array */ function validation_response( $error, $code, $reason, $success ) { return array( WCK_API::API_RESPONSE_ERROR => $error, WCK_API::API_RESPONSE_CODE => $code, WCK_API::API_RESPONSE_REASON => $reason, WCK_API::API_RESPONSE_SUCCESS => $success, ); } /** * Helper function for * * @param WP_REST_Request $request Incoming request object. * @param string $post_type WordPress post type. * @return array */ function process_resource_args( $request, $post_type ) { $page_limit = $request->get_param( 'page_limit' ); if ( empty( $page_limit ) ) { $page_limit = WCK_API::DEFAULT_RECORDS_PER_PAGE; } $date_modified_after = $request->get_param( 'date_modified_after' ); $date_modified_before = $request->get_param( 'date_modified_before' ); $page = $request->get_param( 'page' ); $args = array( 'post_type' => $post_type, 'posts_per_page' => $page_limit, 'post_status' => WCK_API::POST_STATUS_ANY, 'paged' => $page, 'date_query' => array( array( 'column' => WCK_API::DATE_MODIFIED, 'after' => $date_modified_after, 'before' => $date_modified_before, ), ), ); return $args; } /** * Helper function to build arg value for date_modified query. * * To maintain backwards compatibility we need to convert the * datetime string (e.g. 2023-06-01T17:01:29) to unix timestamp * because dates are not fine-grained enough for periodic syncs. * https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query#date * * Date query arg value passed to wc_get_orders can be null. * * @param WP_REST_Request $request Incoming request object. * @return string|null */ function kl_build_date_modified_arg_value( WP_REST_Request $request ) { $date_modified_after = $request->get_param( 'date_modified_after' ); $date_modified_before = $request->get_param( 'date_modified_before' ); // strtotime() returns false if it cannot parse datetime string. $after_ts = strtotime( $date_modified_after ); $before_ts = strtotime( $date_modified_before ); if ( $after_ts && $before_ts ) { return "{$after_ts}...{$before_ts}"; } elseif ( $after_ts ) { return ">={$after_ts}"; } elseif ( $before_ts ) { return "<={$before_ts}"; } return null; } /** * Get orders based on request params. * * @param WP_REST_Request $request Incoming request object. * @return array */ function kl_get_orders_count( WP_REST_Request $request ) { $orders = kl_query_orders( $request ); return array( 'order_count' => $orders->total ); } /** * Get product count based on request params. * * @param WP_REST_Request $request Incoming request object. * @return array */ function get_products_count( WP_REST_Request $request ) { $validated_request = validate_request( $request ); if ( true === $validated_request['error'] ) { return $validated_request; } $args = process_resource_args( $request, 'product' ); $loop = new WP_Query( $args ); $data = count_loop( $loop ); return array( 'product_count' => $loop->found_posts ); } /** * Get products based on request params. * * @param WP_REST_Request $request Incoming request object. * @return array|array[] */ function get_products( WP_REST_Request $request ) { $validated_request = validate_request( $request ); if ( true === $validated_request['error'] ) { return $validated_request; } $args = process_resource_args( $request, 'product' ); $loop = new WP_Query( $args ); $data = count_loop( $loop ); return array( 'product_ids' => $data ); } /** * Query for orders using request params. * * `wc_get_orders` is an HPOS compatible query method that is backwards * compatible with the old wp_posts table as well. Passing `paginate` * as an arg returns an object instead of just an array with result values. * * e.g. * stdClass Object * ( * [orders] => Array( * [0] => 157 * [1] => 156 * ) * [total] => 51 * [max_num_pages] => 26 * ) * * @param WP_REST_Request $request Incoming request object. * @return stdClass|WC_Order[] */ function kl_query_orders( $request ) { $page = $request->get_param( 'page' ); $page_limit = $request->get_param( 'page_limit' ); if ( empty( $page_limit ) ) { $page_limit = WCK_API::DEFAULT_RECORDS_PER_PAGE; } $date_modified_arg_value = kl_build_date_modified_arg_value( $request ); $args = array( 'type' => 'shop_order', 'limit' => $page_limit, 'paged' => $page, 'date_modified' => $date_modified_arg_value, 'return' => 'ids', 'paginate' => true, ); return wc_get_orders( $args ); } /** * Get orders count based on request params. * * @param WP_REST_Request $request Incoming request object. * @return array */ function kl_get_orders( WP_REST_Request $request ) { $orders = kl_query_orders( $request ); return array( 'order_ids' => $orders->orders ); } /** * Handle GET request to /klaviyo/v1/version. Returns the current version and if * the installed version is the most recent available in the plugin directory. * * @return array */ function get_extension_version() { return WCK_API::build_version_payload(); } /** * Handle POST request to /klaviyo/v1/options and update plugin options. * * @param WP_REST_Request $request Incoming request object. * @return bool|mixed|void|WP_Error */ function update_options( WP_REST_Request $request ) { $body = json_decode( $request->get_body(), $assoc = true ); if ( ! $body ) { return new WP_Error( 'klaviyo_empty_body', 'Body of request cannot be empty.', array( 'status' => 400 ) ); } $options = get_option( 'klaviyo_settings' ); if ( ! $options ) { $options = array(); } $updated_options = array_replace( $options, $body ); $is_update = (bool) array_diff_assoc( $options, $updated_options ); // If there is no change between existing and new settings `update_option` returns false. Want to distinguish // between that scenario and an actual problem when updating the plugin options. if ( ! update_option( 'klaviyo_settings', $updated_options ) && $is_update ) { return new WP_Error( 'klaviyo_update_failed', 'Options update failed.', array( 'status' => WCK_API::STATUS_CODE_INTERNAL_SERVER_ERROR, 'options' => get_option( 'klaviyo_settings' ), ) ); } // Return plugin version info so this can be saved in Klaviyo when setting up integration for the first time. return array_merge( $updated_options, WCK_API::build_version_payload() ); } /** * Handle GET request to /klaviyo/v1/options and return options set for plugin. * * @return array Klaviyo plugin options. */ function get_klaviyo_options() { return get_option( 'klaviyo_settings' ); } /** * Handle POST request to /klaviyo/v1/disable by deactivating the plugin. * * @param WP_REST_Request $request Incoming request object. * @return WP_Error|WP_REST_Response */ function wck_disable_plugin( WP_REST_Request $request ) { $body = json_decode( $request->get_body(), $assoc = true ); // Verify body contains required data. if ( ! isset( $body['klaviyo_public_api_key'] ) ) { return new WP_Error( 'klaviyo_disable_failed', 'Disable plugin failed, \'klaviyo_public_api_key\' missing from body.', array( 'status' => WCK_API::STATUS_CODE_BAD_REQUEST ) ); } // Verify keys match if set in WordPress options table. $public_api_key = WCK()->options->get_klaviyo_option( 'klaviyo_public_api_key' ); if ( $public_api_key && $body['klaviyo_public_api_key'] !== $public_api_key ) { return new WP_Error( 'klaviyo_disable_failed', 'Disable plugin failed, \'klaviyo_public_api_key\' does not match key set in WP options.', array( 'status' => WCK_API::STATUS_CODE_BAD_REQUEST ) ); } WCK()->installer->deactivate_klaviyo(); return new WP_REST_Response( null, WCK_API::STATUS_CODE_NO_CONTENT ); } add_action( 'rest_api_init', function () { register_rest_route( WCK_API::KLAVIYO_BASE_URL, WCK_API::EXTENSION_VERSION_ENDPOINT, array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'get_extension_version', 'permission_callback' => '__return_true', ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, 'orders/count', array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'kl_get_orders_count', 'permission_callback' => 'validate_request_v2', ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, 'products/count', array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'get_products_count', 'permission_callback' => '__return_true', ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, WCK_API::ORDERS_ENDPOINT, array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'kl_get_orders', 'args' => array( 'id' => array( 'validate_callback' => 'is_numeric', ), ), 'permission_callback' => 'validate_request_v2', ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, WCK_API::PRODUCTS_ENDPOINT, array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'get_products', 'args' => array( 'id' => array( 'validate_callback' => 'is_numeric', ), ), 'permission_callback' => '__return_true', ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, WCK_API::OPTIONS_ENDPOINT, array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => 'update_options', 'permission_callback' => 'validate_request_v2', ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'get_klaviyo_options', 'permission_callback' => 'validate_request_v2', ), ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, WCK_API::DISABLE_ENDPOINT, array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => 'wck_disable_plugin', 'permission_callback' => 'validate_request_v2', ), ) ); } ); /** * Check if there is a new version of the Klaviyo plugin. Return early if we've already identified that we are out of * date. This is stored in a transient that does not expire. This transient is created here the first time we identify * the plugin is out of date and send that information to Klaviyo. It's deleted when someone updates the Klaviyo plugin. * * @param stdClass $transient_value The transient value to be saved, this is an object with various lists of plugins and their data. */ function klaviyo_check_for_plugin_update( $transient_value ) { // If we're up to date or we've already sent this information along just return early. if ( WCK_API::is_most_recent_version( $transient_value ) || get_site_transient( 'is_klaviyo_plugin_outdated' ) ) { return; } // Send options payload to Klaviyo. WCK()->webhook_service->send_options_webhook(); // Set site transient so we don't keep making requests. set_site_transient( 'is_klaviyo_plugin_outdated', 1 ); } /** * This fires when the 'update_plugins' transient is updated which occurs when WordPress polls the plugin directory api * to check for plugin updates. The wp_update_plugins cron runs every 12 hours but requires a pageload for the cron * check to fire. More information at: https://developer.wordpress.org/plugins/cron/ and https://developer.wordpress.org/reference/functions/wp_update_plugins/ * * We hook into this to see if there's a new version of the Klaviyo plugin available, if * so we want to send this information to the corresponding Klaviyo account. Only do so * if we haven't already sent this information to Klaviyo. */ if ( ! get_site_transient( 'is_klaviyo_plugin_outdated' ) ) { add_action( 'set_site_transient_update_plugins', 'klaviyo_check_for_plugin_update' ); } function kl_get_plugin_usage_meta_data() { if ( class_exists( 'WooCommerce' ) ) { global $woocommerce; $woocommerce_version = "woocommerce/$woocommerce->version"; } else { $woocommerce_version = ''; } $wp_version = get_bloginfo('version'); $php_version = PHP_VERSION; $klaviyo_plugin_version = WCK_API::VERSION; return "woocommerce-klaviyo/$klaviyo_plugin_version wordpress/$wp_version php/$php_version $woocommerce_version"; } includes/wck-core-functions.php000064400000001515146726766340012632 0ustar00

get_conflicting_plugins(); if ( $wck_conflicting_plugins ) { ?>
To avoid functionality disruptions, disable the following plugins: format_conflicting_plugins($wck_conflicting_plugins)); ?>.
display_ssl_error_notification() ) { ?>
Ensure your WooCommerce store URL has https and SSL enabled.
Learn more
is_integrated() ) { ?> Your Klaviyo + WooCommerce integration is connected! Head back to the Klaviyo dashboard to continue with next steps for getting your account up and running or to modify any of your Klaviyo + WooCommerce integration settings. Connect your Klaviyo account to use the Klaviyo + WooCommerce integration. Build custom segments, send automations, and track purchase activity in Klaviyo. Log in to authorize an account connection. New to Klaviyo and want to learn more? Check out our How to Integrate with WooCommerce guide.
includes/admin/assets/outbound.svg000064400000001301146726766340013344 0ustar00 includes/admin/assets/warning-icon.svg000064400000000752146726766340014111 0ustar00 includes/admin/assets/error-icon.svg000064400000000645146726766340013576 0ustar00 includes/admin/assets/wck-logo.svg000064400000220040146726766340013232 0ustar00 includes/admin/class-kl-plugins-screen-updates.php000064400000003415146726766340016313 0ustar00 'IMPORTANT: Please review and save your Klaviyo plugin settings after upgrading to ensure consistent functionality.', ); /** * Hook into `in_plugin_update_message` action. */ public function __construct() { add_action( 'in_plugin_update_message-klaviyo/klaviyo.php', array( $this, 'in_plugin_update_message' ), 10, 2 ); } /** * Callback method that adds upgrade notice in Plugins page. * * @param array $plugin_data An array of plugin metadata. * @param object $response An object of metadata about the available plugin update. */ public function in_plugin_update_message( $plugin_data, $response ) { $this->new_version = $response->new_version; $this->upgrade_notice = $this->get_upgrade_notice(); echo $this->upgrade_notice ? '

' . wp_kses_post( $this->upgrade_notice ) : ''; } /** * Gets upgrade notice for corresponding new version from map. * * This can be expanded to utilize data available in plugin * readme.txt but keeping this super basic and manual for now. * * @return string */ protected function get_upgrade_notice() { if ( isset( self::UPGRADE_NOTICE_BY_VERSION[ $this->new_version ] ) ) { return self::UPGRADE_NOTICE_BY_VERSION[ $this->new_version ]; } } } new KL_Plugins_Screen_Updates(); includes/class-wpklaviyo.php000064400000004261146726766340012243 0ustar00options->get_klaviyo_option( 'klaviyo_public_api_key' ) ); } // Display config message. $klaviyowp_message = new WPKlaviyoNotification(); add_action( 'admin_notices', array( &$klaviyowp_message, 'config_warning' ) ); add_action( 'widgets_init', function () { register_widget( 'Klaviyo_EmailSignUp_Widget' ); // Only display Built-in Signup Form widget if klaviyo.js is checked in settings. if ( WCK()->options->get_klaviyo_option( 'klaviyo_popup' ) ) { register_widget( 'Klaviyo_EmbedEmailSignUp_Widget' ); } } ); } /** * Add default settings. * * @deprecated * @return void */ public function add_defaults() { $klaviyo_settings = get_option( 'klaviyo_settings' ); if ( ( 'true' != $klaviyo_settings['installed'] ) || ! is_array( $klaviyo_settings ) ) { $klaviyo_settings = array( 'installed' => 'true', 'klaviyo_public_api_key' => '', 'klaviyo_newsletter_list_id' => '', 'klaviyo_newsletter_text' => '', ); update_option( 'klaviyo_settings', $klaviyo_settings ); } } /** * Format text. * * @deprecated * @param string $content Context to be formatted. * @param boolean $br Contains a break element. * @return mixed */ public function format_text( $content, $br = true ) { return $content; } } inc/kla-admin.php000064400000045743146726766340007725 0ustar00klaviyo_options = get_option( 'klaviyo_settings' ); add_action( 'init', array( $this, 'includes' ) ); add_action( 'plugins_loaded', array( $this, 'setup_admin' ) ); } /** * Include dependent files. * * @return void */ public function includes() { include_once KLAVIYO_PATH . 'includes/admin/class-kl-plugins-screen-updates.php'; } /** * Runs before admin_notices action and adds a wrapper div so we can hide the notices. */ public function inject_before_notices() { if ( $this->is_klaviyo_settings_page() ) { echo '

'; } } /** * Runs after admin_notices action and closes a wrapper div. */ public function inject_after_notices() { if ( $this->is_klaviyo_settings_page() ) { echo '
'; } } /** * Add the Klaviyo plugin style sheets for the admin area. * * @param string $hook The action slug so we can only enqueue for Klaviyo's settings page. */ public function enqueue_styles( $hook ) { if ( strpos( $hook, 'page_klaviyo_settings' ) !== false ) { wp_enqueue_style( 'wck-admin-settings', KLAVIYO_URL . 'includes/admin/css/wck-admin.css', null, WCK_API::VERSION ); } } /** * Handle settings page dependencies and add appropriate menu page. Also hide notices for new auth process. */ public function setup_admin() { if (! function_exists('add_menu_page') || ! current_user_can('manage_options')) { return; } // Continue supporting non-WooCommerce sites. Display old settings page if WC is not activated. if (is_plugin_active('woocommerce/woocommerce.php')) { // The last arg (int) ensures the submenu item is added at the end. add_action('admin_menu', array( $this, 'add_klaviyo_settings_oauth' ), 1000); // Hook into action as early as possible so it's the first to execute. add_action('admin_notices', array( $this, 'inject_before_notices' ), -9999); // Hook into action as late as possible so it's the last to execute. add_action('admin_notices', array( $this, 'inject_after_notices' ), PHP_INT_MAX); } else { add_action('admin_menu', array( $this, 'add_klaviyo_settings_original' )); } // This adds the "Settings" link to the Klaviyo plugin entry on the Installed Plugins page. add_filter('plugin_action_links_' . KLAVIYO_BASENAME, array( $this, 'plugin_settings_link' )); } /** * Check if we are on the Klaviyo specific admin page nested under Marketing. The legacy settings page does not * use this method. We cannot use get_post() here because this fires before the post data is setup. Instead, this * uses the WP_Screen object which is globally available under the 'current_screen' key. The WP_Screen object * base attribute corresponds to the $parent_slug and $menu_slug arguments in the add_submenu_page call. * * @return bool Whether the page is for Klaviyo's plugin settings nested under WooCommerce's Marketing tab. */ private function is_klaviyo_settings_page() { return 'marketing_page_klaviyo_settings' === $GLOBALS['current_screen']->base; } /** * Add Klaviyo menu tab nested under WooCommerce's Marketing tab for oauth authentication process. */ public function add_klaviyo_settings_oauth() { add_submenu_page('woocommerce-marketing', 'Klaviyo settings', 'Klaviyo', 'manage_woocommerce', 'klaviyo_settings', array( $this, 'settings_oauth' )); } /** * Checks active plugins against known conflicts list and returns an array of plugin names. * * @return array */ private function get_conflicting_plugins() { $installed_plugins = get_plugins(); $active_plugins = array_flip(get_option('active_plugins')); $active_conflicts = array_intersect_key($installed_plugins, $active_plugins, array_flip(self::CONFLICTING_PLUGINS)); $conflicts = array_values(array_map(function ( $plugin_details ) { return $plugin_details['Name']; }, $active_conflicts)); return $conflicts; } /** * Formats conflicting plugin names for display in warning. * * @param $plugins array * @return mixed|string */ private function format_conflicting_plugins( $plugins ) { switch (count($plugins)) { case 0: return ''; case 1: return $plugins[0]; case 2: return $plugins[0] . ' and ' . $plugins[1]; default: $last = array_pop($plugins); return implode(', ', $plugins) . ', and ' . $last; } } private function is_integrated() { return isset($this->klaviyo_options['klaviyo_public_api_key']); } private function display_ssl_error_notification() { return ! $this->is_integrated() && ! is_ssl(); } /** * Add Klaviyo menu tab for old authentication process. */ public function add_klaviyo_settings_original() { global $submenu; add_menu_page('Klaviyo', 'Klaviyo', 'manage_options', 'klaviyo_settings', array( $this, 'settings' ), KLAVIYO_URL . 'img/klaviyo-logo.png'); add_submenu_page('klaviyo_settings', 'Help', 'Help', 'manage_options', 'klaviyo_help', array( $this, 'help' )); // Rename the slide-out menu option from Klaviyo to Settings. $submenu['klaviyo_settings'][0][0] = 'Settings'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } /** * Settings page content for new authentication process. */ public function settings_oauth() { include_once KLAVIYO_PATH . '/includes/admin/partials/wc-v1-auth-settings.php'; } /** * Help section content. * * @return void */ public function help() { $content = ''; $content = '
  1. Where do I find my Klaviyo API keys?
  2. How do I add a Klaviyo email sign up into my sidebar?

1) Where do I find my Klaviyo API keys?

You can find your Klaviyo API keys by going to the account page in Klaviyo. Your public API key will be 6-7 characters long. Your private API key will be 7 characters, a hyphen and then 16 more.

Once you have connected your Klaviyo account, tracking will be enabled for visitors.

2) How do I add a Klaviyo email sign up into my sidebar?

Make sure you have connected your Klaviyo account on the Klaviyo settings page.
Then you can find the widget under Appearance » Widgets titled "Klaviyo: Email Sign Up".

'; $content = $this->postbox( 'klaviyo-help', 'FAQ', $content ); $this->admin_wrap( 'Klaviyo Plugin Help', $content ); } /** * Legacy settings page form content. * * @return void */ public function settings() { $klaviyo_settings = $this->process_settings(); $content = ''; if ( function_exists( 'wp_nonce_field' ) ) { $content .= wp_nonce_field( 'klaviyo-update-settings', '_wpnonce', true, false ); } $content .= '

You can find them on your Klaviyo account page

Subscribe contacts at checkout
Contacts will be subscribed to the specified list when they click "Place Order"

How to find List ID

Email

options->get_klaviyo_option( 'klaviyo_subscribe_checkbox', false ), 'true', false ) . ' />

Adds a checkbox to the checkout page for opt-in

SMS

options->get_klaviyo_option( 'klaviyo_sms_subscribe_checkbox', false ), 'true', false ) . ' />

Adds a checkbox to the checkout page for opt-in. You need to first set up SMS in Klaviyo

You must include disclosure language for TCPA compliance. You should also update your Terms of Service and Privacy Policy to include the terms of your SMS marketing program

Learn more about SMS consent and compliance

'; $wrapped_content = $this->postbox( 'klaviyo-settings', 'Connect to Klaviyo', $content ); $this->admin_wrap( 'Klaviyo Settings', $wrapped_content ); } /** * Legacy settings page form processing logic. * * @return mixed */ public function process_settings() { $klaviyo_notification = new WPKlaviyoNotification( 'settings_update' ); if ( ! empty( $_POST['klaviyo_option_submitted'] ) ) { $klaviyo_settings = get_option( 'klaviyo_settings' ); if ( isset( $_GET['page'] ) && 'klaviyo_settings' == $_GET['page'] && check_admin_referer( 'klaviyo-update-settings' ) ) { if ( isset( $_POST['klaviyo_public_api_key'] ) && strlen( sanitize_text_field( wp_unslash( $_POST['klaviyo_public_api_key'] ) ) ) < 8 ) { $klaviyo_settings['klaviyo_public_api_key'] = sanitize_text_field( wp_unslash( $_POST['klaviyo_public_api_key'] ) ); } $klaviyo_setting_keys = array( 'klaviyo_public_api_key', 'klaviyo_subscribe_checkbox', 'klaviyo_newsletter_list_id', 'klaviyo_newsletter_text', 'klaviyo_sms_subscribe_checkbox', 'klaviyo_sms_list_id', 'klaviyo_sms_consent_text', 'klaviyo_sms_consent_disclosure_text', ); $klaviyo_updated_settings = array_fill_keys( $klaviyo_setting_keys, '' ); // Only iterate through relevant keys in $_POST. foreach ( array_intersect_key( $_POST, array_flip( $klaviyo_setting_keys ) ) as $key => $value ) { if ( in_array( $key, array( 'klaviyo_newsletter_text', 'klaviyo_sms_consent_text', 'klaviyo_sms_consent_disclosure_text' ) ) ) { $value = trim( stripslashes( $value ) ); } $klaviyo_updated_settings[ $key ] = $value; } $klaviyo_settings = array_merge( $klaviyo_settings, $klaviyo_updated_settings ); if ( empty( $klaviyo_settings['klaviyo_sms_consent_disclosure_text'] ) ) { $klaviyo_settings['klaviyo_sms_consent_disclosure_text'] = self::SMS_DISCLOSURE_TEXT; } if ( $klaviyo_settings['klaviyo_subscribe_checkbox'] && ! $klaviyo_settings['klaviyo_newsletter_list_id'] ) { $klaviyo_notification->admin_message( 'add_email_list_id', 10 ); } if ( $klaviyo_settings['klaviyo_sms_subscribe_checkbox'] && ! $klaviyo_settings['klaviyo_sms_list_id'] ) { $klaviyo_notification->admin_message( 'add_sms_list_id', 10 ); } if ( $klaviyo_settings['klaviyo_sms_list_id'] == $klaviyo_settings['klaviyo_newsletter_list_id'] && ! empty( $klaviyo_settings['klaviyo_sms_list_id'] ) && ! empty( $klaviyo_settings['klaviyo_newsletter_list_id'] ) ) { $klaviyo_notification->admin_message( 'same_list_ids', 10 ); } $klaviyo_notification->display_message( 3 ); update_option( 'klaviyo_settings', $klaviyo_settings ); } } return get_option( 'klaviyo_settings' ); } /** * Callback method for "plugin_action_links_{$plugin_file}" hook. By default, this does not include * a "settings" link in the Klaviyo entry of the Installed Plugins tab. This adds the Settings link. * * @param $links * @return mixed */ public function plugin_settings_link( $links ) { $settings_link = 'Settings'; array_unshift($links, $settings_link); return $links; } /** * Plugin support panel content. * * @return string */ private function show_plugin_support() { $content = '

First, check the Help Section. If you still have questions or want to give feedback, send an email to Klaviyo support.

'; return $this->postbox( 'klaviyo-support', 'Help / Feedback', $content ); } /** * Return a collection of elements. * * @param string $id Element id attribute. * @param string $title Content title. * @param string $content Content. * @return string */ private function postbox( $id, $title, $content ) { $wrapper = ''; $wrapper .= '
'; $wrapper .= '

'; $wrapper .= '

' . $title . '

'; $wrapper .= '
' . $content . '
'; $wrapper .= '
'; return $wrapper; } /** * Wraps some content in other elements. * * @param string $title Header title. * @param string $content Content. * @return void */ private function admin_wrap( $title, $content ) { $showpluginsupport = $this->show_plugin_support(); // phpcs:disable echo <<

{$title}

{$content}

{$showpluginsupport}
EOT; // phpcs:enable } } inc/js/kl-identify-browser.js000064400000002467146726766340012225 0ustar00/** * Identify browser on site if they are logged in. * * Object possibly containing user/commenter email address: * @typedef {Object} klUser * @property {string} currect_user_email - Email of logged in user * @property {string} commenter_email - Email of logged in commenter * */ function klIdentifyBrowser(klUser) { var klaviyo = window.klaviyo || []; if (klUser.current_user_email) { klaviyo.push(["identify", { "$email": klUser.current_user_email }]); } else { // See if current user is a commenter if (klUser.commenter_email) { klaviyo.push(["identify", { "$email": klUser.commenter_email }]); } } } window.addEventListener("load", function() { !function(){if(!window.klaviyo){window._klOnsite=window._klOnsite||[];try{window.klaviyo=new Proxy({},{get:function(n,i){return"push"===i?function(){var n;(n=window._klOnsite).push.apply(n,arguments)}:function(){for(var n=arguments.length,o=new Array(n),w=0;wklaviyo_public_key = $klaviyo_public_key; // Add analytics. add_action( 'wp_enqueue_scripts', array( $this, 'insert_analytics' ), self::HIGHEST_FILTER_PRIORITY ); // Add js to identify user if commenter or logged-in. Priority 11 to add before Viewed Product. add_action( 'wp_enqueue_scripts', array( $this, 'identify_browser' ), 11 ); } /** * Check WooCommerce plugin status, and run is_checkout() function. */ public function is_woocommerce_checkout_page() { if ( is_plugin_active( 'woocommerce/woocommerce.php' ) ) { return is_checkout(); } } /** * Add klaviyo.js as external resource if public API key is set. */ public function insert_analytics() { if ( '' == $this->klaviyo_public_key || $this->is_woocommerce_checkout_page() ) { return; } $klaviyo_js_source = '//static.klaviyo.com/onsite/js/klaviyo.js?company_id=' . $this->klaviyo_public_key; wp_enqueue_script( self::KLAVIYO_JS_HANDLE, $klaviyo_js_source, null, WCK_API::VERSION, true ); add_filter( 'script_loader_tag', array( &$this, 'kl_add_async' ), self::DEFAULT_FILTER_PRIORITY, self::FILTER_THREE_ARGUMENTS ); } /** * Filter to add async attribute to script tag. * Currently only used on enqueue script handle "klaviyojs". * * @param string $tag filter hook. * @param string $handle tag of enqueue script. * @param string $src source of script to be enqueued. * * @return string if script handle is for installing klaviyo.js return script element * with source set to $src and async attribute else return filter hook */ public function kl_add_async( $tag, $handle, $src ) { if ( self::KLAVIYO_JS_HANDLE !== $handle ) { return $tag; } return ""; // phpcs:ignore } /** * Get logged in user and commenter and make available to kl-identify-browser.js */ public function identify_browser() { global $current_user; $commenter = wp_get_current_commenter(); $commenter_email = ! empty( $commenter['comment_author_email'] ) ? $commenter['comment_author_email'] : ''; wp_enqueue_script( 'kl-identify-browser', plugins_url( '/js/kl-identify-browser.js', __FILE__ ), array( self::KLAVIYO_JS_HANDLE ), WCK_API::VERSION, true ); $kl_user = array( 'current_user_email' => $current_user->user_email, 'commenter_email' => $commenter_email, ); wp_localize_script( 'kl-identify-browser', 'klUser', $kl_user ); } } inc/kla-notice.php000064400000006524146726766340010110 0ustar00admin_message_text = ''; $this->default_message_text = $default_message_text; } /** * Configuration warning. * * @return void */ public function config_warning() { if ( ! WPKlaviyo::is_connected() ) { if ( ! WCK()->options->get_klaviyo_option( 'admin_settings_message' ) ) { if ( ! ( isset( $_GET['page'] ) && 'klaviyo_settings' == $_GET['page'] ) ) { $this->admin_message( 'config_warning' ); } } } } /** * Admin message case statement. * * @param string $message Message slug. * @param integer $display_time Seconds to display message. * @return void */ public function admin_message( $message = 'default_error', $display_time = 0 ) { $message_text = ''; switch ( $message ) { case 'settings_update': $message_text = 'Klaviyo settings updated.'; break; case 'config_warning': $message_text = 'Please go to the Klaviyo settings page to add your API keys or to hide this warning.'; break; case 'default_error': $message_text = 'An error occurred, please try again or contact Klaviyo support.'; break; case 'add_sms_list_id': $message_text = 'Please add a List ID for SMS consent'; break; case 'same_list_ids': $message_text = 'Both List IDs are same, please use different lists for registering Email and SMS consent'; break; case 'add_email_list_id': $message_text = 'Please add a List ID for Email consent'; break; default: $message_text = $message; break; } if ( in_array( $message, array( 'same_list_ids', 'add_sms_list_id', 'add_email_list_id' ) ) ) { echo '

' . esc_html( $message_text ) . '

' . "\n"; } else { echo '

' . esc_html( $message_text ) . '

' . "\n"; } if ( 0 != $display_time ) { echo ''; } } /** * Appends message. * * @param string $message_text Message content. * @return void */ public function add_message( $message_text ) { if ( trim( $this->admin_message_text ) != '' ) { $this->admin_message_text .= '
'; } $this->admin_message_text .= $message_text; } /** * Display message for specific amount of time. * * @param integer $display_time Seconds to display message. * @return void */ public function display_message( $display_time = 0 ) { if ( trim( $this->admin_message_text ) != '' ) { $this->admin_message( $this->admin_message_text, $display_time ); } else { $this->admin_message( $this->default_message_text, $display_time ); } } } inc/kla-widgets.php000064400000022013146726766340010264 0ustar00 'Allow people to subscribe to a Klaviyo email list using a Legacy Signup Form.', ) ); } /** * Echos the widget content. * * @param array $args Display arguments including 'before_title', 'after_title', * 'before_widget', and 'after_widget'. * @param array $instance The settings for the particular instance of the widget. * @return void */ public function widget( $args, $instance ) { extract($args); $klaviyo_settings = get_option( 'klaviyo_settings' ); $list_id = $instance['list_id']; if ( empty( $list_id ) ) { return; } // check if the form fields were submitted with any value // if they were, use the value submitted // else, use a default (empty string or 'Subscribe'). $title = ( ! empty( $instance['title'] ) ) ? $instance['title'] : ''; $description = ( ! empty( $instance['description'] ) ) ? $instance['description'] : ''; $button_text = ( ! empty( $instance['button_text'] ) ) ? $instance['button_text'] : 'Subscribe'; $styles = ( ! empty( $instance['button_styles'] ) ) ? $instance['button_styles'] : ''; echo $before_widget; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ( trim( $title ) != '' ) { echo $before_title . $title . $after_title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } echo '
' . "\n"; echo ' ' . "\n"; if ( ! empty( $description ) ) { echo '

' . esc_html($description) . '

' . "\n"; } echo '
' . "\n"; echo ' ' . "\n"; echo ' ' . "\n"; echo ' ' . "\n"; echo '
' . "\n"; echo '
' . "\n"; echo ' ' . "\n"; echo ' ' . "\n"; echo '
' . "\n"; echo '
' . "\n"; echo '' . "\n"; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript echo '' . "\n"; echo $after_widget; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Updates a particular instance of a widget. * * This function should check that `$new_instance` is set correctly. The newly-calculated * value of `$instance` should be returned. If false is returned, the instance won't be * saved/updated. * * @param array $new_instance New settings for this instance as input by the user via * WP_Widget::form(). * @param array $old_instance Old settings for this instance. * @return array Settings to save or bool false to cancel saving. */ public function update( $new_instance, $old_instance ) { return array_merge( $old_instance, $new_instance ); } /** * Outputs the settings update form. * * @param array $instance Current settings. */ public function form( $instance ) { $instance = wp_parse_args( $instance, array( 'title' => '', 'list_id' => '', 'description' => '', 'button_text' => '', 'button_styles' => '', ) ); ?>

'Allow people to subscribe to a Klaviyo email list designed using Klaviyo\'s built-in Signup Form Builder.', ) ); } /** * Echos the widget content. * * @param array $args Display arguments including 'before_title', 'after_title', * 'before_widget', and 'after_widget'. * @param array $instance The settings for the particular instance of the widget. * @return void */ public function widget( $args, $instance ) { extract( $args ); $klaviyo_settings = get_option( 'klaviyo_settings' ); $form_id = $instance['form_id']; if ( empty( $form_id ) ) { return; } $title = $instance['title']; echo $before_widget; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ( trim( $title ) != '' ) { echo $before_title . $title . $after_title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } echo '
'; echo $after_widget; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Updates a particular instance of a widget. * * This function should check that `$new_instance` is set correctly. The newly-calculated * value of `$instance` should be returned. If false is returned, the instance won't be * saved/updated. * * @param array $new_instance New settings for this instance as input by the user via * WP_Widget::form(). * @param array $old_instance Old settings for this instance. * @return array Settings to save or bool false to cancel saving. */ public function update( $new_instance, $old_instance ) { return array_merge( $old_instance, $new_instance ); } /** * Outputs the settings update form. * * @param array $instance Current settings. */ public function form( $instance ) { $instance = wp_parse_args( $instance, array( 'title' => '', 'form_id' => '', ) ); ?>

img/klaviyo-logo.png000064400000000316146726766340010467 0ustar00PNG  IHDR;mG pHYs  ~IDAT8c~ %a`‚.0iLISgՔƐN~0$ElHME //eHKalHK{\ya(b`nj >>h0```0 m,P IENDB`signup_screenshot.png000064400000130434146726766340011046 0ustar00PNG  IHDRC_iCCPkCGColorSpaceGenericRGB8U]hU>sg#$Sl4t? % V46nI6"dΘ83OEP|1Ŀ (>/ % (>P苦;3ie|{g蹪X-2s=+WQ+]L6O w[C{_F qb Uvz?Zb1@/zcs>~if,ӈUSjF 1_Mjbuݠpamhmçϙ>a\+5%QKFkm}ۖ?ޚD\!~6,-7SثŜvķ5Z;[rmS5{yDyH}r9|-ăFAJjI.[/]mK 7KRDrYQO-Q||6 (0 MXd(@h2_f<:”_δ*d>e\c?~,7?& ك^2Iq2"y@g|U\ @IDATx|5{c *El;b(Ez%.%H'PJ7^.{KHBvgggg{wLH @*#6%     C?$@$@$@$@$@Ty9h     *C JTRmIHHHH 3@$@$@$@$@$* PJ&    2 @$@e(UvHHHHH?$@$@$@$@$@Ty9h     *C JTRmIHHHH 3@$@$@$@$@$* PJ&    2 @$@e(UvHHHHH?$@$@$@$@$@TySϠ/^(/TPAJ*%<򈄆) Wi"UMYfɢEСCr 9uIo}5jԐzًhb^Ͻ%SP&y S;qFWD y'̙3}+N+WqSN͚9c! ۶mFyjժUeٲe^sڵkvۛ޳[0I I $%d_~ҽ{x)B+b,DVJR0l'p1qI<@$@$@-[$88F߾j'@"HmիG*۷Oڵk'9TIr};^^ƍ;caX͛7-C 2yz K`ԨQRnD.%ޕӧdS.]*]tK.%H\bDҧ}&Jmٳg;Wʍ%>}zg*/\`:2(ŋIHH& 0i$%k֬#Gdܹ>/9N.9(He>hPƌzwܹSՀpG[Xw!W,c>Gmڴjժn(= I2vX3W~IB)@(C}<&EJ4i"3dtE7ߔ?C{ gyVV\L$po*YJG$@$@$@ 0zx+C O!% Nq޼y^GUvmYpE,E'ϟ߸:=yr${Fte$@$@$@$@A`Æ qq)B)@[F~W]?i~Yt>~ڱ]qc]v BBBEE%KҥK9J%I`$"<©)[jIuDq nظAfΘ)6m2 "ΥlٲRJƺ̽n:̑cnѢԯ_OlL6MX) 7EzcR!aM\ UVL?ѧL"W66ɑ# eiz{`3g =N5sٳnj*UdIjU}ɛ'uW^ӧˎ;p}XIKⰸ7xP$~#eE  $$?g>t={zII9[|@i/+W.7Rwqe nl q $:C{:눷u]gHݺu) +&CjaDԗdɒŘ0@<'P?~P %PԾK)X`\UsO~[dϞ]olٲyEwx%ZjSOr;~3.bМ䯿-[:2 >ܥ9VB, 7\xDzg# P\,M6* @ĵΐ1F?2Pre| q3X󤤘͙3/׬Yu|ց… / ={t=y $܂ *BF5. ,*NkEŐ%.E"_H5fb/P(AhР7.. ŭ*xS:t+6*_~XdQٳgeذafͨ͛7;5g٫j_c dРAq*B8?fMɺ}d֭>Ѓw_ aA^|E$hpGcVm>W_ًކm۶^m_>o?8n_ޟnu`ylӺ?P&zĉ2!n $KSu}2)p._g'p񄇔湾gȽ=0Pn<$U3$ERaå  v=_`e̙o؋bm;=W ņً{{̘1rÕkŊMj84xJ#u(B QpOe[_K/ə3gb5ϛm0C)[Br>Y;2  $W.O'=8 q%V#I=OO_<~s,/86HRe@'A }&XSmŒ1\tQA?Ǐo[s5k7V=cL#$y1{!c@Āb)p廊($N*H{\ "#P^yU1hbѵzKs$pb ) b"k׮$pU `łN 0bȐ!5kV2saqgpɒ%&iZH߈7 @RG b][UQM;F spUHyRbQ?zvc%J0s;'A өS,9 +ID݂"j޼y\ިG8U{UvH5:PKqEǺ?aZwH;cǎŪ'OFSut;|LTE˭>v4@d|s v*b^뫢 *8nt;GE8ֵƧ\Q\)?d u*֎*1X:/wܑ?dU353`Z\xEjFXmۂIHH 4 8C=븆*D^ xMU=Ij.I=Oµ:h1h2H"õ:#173fqH2ː~!tIjٹcwE6 hN/o>뮻;ű-DS \ K5(SX8ż̙XizGOAo?O2e2S[{HwX*{f+>?DOrpOխ[W??֥h1O6mvN'nNi R#(6,_PQHHH dTN鐭k6lm&;܌(q% A6GpS[Wpy{7ݧwT]:wGD3؛$Xoz G, zy?U㬃ucǎٗ<'ƪL~,Qt{K#?#I).  H d-SLѣG<=̲X/_IyVW^W|\BJ>'@)CBxuv膖WU-)>I_?Nu 2}+Bb'vUkRfPӜ ):wtU2aO7I3oOq'Hr IOݮUZ(N;cҷ"Rf ="   M<&(sx&k<&'<)'q@'9URmz'd\0UXWgX']g 7`~haM$ .ZNlӽ}ɱ(^ m J$pCne0#N?x8aKRv(ʸWq):ฮqo CJ](\d ՛[!\ )~ႇEaQ||HHH >|AyHMw5ewٷ s%ݺwvzOy_G% [/1Ü~ByxAUL„iA =9(?}I}_nl('.xSV揔*Uʟj^"΁d^㺬z6)u7 h@i HQ/u# /du=ҦM    ];wgdM̀ɓ62p󤤞'y^/po1b`M6R$Ki0!BAtrxxڎ!5+PJ ;q3wF;?o<F ,&RVN7|AR|?PVW'Ot%  &cjĉ2uTS) $<ɺӣF]ɔqnY0F9f1a 4h / }bz`b%s/|ҍO)ƍcu Y4er  SSB\߯K":&.pq >pt~dq-}Ϭ2x'HXlO԰7թ]Q9,#  "mt+z;H8aÆ^3WRϓ|]۟c .0*xŘqM!;ʐ擗/qȔO{9i$Wh2e]}u ~Rt HP yH{%@+_=xx77;s$^Ǜ ԛ  J2ɧę/!׿tR088ǥJ4n8{riC$@$@ "ЫWX,>qD,M`u*Iut| !Q[ 5yN I\I&Tܡ0kcϡ"`%'Sv-sL]˩Ɉm >!I*wv088 ̸ޘb]ޔ!ü5 C'A'rXe,QޒD$u3fh$ Iß)@5ӛ,\! @xMq]\]l#iS]bS$Κ5&9qG d݃ ˠx&;'`hxrJ=mq[oյΦLzRmRϾĵ{1m-7v1#4{.\`k6Ċ*z}>Ǐwm'ʉTM[|_W@B}+"Dz#b\)]F'˖-3fu8STe$@$@$X8-LG6ۏ'os5V$ Zi<$2aߤ v|'?p:*äqС^]PGu?e#LN}i5gW;qm`IKzpϝNq!ZmN:n/|-kLNϟX~O3) ]/>~oK9s:WDo21NnAl%sN\e{CJ͚50oL0_OՎ$tɳ#ƉB$@$@Mwq^筡'Y׵F[wos8oِ6mF`@+CH 0x`'x(3?nUJuʰaü$ӊjUz駟:äO2_0!t/ȵ ˍ7S0~+5g}ֳkҿ7(,r0:c W6*s6$G@LSV8Ċ!ʹ*Nu[n;#VwP5<+`=.NsXذn HH@ƍzX}FoV_I=O92; ҁ{C-X@=%HGUQxR>k,ZSqXp` KYt.f}Uk4oLJj,N֭[@Y$-M6a\)VUUtl޼(ޒ I|y-5ktL a;x~fՁ %8}E ;a҅/K mШ]L;ӻjmXW[=%C f8㡲l2q TR)?(\N)byĢxh"?o1^S4if͜c M`۶m& XV'sAreBM{v 2Z$:!ŭ-70iy&d%H_cIL I/J5{[ǭwLܑYSx@k;rUTLHNט(/Au}o>2:x3ZmXh) O=8ֶ" XM:0OeNȌ6a޹sXIQdWӶj^V~ۥ$i=RiƲ$zС4SEB$@$@7WoC<ݭ'YF5iixt )%@YsJ}猅z4h<3ό/[Kلejԏ3&IHH >%[L+"SjbHRϓ0^HaHqɦ aX 56ܺ \n1]&2/RvmoU5vXI!~2O)! &ΰyK?A={;}@!z v2%D9C؁E|ׂI+{n|o]҈z*꫎%ķmƏ3! HHSMI=OJ.g\zJ1BIU|Y߲vZ@Sz#bvio~Y"E5_V&(Z dC>>&3NX*"ews9sfDaƍ2rHEf3Ğ`">:/0̵Am۷,bNsV_|Y_i&c:+pq^`6ATv;+~}]Yzq?KK߲eX0n(Heq:|/d #QW|~n ,00Le+`\/{v2g˖(PHHHHH P 1mS qAVԞ,o9=rHHHHn|3ܥx[~E!e$\ RA"7"zEmqHHHHnV ݬwG=j,HHHH-CvG=ƃؠcƚoq4. ,+H bW` *Cr#ƒ%KL9$INX y𱎓[rSHHHHH P ; 8`̐# :*C~9>     GTHHHHH P ; 82䈅$@$@$@$@$@NPaHHHHH!G,,$    tTs|$@$@$@$@$@ 9ba! @2w#    p$@e IHHHH@ #*CXXH$@$@$@$@$ HHHHH PrB     @'@e(0G$@$@$@$@$Hʐ# :*C~9>     GTHHHHH P ; 82䈅$@$@$@$@$@NPaHHHHH!G,,$    tTs|$@$@$@$@$@ 9bIWEʘ{dCGN$@$@$wɥ+Wgg9s%;MflxnYcK!k;caxoz1qOϴ.uJ%υ}\e5FLe-}!    \|UUfl/!)/-SXح)(B۔.xr IlyPro2 ƋWɶC̹}o$jLP;NJ,_M ǒLh,Z0,^4WukZ)X먄?gzUh.]`' 88am\TXbI76ȹʉ -lytubIm;2JD&2t1wWG"W2iWSjehpr52Rrg Goo6<HHH 8tt]ʑ3͗U=.׮AWOǮpY-%[:Օ}'ɛsMJ%Zs?V :ڧsƶՋKǟ"+367Ǫ+o sXKY8vJ5G0mYdʐNn]u$i)K Rf3vA{%(ezɿ%T!9_3W7J?d ϐIanA^<T-K( `CGN ^(C/g3t;Aԕw$NqM2cUN뗪D)C8ʼ5{1뿃6eB7|?;u!fց2tA9nS㿇We(UMx*43Ut)W uH>-w}8_~Kn " $lA\X`zKXښV **Z7p)Co"hSB>{,)m!w'-KpsDsd?WEbyKTQd>S-`$sT|i"W{C E~nbN,ԟK51UULy%9EdV划x`ůMn~pTc|Y Ǯtu֕UyURXW ߚ֥NB)k\S?O SY ɓݔՈNpM'h}fZVP}Ǟ6)BYe*e J:5G9[IMXR`nS4(&$D|3O'O9jJ:5{{ʼR^%/5 d*b+WKW/8~knP|9ZN uFY}QZXRZE(+'*v?{y.E`R(zp^>02*vu;wN%\USv"NʪșE[%;?HciPs&a 'B%Wty諹Fʧ^Jvwx>Q@X&gY4ϝkn\֋54|@\8nT1+̱,OliH|p!5e? N6emƔi%Pڰ1%7 w,X. +?-6U_}lT+ǒUɴo7ƖCA,`%#^Av& ^g]!kSOSp U`:mֶǤ_QKڻWC :>G2eL/U dzHƺqnd<^O,tU?~5S;bWWMRP3m嫅S^$Aiy̐NǤmqo2W],yX4WF xuSRE!GNIZ%IC$ vqY^ *dB[} wc|[&1T5He&/+ҽQYs -Ә"rגƺTJ1Or \ J?\C뗧\P ?j]i5dOKg/-Ϋbd͏%a.[;!'+W5>P爈qRek>RectL4: i_lWlS%Q0OC2mhgY#Mcop݋`h) 画ɒ}pG Y,CEVUݓ/垀]=[#Q>/$ߩ6.\ w=pή#1}H/IPl?|JRP_6jK`^ݮ>([ȥ@Ye-aZ ;լ)xN$D^+Xnz"S`.mڦcOx<}g4=ț%J)ۥmb|S&  >Ug-^"U\L@`1Uk+ؗ0v)f1sX  T3<.mnՃc[r?xkgZ2.YYa=[R"oVk3vF\WE%dPN-jXF97#K]E l /ֻR /ꏀۡ8wnj_ 'ϓSdC$Zd|Iپr+6 j#Y yf`|rN͸5Dgl W6޶u%m?,VY |'  >Hjj Cܓ1Po>`e-F/˚#xC²M\c4/'.ȴu\2T 9]#)2s")L2e-iM__?._o3YV#e5l=AvsѺR<p3^I/b?,oDz_#yf&u{YCfgt*{5Zb{l2oI9[],!;7 \ ∇eC4 Ғbψ{],J|ml _0yg:,}W@>8g2uJ%rg3Ȍ2yT;f^W5 څ0n+cI@<'<~H2eVGQ2kW~t|_R^H]TSrZ}R5o?q_՚\d3*l-5.+Fjv tl2E쵉rA\c= Ч2eq=]V*lu!?YVRaIT2z|>˕Ʋ>9U)5(4Es͍o4}45:U]+EFO;DW5ϋBhw    `2^VҩŪ0 .vPC,k%hגгM3)s,sOKwZ]wk]NR\R|U-&cl.rj֐>?.$'U`)/,?3u(@)CyԈ_NܱLc>*l=$_ڌbG)+Ƽ~U4}4bp^4.*HurGODQ?E 1m"Od'hf]G~v,gܥDLJt|S_rj'by2EN&d稅_BraxtjnTS"<*0HOM[3!if TnZQkR>C_̑ԪZ!$@$@$@MnitU[HAc* k,׸!+2˵ԁ|93NɩjiTyT*uu\4lRNv"%IP˩iSvPY%☺jr:O,8yB3}HRwibKn<Lw5[Lj:SG'ZsQ5%]),G_vA*=x)jQ >ֽ մCǙMiXQ;T!:e*FoHlKMBd%֖t]MLYO+` Ǐ_`eǾLf}#cՃiDV5$Ǟna$OI{ !圍.I 5J@.xFY0wq)՞> @ e;ZG'Mÿh9sYd4I2(Cgjk̝~x٥~Nzq[]I)%hL8dS0듚퇅1˰ՇxA&bTC=3A"ZOHao,lBu=RLKCe[NTo-`>;Lm.M}&?IfӒW %@4ջLmwtZW'ٶ Gάfo5ݣthXa6nX`5curexf.uj]bjX.X݇4K7pWm >B, !  H0Iw)6@:z\cw4kX12"}Ք#dz{ly*~_D,}yԃ :q!iX"dʳ SR4aa?BU+rb_Yʶjқ )Mзç.Gf/E|BVxKa9#Uy vjdK?PR y bj(~q)]VRZ$@$@$@G L8=q^:!O =!dۡȚ\/>@ON)Dc0grJɪ %nV1xGrm[$>p<2{QVЅT|#     @P Xᑳ]aʞ#e0!/x+c\ 6@$@$@$@$@75*C)a1W4i'=Y^zR[)$@$@$@$@$@CPpLV:)ȩ .TTމv!6D$@$@$@$@TIHHHH ^-IHHHHR*C6s$@$@$@$@$@ y> @ @e(UfHHHHH!O"'    H A x2I$@$@$@$@$@Tq9H     OT eiTt{ 텈u"[KWbҴ|!yuUɘ.˻SכSd {57#"w6ej+ڽ<&QRZc(GPSHHH=@IDATĵ!ob90ٸ[CTG֗]Ƭ#O|@F\q[Wh\N2d=H]Ubm]~X(8.6e?k2n*sO_t3WV)f{Ů%UXdޖuQNYʐىQgL @"wV+.KkYEjT(dy|2iLPɒåE.E(M4Rd>)R(,QeYqƥHf%sLܐ'ߵHU(uJ*e ]baSdVΝ!@PKHHH1+岺AJ#+-Y3FM#~d9Z%kǪDE{QH }َM-j#^MʛwIɵ+dWqzעn|6;4^Ac\>g . APTHHHQ5hS.E6.7#Ԃ;^1B)ϥdڲ`SŤZ,X0TQ% 6N~2&V"   Ip9v5UL]Ҧl=|w,J)mU2v-V"ç]j-RB,٩1D*Cź$@$@$@$v] {fkӼgVE(S sp)$UWYAmk#Oe1KQv(˗#9m}m&_ c$@$@$@$ llbnswEۨ1=]rQ\$.?lmkce1{Ql#Cf{5\t]$YKrrb6kU禎y9j   2%s+1IibUƜrtݢɓ1nt6YzP,AJx*,+zuU6NG} YcTc% Ĩ=NHHHHgGD껝`]?Og:ucWJGI:qܾjHLQ&b2A!iDԠ\P+wi46^\*/;D%dly[}ə9|2k\T$(s h[lWHBzSJ2i+w{va].=[(]5aý JKlQiǹ}2tCHHHL)Wn DINTTF^)߲-ZZ3}[yO'd ;sA~Py]ڷ ԷIa$\9$ }1ۭa4KA:#E+jX9yd۪ a!k`դ\~*ChF>r$@$@$@$~B>RL%(KRșUʗ/}Ք/iA~:{Ҫ^icB hwKbƝ \N[LTkX ;c6jm yds:e7/4'|ޛw9 @ #aR,OA'9cg͡Y2JIV9*H)U/G^nWKV;2'"   !0Yn_eb牶 KvLIբv񼶣qoy^ΜR.$oYʨ2kAyibc N.V8MF;pE^&_% nuKY*C~@b   TXk^\Y^R\5 ljk”}9eض͖XR8Kn]J~BrD[VCȕ+;hEY]PZC&&ϷnbTFi-N૫ېu\+U)x[]1165/YWJymRTi vbU3F@!?A f 5[~Y"{RPĴջefrEjCGN ^ᗯz.NB4&|z(w;u!ֱ3w dڨcJƨ-%st5UN:j˪xmN.4x|[cɞ}*p J3ʛU^9(ENZWU(&U²mhwk$`iy& .5nZN/a9ɐ?WE(kY.@M ɓ-HT.*sdq5'OvSV!qU~Et|OS_ƥmD)?s1)ߩRIȐHRA΅]/0 o꽮SaiY)J9[-y&)9wy^Y~;4Y-(XOl_TuRgp;>bź$@$@$@$@A TZ8jGXΪhMsݹvI9q>\aKXS_ u{Jеaء=7*e eç巹[a$"XN7R8*Ck㌻ VE "KvD '=ڪ &Iq媄rqI3E+Tޮ򰋗eAN) dmf)O dJQ}o#5*1TPKQe6G@m;,6A{NBA9}ZZR(GfSOʐXHHHH@ SOWR0U0W^TG=d!&Ҡj1);HIM2P\!٤J5Ul;IG/z\U؁?KC̔@kn$|@v<`Q%RM"tlB~SK- O?9>N!    Q*t{Mk}_u4gKb,uL\[j>ҴZf̱b wGբf1IB*yh&yCcx,YJ v6 +X5YεW|UǺ$5 8$(CZtG 7)&ڥ*r[\n5CTgŚ$@$@$@$@$`mȚf6=8D.Jv>JD&ӚG-@weHݐ[#!:CH%F5jii)u˭>hbzJHEmdE;c<,SgJIǺXF"._7;ֶ& <(a Pk @83GLwWu?tcZ]'q]#*UuwinV9/G:Ds_KZ+mq (H~3y - Ϳ6kVUv7I`ƍxVb;I6ƹ`VgйdmOOu#vs`YҨd~{$@$@$@$@$¯\s[T41z-Rv \qfsCSV2o6DD0ѝ8@.^uQ̙9ɗ]ri;qW{BJdHs%J PJ8;I$@$@$@$;yΌkARھ!Q" @j$׷fgG6 SҾg_R㽊3_bO$@$@$@$@$h A $ٟ`Hנ2Hwc!    FŊ$@$@$@$@$@DP MHHHHHoTFŊ$@$@$@$@$@DP MHHHHHoTFŊ$@$@$@$@$@DP MHHHHHoTFŊ$@$@$@$@$@DP MHHHHHoTFŊ$@$@$@$@$@DP MHHHHHoTFŊ$@$@$@$@$@DP MHHHHHoTFŊ$@$@$@$@$@D } c!   "E;dI9xΛM%ꕒįA־Kv:!w )W0R\ı  N$}Jddϟ]#/%e dw<ż2c~ 9&g/DH|٤^۪Hˊϻitp76 !y}1QzT,kU:C3'T-Kj~| 2t8k6ZLFUVߖՈ+nNiePzZZVy_*]>%agλwZ--i؋o6n(~^HHHn (BߜJ/eKY3:qt9}!Uƍ9g<\eٮ7ǙP\rf^Ygv;cՖr.2kAyib7EXRX׹׮Wɟ!2X~șU*- %jnx\  $/glOӦO+l*}Wto쐁cȄ~%o'y"PTӶ jc;2wSaantS:kɝ',I劲?u^t6dFV7_3,BiҤ)*Eą]gX!m7y޽dJ ; @21+6w]Vr|VoVQV{T]" ɜ!f{E>clwBv=#sgErKtB,+wkRLjTNSk}JJ_fLesNglO._&M+:Օz̒{ԪqEi._V>F ɃJrqX1&'kJ96UmKWʈdŮc)ɛ=T)5zj1ːd6M(ՙ吜:Ikʐu\5]f"r/_^iWUT1q$se% Sib2m᫇HEVLpq2AXD.Eep%ňF)geՊ!T̑9|ܣW%*\ρ<&5kJOTJ @$@$@$@$flQ.U qwԓ:aJdPiQٞ6DZ,]4/44LV7N2<ש/K2{UE"/~HQm8rJ>@Z0Mreɶdd;]\\ +n,=>!W Ÿ%2e~y헨 <$ևg}Z#MʣC/g~U,El2Aԕwֶef]^cQ чiw7:Kb𙋮[:rZ Ga7{d'TaB?.!j-]-9V;y/3+5ΓA$,zgҚ*DY3ƨ -Myu{.K._#g,tjeK7A-d҃ n!LygeCVnCRf0plT2<قN墒=G׹?jD'Qءmz,z6VJs+6⻱8qnh-dCRO,~Xviv8oҦji3b|9w-.[7ׯ|˂ /u<2[dė|xKyQy9.eޝ,B׭j}m@;XZS.HwJ9I/ʑYFdY`Aִ?&2ɰh^_W*SQeɯnt[Ԣd~W~Zl.e;]culEL[\8.H[!dZJ K嗅:G[ĺ6,c\Lo#.}fPUIG?iתqki~5c+2cp*m̈{ȢmӦKw (uJkjVC4,+q֪ݦn+flEb2] t7ڸQ1K$@$@$@$mi3eE̒N.J`r!9ݡQP7pl\^-?n:LF>\-9JUe@\ie6Q5m;{X]^sj}Ne1B]LAԺ$i"K`) =wSk,٦)t2I;% -C)n/$@$@$@$ JAw0N]AaQxkkkU*xQT#xYRV-5v)'昽ܾ],:YUV*ⱖ@)ʐ)Dh3R1K>Zrm)LU2W+iGEXxů&褖eim{ M`~,H-D#4JMuVhҒ&̀_T7e{-\jUH] /XiZjJ)"*C; 42@6sv=1L?SK\xh+ C x_YLp\8 %|rx 8&ϾvrX"PQY!JjRkZ|5jmnrPgپb8,C2KDBٿlڀ:"xKo.㪲ZRoRwZ*5@,Bd_ˢ)ԨM˛IIڔ3t<73s3e?Hh1sݸJ6mw\`4(  @ZSL#5tfo-%8VƝfc{҂3Ndf͢|iR5Ou6j;F0N~n2ZkV+dɊzJn?ԗTYO4][\{e'.XE')(wn G5:N4Vh&A|%-kh7Ҝ?vs>C;4  vk$+Ws#/K_y f_/H%]G]Kȗp֘4(Qs{nx\ ׁjm)Jj.6NZ+dFU䞓[{OˤV0 Kj_4Miރ:8ktQk} dˎҤVyi5/D?KeѺt͖`vZ鼞\^ `ր\Ȗ=KWL%,5tK:gAp6}xY&>\l^B1eț8DմVmnZre8ձ|)W:Iis7ϳ&%ǜlKm4R*]s\#yꛉbY'ce܀3tyMкQ5}Fj245WHFU]aӖAlkhW[AexQ H1hY+eJkgzD3̋  ĹkWXR! n t|9];7t)ԫڏRng)Wx}MG֠< og ,K`~)QD8@@C-pkf2gI5*=^Z}\ZrǻS5cu H^'?N߬)46ݳ}=ѿޜ@H՚Z=+IiO{ yX{4;9[vѯzR5ƄΔF@@ J]W'G#x  y+@Pst@@@~k}>³clB  ė~r5   MlB  ėP|O@@)@0M(VC@@    @6 j   _Cu?@@ȦP6X @@K`('W   &!  @|  j@@@ Cلb5@@/\   dS`(P  %P8.A@؟<ۯԫT*xo+]ҩwt݈Gku{.U4I>x7=s&љkV޴tS)uXCuZ@HpJ?]{Ĉvu2Co2[FMY,P C04lrny 0j>z,~7:D6.$%x$$s  E v FYLĞ})2~`h |mbm:4" jtOKEłBE ghɏ3HLļ\5 $ ͪWMJv۹m_&xo[?i`:?NL}\Y%{JˆC܎ &@@ Az5.IjȼEkeR0_@-]/;vFGDܽ$ܗiM>7/*kӻR1m =?oٻO7&w?nW[kc|6g65ʋzl+=V=7lT<$5ܱg<9hJf5/UTV;Bn8U>Vmi%xf)kU,-v+iBQjK6l'z-\n),u+Ol&7uދ7ʽ_wvD ỵaSk}֖-[e<1hX;w?K//[>BOo'Gpbz߃/֚2ڻG/=V.\ʫ >\܋5Ze%IBiyv/PEݪ_]EXoS3oo ' : lԚ1i/L?nQ]v'_-{AiB=Ww'h #8g暀ٌn~,t_s~)wk'Kם(giS6kNg$g?_5.}[}Ur#_MjͳChZUktH94Rp/y[B+9bM<5U+5 2ϗ_Iz5)좎<-}QjzRPkM%>WЦrVfe'jҊދwJzsڻfy)"=7_n8%7h!-Cgs ȍ{rBZU@ X >}{qI-[\?"(M 壛   jVk bīiJrDZ P_^Z벹՚X}Kjh"+3o/-ԘyݚT Yi}t2ڧu-?^JoְN)#Sk Lk֦(WR޹ lYU@[2+,oGj5N7l!6­f\}k IZG.D#˸x޹|9r{o/XqrZV?[9 ;6Ghly/>Cnp.  (p\ ivAZ+4=7~z Վt J]tdbX*_ζ)6?\J$69/Kkҩ}٘Z՚ b#D7-`]j_ȒGdEk\ѥr}wwL>F7]-rtLH^qzQRlji ةFJӻ?_ҜM(FFY=_ʥB<iSL |=?)}Ӿ4~5޷M|Zv$lY`Ӛemx/K>[Tu7]Wkpʣq+7_j[;\?;5ĥDIS=ЧF[,uVʈNz(E]#Y\2pASŇ+5C-@@I +>մzS7uL![ХAeU̦:o{N>c'RO fL(ZӪimơܻiPh#iX6OrͺiׁBv6/'ݫ՜ܮ{w\(Z՚Ǹ>+vˎlW!ZFim_S]3DkyV7YbfP܋k߭^_jp˺;eT~Jr8O@@ *,*uocKmf |ڊ4 AI}~#>;Mki9:RFS3)ֳ/ln1e[$Y&+??DЁM90zlIrGib!rm&mLle67D& lДFJh#gySK/mo|?9_оy_ȗ[5Ȳ,8>ҥ~Cr/ZLWg|.j?',:^ Ź" ,p:`W?TOPNmbWƣcN}_Ix~px؅N`sˍpvkKLtÿw~߲\Yfr߹ȀwGAP V]Qɋ:ʵK` #:U˺q>>ͽyP2djjl\0t(_E}fuɤO`~0ڟaJ3vq  @ L ]Ni9ςb&?zpTھXbF;CԄ9)vl [\553Wu>kdWZ3=sŦ{'#Vӛ]޽$7P_׿,}o3Kt. ׵Cuk[[w I~P+qM^F+;wdzcݵ|sҪF9hMĝ4X  @঳VnҾ3{tҲy8ٸGJ<;tM\ԫX*C:m[PխPj)|6[JN3o;bZ‹݀{/ ߼5}* MC`;A?+M4 g.q  ;Mw4;_^V:љC97c @@JOi}­)dRR 9ͳ1F?CJiå .wD@@ a<|?*;קY:}]v du;c/;$%%%~9e[%9)9X>l0߿{o 4o>92hР:UJ}hѢ࿏>_q8qb 7 o4k,rf  Cyϱc /wlmJ-$HRaʔ)ݻwϒ9ٲ;v֭[,|^q[kNLo7X'4 􁇂@)|: ~C`Ĺw>Lh 1>C4Hч`_R&NRH7_m4 f?)/b><^SVZlՄ v=zXMee˖ i̷|g.v|ۗ>Xd5-_6lߞq}Iчu`>(ݯma}NѠ&ob{wa;v؈7xc쩧 ~횴Yeͳ{U9{o{'EԈci2ppY{ꩧmwo9bάm~>b{M0gb#;]vJ'̰?>>L &3j`=8{  %`:Sȷ!C֦h씯:Eܳ A^kC})k{oq5`Ȃ&{ʕ+]s5KxCvLz;t>Tvp@ٺZC8vܾ}k>= o@ݮG(䕀9Xӹ^xAkm WEc@W0gMXs+Wn'Ekt&Jk֬;3ؽ5 ;ŚFCkr4xࠉj/Z3S{`qXy饗Dݴ5_fR\ΚYYz\wun:kt׊^9/z={oO7Kww լs3DHtXDO5o^jgjj,E/Y Y1e__^O8ᄠF'bA&o  l@]_aScسW*:ٽ>`W/si׮]jY1CJ=ER?udV]/Bb-uM7ݔխOc5?Vs]WfIXo1ld5,XOF= o@P!>V Y){WdfMO}w>ZKkby,g7"m2. gY,i>o3@,:k]5;в~i1q~f/^8M[kK˱Ϫ,L6ܵkW*W97<̊}wqSLJ`[`8AbY۬hj\uUqF҂*_Ԛ=.(ˢ_-âe%󯖵ς-/eVz3;@[`(z x^{5wW]u{ =x[k3_jC=LܹӍKTr`/̨ ɭ#1l n>mSF69|RfM*یKXM7 /r6xkv6 6t<#6]$f֯_?b@`~yf̖g5/>\cYDfpx Y>ճTZM,4ov/c.ƛn V 0Ub?vmbcnEu~Eϕj3t~={N^մ!–otVfzf"$@z.>_ `ك ]Wo_l'cxgխf'ժFM=&}Vo45sĢ/2x( P|{$6PtpD h׮KѰ},م/`c~d"'1gY_Ν! 5KJNr_w^Ab.]5 w﵍6hC=$w~t`cci_?Em+~0Wj,)DJnµ 5D,ߛ\ڏݻwec_Y d{ /Qȗ5Agj fR4qCOu4[WpY%P'.}L wʷY%PG}sǷUl[ϟcIs|8UwV vR:y,&lܚX%l@!خ{M~#)zD  'PI w,baСx#똯Ƃu]jpb?Ek mf |m+D8Bv0Xf Ʊd^q}Ŗd{k86oK1#|l i`!;g% Z&[)t.G [ ;{ǵϵ}~rJyS4J,E ]veڽ _ϗpC= d&`'([M<@^v1YC6Jfc>9PCvn4bijBwYC6Jxܗ@`ȶ:@u0d jjU0djc#Ղ"솾he6@pXpm45zIVWm]xOk؄6a̰ h۴wk"G//R`K 4o^JN*X`ch𓭋~kRd&9st}8}IS=~L4ٸ_6L9XKo62VZ4p k5%'Ae,袋";Y*;Il6??yb? ~hG5}xCЗp^ڌΗڵkKMS4]g'2Y;1|xHև7+[:nh˞1}l'Op}|.K1zhxXv_|h-c *׮P"aᢅpB2:9: "Yd YfuZg/WKm^:gUlpN7ƪիeY@ڽK&O:;Ⱦ] nj&I1X"IEN:}c ,5u֑"Wu 'l} 1s ns_VY"ShF?vLe۾c4o<"d3۾`R^}#_+ R`P|'`cXu{k߾5EfcKm\p<' ăP ٺmkDڄYg&%!O}N9I6i$+V~"K-@`h-2i,1k/WFthB{^Wk6,@@hIh2m|֩TT^?f3/^(V4"J.$. N7atvB)2x٬_ukU+ʴ1m4@$ sm䖭XX063xh)RJEmOHIa;i,KWWI=#+WpA+9(s7d},Q5}8^ 휧Θ+O"۟5 ;]fM?s?(vXFupメڶv^|͈`+/ : 8}١?sr+{+d'[6o&_y^J,){{ ϗ5K>g@ IMܳH;np;\\}+^-o~Ez]Vb?6Z\.F8iJ&@8ɭ^^>jpժպt<Ӯz)FJuk5R#\U뤧m7jʯ;vmy[fVm r// R%I#h@m$*m[4 Z_'WƓr;Kf =VdoՀ鄮Gs8usY~*'۷GrztYNjI7o\ߙ pn̙;O*+{32fruWނ%N{vGָ;Fl ibmkZ*Xcb¤iRZ4Я|eQxEGw͌u@V|ݻRۦQҦdTROFZ /D%RfhV+?5[nFtfjS>(Gw9YAG K/wO巃 [W׬?qMw[/ ot +0Dhv%jr5>])[~k#1`h&#aL/i'f:?<?Y7$4pҡKh0]3m&}V֔f_[i- /u<&d.KjYv;+c6Uߗ&f{6n׎;4$gլ.@J dWC%@ULxtM;Aȡ uw5ɽI=Rߖ\)iO?q֭GYnz' G|IsʥG'[Ľ?,hxUr筩߬&^ 2=g;\ʼ8C3&MCFE:D1xq|'uw}kdefine_constants(); // Include required files. $this->includes(); // Init API. $this->api = new WCK_API(); $this->installer = new WCK_Install(); $this->webhook_service = new WCK_Webhook_Service(); $this->admin = new WPKlaviyoAdmin(); $this->options = new WCK_Options(); // Hooks. add_action( 'init', array( $this, 'init' ), 0 ); $this->define_admin_hooks(); /** * Plugin loaded. * * @since 2.0.0 */ do_action( 'woocommerce_klaviyo_loaded' ); } /** * Autoload inaccessible properties on demand. * * @param mixed $key Key to be loaded. * @return mixed */ public function __get( $key ) { if ( method_exists( $this, $key ) ) { return $this->$key(); } return false; } /** * Auto-load WC classes on demand to reduce memory consumption. * * @param mixed $class Class to be loaded. * @return void */ public function autoload( $class ) { $path = null; $class = strtolower( $class ); $file = 'class-' . str_replace( '_', '-', $class ) . '.php'; if ( $path && is_readable( $path . $file ) ) { include_once $path . $file; return; } // Fallback. if ( strpos( $class, 'wck_' ) === 0 ) { $path = $this->plugin_path() . '/includes/'; } if ( $path && is_readable( $path . $file ) ) { include_once $path . $file; return; } } /** * Define some plugin constants. * * @return void */ private function define_constants() { if ( ! defined( 'WCK_PLUGIN_FILE' ) ) { define( 'WCK_PLUGIN_FILE', __FILE__ ); } if ( ! defined( 'WCK_VERSION' ) ) { define( 'WCK_VERSION', $this->version ); } } /** * Include required core files used in admin and on the frontend. Only include * wck-core if WooCommerce plugin is activated. Always include analytics. * * @return void */ private function includes() { if ( is_plugin_active( 'woocommerce/woocommerce.php' ) ) { include_once 'includes/wck-core-functions.php'; } include_once 'includes/class-wck-install.php'; include_once 'includes/class-wck-webhook-service.php'; include_once 'inc/kla-admin.php'; } /** * Add admin styles. * * @return void */ private function define_admin_hooks() { add_action( 'admin_enqueue_scripts', array( $this->admin, 'enqueue_styles' ) ); } /** * Init WooCommerceKlaviyo when WordPress Initialises. */ public function init() { /** * Klaviyo plugin init hook. * * @since 2.0.0 */ do_action( 'woocommerce_klaviyo_init' ); } /** * Get the plugin url. * * @return string */ public function plugin_url() { return untrailingslashit( plugins_url( '/', __FILE__ ) ); } /** * Get the plugin path. * * @return string */ public function plugin_path() { return untrailingslashit( plugin_dir_path( __FILE__ ) ); } } endif; // phpcs:disable if ( ! function_exists( 'WCK' ) ) { /** * Returns the main instance of WCK to prevent the need to use globals. * * @since 0.9 * @return WooCommerceKlaviyo */ function WCK() { return WooCommerceKlaviyo::instance(); } } // phpcs:enable // Global for backwards compatibility. $GLOBALS['woocommerce-klaviyo'] = WCK(); // load the WordPress tracking and widgets. // Makes sure the plugin is defined before trying to use it. $url = plugins_url(); if ( ! function_exists( 'is_plugin_inactive' ) ) { require_once ABSPATH . '/wp-admin/includes/plugin.php'; } if ( is_plugin_inactive( 'wordpress-klaviyo-master/klaviyo.php' ) ) { // plugin is not activated. $my_plugin_file = __FILE__; if ( isset( $plugin ) ) { $my_plugin_file = $plugin; } elseif ( isset( $mu_plugin ) ) { $my_plugin_file = $mu_plugin; } elseif ( isset( $network_plugin ) ) { $my_plugin_file = $network_plugin; } /** CONSTANTS */ if ( ! defined( 'KLAVIYO_URL' ) ) { define( 'KLAVIYO_URL', plugin_dir_url( $my_plugin_file ) ); } if ( ! defined( 'KLAVIYO_PATH' ) ) { define( 'KLAVIYO_PATH', __DIR__ . '/' ); } if ( ! defined( 'KLAVIYO_BASENAME' ) ) { define( 'KLAVIYO_BASENAME', plugin_basename( $my_plugin_file ) ); } if ( ! defined( 'KLAVIYO_ADMIN' ) ) { define( 'KLAVIYO_ADMIN', admin_url() ); } if ( ! defined( 'KLAVIYO_PLUGIN_VERSION' ) ) { define( 'KLAVIYO_PLUGIN_VERSION', '1.3' ); } /** INCLUDES */ require_once KLAVIYO_PATH . 'inc/kla-analytics.php'; require_once KLAVIYO_PATH . 'inc/kla-widgets.php'; require_once KLAVIYO_PATH . 'inc/kla-notice.php'; /** Helper Class WPKlaviyo */ if ( ! class_exists( 'WPKlaviyo' ) ) { include_once __DIR__ . '/includes/class-wpklaviyo.php'; } /** INIT */ global $klaviyowp; $klaviyowp = new WPKlaviyo(); // Handle deactivation. register_deactivation_hook( __FILE__, array( WCK()->installer, 'cleanup_klaviyo' ) ); } uninstall.php000064400000000400146726766340007305 0ustar00