From 0f49440e7b3042b9a6fcc0fbb0a645badd6b40db Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 26 Sep 2019 12:15:39 -0400 Subject: [PATCH 01/39] Created modal UI --- .../roboto-condensed-latin-bold-700.woff2 | Bin 0 -> 15640 bytes app/fonts/roboto-latin-bold-300.woff2 | Bin 0 -> 15784 bytes app/fonts/roboto-latin-bold-500.woff2 | Bin 0 -> 15872 bytes app/fonts/roboto-latin-bold-700.woff2 | Bin 0 -> 15816 bytes app/fonts/roboto-latin-bold-900.woff2 | Bin 0 -> 15712 bytes app/images/panel/checked-circle-icon.svg | 13 +++ app/images/panel/insights-ribbon.svg | 58 ++++++++++ app/panel/components/Panel.jsx | 81 +++++++++++++ app/scss/panel.scss | 7 ++ app/scss/partials/_fonts.scss | 44 +++++++- app/scss/partials/_insights_modal.scss | 106 ++++++++++++++++++ .../ModalExitButton/ModalExitButton.jsx | 40 +++++++ .../ModalExitButton/ModalExitButton.scss | 36 ++++++ 13 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 app/fonts/roboto-condensed-latin-bold-700.woff2 create mode 100644 app/fonts/roboto-latin-bold-300.woff2 create mode 100644 app/fonts/roboto-latin-bold-500.woff2 create mode 100644 app/fonts/roboto-latin-bold-700.woff2 create mode 100644 app/fonts/roboto-latin-bold-900.woff2 create mode 100644 app/images/panel/checked-circle-icon.svg create mode 100644 app/images/panel/insights-ribbon.svg create mode 100644 app/scss/partials/_insights_modal.scss create mode 100644 app/shared-components/ModalExitButton/ModalExitButton.jsx create mode 100644 app/shared-components/ModalExitButton/ModalExitButton.scss diff --git a/app/fonts/roboto-condensed-latin-bold-700.woff2 b/app/fonts/roboto-condensed-latin-bold-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1a32150b536267cec9e4985f03cca89a5614e17b GIT binary patch literal 15640 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 mNXHC0mTw2?_<200$rp zf>RqJfEDAI1#BDuK<=H@$Uv}hfZ!N!8yN^T4geDUIKls)jvGS+JCH74i)094RWh61iPGc=?gL5>)AA3tJleVZ3@DW>~%mPl|@>)rBa~d6!GQO`#hk zqGae@il|43DEJUgr`*c(leLe`R_Pz@O1KbQNM%%8=YmLr$NA_Ca3@D3>^!15|7Z6k zrbs;%JPG2#f`))6xCYVuKB_?bsfnc~Mh&ZohKI=Rsn^xjGbGl*9)tm_|xEDJhx@9iS#N)pdEm$H{8{$vFc zg8Jn*yM6I(-Mf$^VIfO|qdeIuSmKa_i(6k2lXDomh5GUz>Leknq`@NWDT{Yp*|gG` z9qoyZ#y-rfJ65J3^WAfH6l9-$5UqMw3p2Z7+qcqz;K1^+;ecfy$g=N&Ap?^11Q30F z_#smI83)M+(Z?}x^dSXd@j@7$}$|f|8(YcRFXh%i)`8UDsX0aoyMASJ0KXJ@%zWY*lYws z!sS4kG(lRlK*ksYY10OoU;<>4DUh``z>fL?K`;O=03iebL7E@{Y%T(kJM9va1ig=G zs;`9JC)5{LK<`uY8>^u=5&*m)J#or$_0_-xTn9+@3S;M$2Ns50k1PsVtKN7~D-rc9 zfm|&4Wv}3gh0;2D$BX_qBIO&wr&N%Cw^DemNx*Kk~<-Sf`y7hSJmDk>Q>z&WO`0AVQ z5T8DQq$1_VRsc))SwbY8jSDsk_l}r*vk5|(5+73Gy`|}^$V4leUUGX%`_lU&qffF^ zOz~m?TO3QNIu^unC!BQ3>8icA9(d@H$DULjebuQ;w;r#&_QqT9s`q{I!AGB}&;64x z9VD|+YrYGxG<+>n6rbKvwac#q0lje_LAsXW(6RDnS1^%HNNHGRFH{w5M4?)OYOC3V zo*?l^1rvokJMu|Dfa~i4ei9Gvp(#7!tKD|TUH8_uLKEWb;=EBz(#4qK`R02&GQT1mnhr!JTG5hC(Vmf#KD28? zU#I$NUutTto&D11edPKa{gzhx)_1JE)N#~MhtBJuYOl2DyCYWQ)M1@~*T>&6#=%8* zn}=4SFhn4%z03!GIRF!9V`aB~lTs<@LyP47@be6V1Q+vN{WA2^N zL)%6GMS#L^AKIY1_bODQ9rT!pxT$tJp{@#_i4yXRfJgzVYgZden>R+yxmMsIth0$i zMC}fOO=};L1zyG4egle*@2k`yUOwBKH~t7O*MQx(eQd6LnEL-A&MqCu*>jolE$e7^ zh-qF=8N0Fob&*;1ozH15^7+`Q24%Yag*ymqq#V(`7Za&ZsqQ6W`v$0LU@Gw@b9YxW zg4)vHs`uGz5MH)Abl6(&Q2x?X5qP2p14_29S+DiU=BpAFUbb78>CbmABX#Sb4{IT# z%)hfjIbNHOt)A=q$0lJmggBam1)jb5~g_7xM@PogCKrO-h#0td^IV~B}? zcIH{k-qO3PbW{&#FTIoOv+putUMUbOVnzDkJ`yNqOM9`SBK8z;dXh-@hlF-uXsq?}eZ=0p@zQY-Eh^Q4$J<4fqpCM;q> zBt$?sLBB_)solz3*9g{FQZ);;?lZj$eIwEt-Whp8V5h)Q7 zsV#K`+!BQ5SWbcoi*PU_-2(|p2nl1~`-CD)OSDOvwZ;B99fcZ0qY7jDBf@2nv7;`@ z4BbT+Jj#FX_?Xc#`vj-Z1L9+c9;A!r84M)TB-%;ZTmsdKSpkZV=K4 zv*l=S{!FBgs};u8`PL&uiM19sTTvVLB^zjhbv%K!cT^-YHOs679Rw{GLrNL|3RYI! z3$WR4h-I#rKeny5*lZhKE8kB7@)*;Ltd*+RjGvk=2| z_!xeKzp_Tz=0lAAf3x}jZUz8&WR_cDi>;11>ze026xX6-Ts}Xnfw%o3eD7ZRkKUtS zA0z6cz-(qeyJZBP-`{h8cmH)A(>f-U1#aya4KGf#aeGMN$@eF%k7uDKTOrj1sH8BP zvf7JV@|Rm2b#*(w@U6zU?T)+dxo@ohYxlrIk39B7n{nIpIgdBNdmnuC$!8ON!36*~ zJ+QtrgolS9GYWZV`M$$e*sO4HizBu=s>ZQHr|m;$pvBokSKWuM!BN)_-J!Vao>uog za@%81jMd?(_CwF0&2#T{41O@qN8h~k-4Ema^w;~re#*2rH4FJtU5Vx1teIV2hB^cCjaXRHfHl*Tl4)1n>e~; zhNWhBquvf9rN|X0uQ1!_6aeWYk2Dq$c5S@LO(3oVadHdNoI==4q=Pk!;TU0L5|1Qm z_zD65&)EN*qp(dI~1ig7+q@|}+qeh<X8 zt~|C&QTd|Rr`o00gb63DPmudhzeX55Rme{J(#{9jW_WU_?`yMLX{>NU0i@8S$Zx#M zz-r+CZ?hx(Bgy{$RB?r0yXwDg{3H72;wKMEfb+fm&OiVHE9Exk3}Z>8c>pFldxFS! zFT-53G)EE7Hgl9?32~mHNZp57|2JeHrdb3_Symy9|F0kv9${`x3@wlgl;g4$8^8>{ z?~rmQzDmgovD0^=5HgyK7kM6gOOCQk4M&#trB$78n?Mn%-aZR7Ms4z*@g)-Va za;Ax*Igj&E@WyJ=ifFbem}1wA>(1ud1{-LfCN|mVva2%Sjwipwn_y!snM)_Ur8b!lBxAshVE_nk38-#M~+ACdLVSSL$eFbCRL*BE20$&{jtP*bPd zLw(l0ChA6+Jy{wHt16?bAVqm(C9ogy?tXo_4@WTIe`BYv3gRGth zrc51ukd0oKvoK93+HR@~hs8c0SNYWNI2iS)Gcm)i?sY6HAsuujD3`i+lizx}q_7G8 zZ zN61ZRbIh#;=FytKl*o=F%H=XH!C1weBM!Dh1lnk?Hw92vU0xOmu;7+$SM)mJ6^KOy zi>uAA{FQ5{ih{1Zb4=yo)~-yLhf>;9u&Ki-vsKDi<$N#m<%ury#WeS+`HwPgT~E&I z|24lljmpK2_y1Y%chc-grdZxlJH)D3EoW-qQS!8e@$LHM7W4I)g>1h3&ft3O971Mk zJryENEuPdpFnL{WI=*Oe+t`KNtYDigmyEH)1LqB%wXaZKFS-(ZMsG?xs`wtMf{9ag z$-}a~G}O*|m0+Q6#c~p-I&sV@Mt2Zlvc079Xw+A;afA4OX~|D|ygD^HD>&gCgXLTI zpO7~nSGHJr^~~Dn@!wZ+cw=b}WxWRJJt6P((!lOMu7kTpSK*7I(QY=Bb!c_rzlU0O?Vx}?JT@+X5rP|$-`@3?vufqBgMHTcCHEElBfSYiyV2nvfR!lLn ziX{*ucW_C27`JSjajx251%=rH=9QuPS;;sliYt_S0}PIHnp;eV8R~&M?J0@O*wuKm zGVgqQ7qY|Fr7(Mt)!v34#V9jg0(;}p{h?pJhJY*IlxVKjv!v&W_&(MpjB9p=f&9Ru(LC5aHFMBPG-ZEcg5w51E zd$P!wF+mOqaFyMfvyC2i*4R~~EZG>;blN<2>k+_Q6-Ja0;>uKwYT41o=sIKk8kN8s zXPZ0c(1L_+^9T_Zvbt*y39O{QRCJZik@pbD<(&=SQ6=pVYgETYHc4a*#@NnQN89nR zUs9f4$F&%+6JND|tI!}wR88G5giUCR>vzYHHBeaC1ei&4dsrEBqy#KZlOHb|g-)vZyrh8}SWU=EQ{ zYB|0Z{^t6r_v)v9B?L1mW8ON!IJba>C*N=;;clX63JFs5n#*0xmnR4L;&^kOV3K(N zQC8TZf&w$Gpx{j(3wi;U7kzlphxOU2$60KImTyeJ+@Y zd3QCM(}kkG4X}|7o3MzOf5}wo=WMlNDpnyoHF(baLl&f~nj%+LoQYszhu|Rl9$_|Y zNW}wn97s$a{l>JKZWx53}m$9)>w8V z-Nd{8$D2yjPJb~EW(zZ&>G_lvAd3(({|3*9mW-kl8Rzqtvk2>SO%6cYB*o>Q0G-Ri?jNY4;n!3v z0Ig|m5hW6rV<(4?3!)-6hBxQ}_)xmB5by&e16X1=Oq1s5#R#+-VL`h`IEb9B%}P@Y%%7vGSna!{c#<_9i#tT2wdfgSyEjFi`x z5_6fcCad>igAU33WF^{}%{Y#dID6glTAb1J^S_|qb)h6-P|((fk?8*T6>j?N$8H8o zL$RpMI1YkPc6rhM_wyzR6bRBTXiD%D*>5U-FJ`uY^pS;mA@n9kjcB zD-H#J5VePO#rP)11M?c~{qUjYpVl3<9c&Ct>^w?%yOM>-mCU`@60ASQRU8|W^c6GG zQLWvVgaOHljhn@Bie!@hiD_&ZuZ`Z@n6jU?WQZb}_NaAB<@&Y4nT(XLij;&0$2tk* zOUt}h#ut>FH0tl6d7L_U9?C|lH>o>Kvu6i(NVHwc@x3Ac;U?}auI-=={}xX>5UDUp zveb5!Cj-IHiH@Jo13A{EN+kBhHtOm8x}CQjVwM79yr&F=0KR3Ddu4IX*Fqnky1u$Uk+v zya8zEGV1_zf_GG6Ry>0gz4{)LxB6{$ASCuEtk(P z);t;L#%~#>hIpg|tmTE(8=jYjGHyQF2`l^T+i(BMCo`-&9KBz;1LMMd!f}CXYUgT>`qz{cE=k(M22vdrBGFh=0PC zP~!{rm6P<9`&-tBr{kxp5J{z=kXm=e2({Sf=n3yNg$EOetN7%QAY8&$Y@GOcZb)oj5Zc!#7}FPw!7A@~#t^DTK5)Ixe)M@QF=Gp8 zB4C*6U5*5a&E8%I?ajRAF-x$!W#;iDdHf}=nV1q^=dT`R`HrCm$Be4M(TwZ546gf} zd!+lUyUgdfz@1|aAbL~-h(5mlu!*E!J~mK@GjT_FxjF_Q?HEA%!Q%Mr`}yZph2^o) z`Y>P?Lqo^RR1Z?kzoCrHkijomIe$L7G1PUxSjC)d4$Cb#=9{_v6wfl28EQ|EbJSN= z;h!Y=C2g{_@P_MBAQKAXZINsToF@|z;$6%3-7=>()afOqz&}CjuJaFHa!7IVz3C*J z{`gt4*;bXXiOIsn_SGH5b*j~EjOaE{SLud0AuYKuek`JIv zjGt(7!Y8QG{|-iiK*uezzW#nSds+xf!UTT!$b7H&zbGQgD>&98=|{L#+ve3zcbI=| zWvk9o1kF&Faint{p*3eIYpArx1e^$|GB!N-`HQ#@&qw-BvItymY z)!Ir8lN>&~!Zp*Y{{cV24;Oz;nCeERI*#LhFKwiM1094POfk2O!JWkJj>tO034lM8 zG|B~_nJ(PZ+|Mm5&jHPJE~Y@DpI=;O{(fio=icHuIuae?7fr@0siUh~rw-kPrnKgU z3l&k~?eRNz?XqWSbGfUv) zW2vq0{QJ!`TU1QU=jPtG{ukHR1rIOo&M8qc@X=9O>G42KYj|$^a*_hZ`PW!gp8B^- z#&rJ4w$5qt3+5ktFY2rOUpgH0u-7%@8kK~Vr@wRn{_t(D=cZ4@s~Nj<960!*4 z7-B*)kTXl`1r|y6_XzZdOyOqf;ve>k4#Ox*`18n20trVA^svc8+4(FIJZi(cudN?f zu%UAoQ!a2-lwu+gkJGJ|JcWfb9(R_`*_(_)M=Pw(M!-eyu+`Qs!Nwuj5`S-6& zKDu^elK%Zh4gIQ?&65Bg=E&byS?Fhc_A-2(G7JkA`fnadV?H0_Ritd*Q)O<+=PyRz z$Y@>FzA8QTrssvEUFd~Xo1l1dk>DR|@w_4-<-}Qzx%<_MS0D!q5|52rHa&i!$1TVcIZF^hW-R zIc|lkl1zU%MvtQFs}_4xS^)dr(@g9i`XKEBZ6OR7Ov#*6{LuCgsQFW_tRo+&)f8{^ z|3Wf_^3c&3VM~SH;y>?@n;Z(Ha-z2YG%yqb;@YRSrP$7Bdfc<93cZveNP1;tTm!Jl zPpicZ8^~n2$K-=)o%O|hV3%SN`pJ;p9X)%`(!Ixoh{7VG5*h(p?CAJrODoC$pDT;3jxIfz&X>U%1Kwje< zq_R?MT>v==2SIM2IG~k2v1z=xW&TSs5jgToa!qHAcjxqYC&Yx0We}_0+JW5a+#)-` zCC}g7o#+iDa0I^=T4Vd(iQVx6TSw-E5vya;k`l}U(9ecW?wx*JMeK{pGmhUfjLnNC z4wA2pPei43yE3PaGy1}j2=`o*L>2}Nz|eyY}KC+sr7pID-u(#zDuOg5$TQP7{NwlZB# zb?IOGyT^`(iy6lL2uB+qGhMBR55#nzI$Fgz<(_s|1;WhnB z!=1wjT~Mvqi?CH?E#*hq1vmI+MXzyzkHX#z9QH-YI{wYXsYS*&-NC=&}gB41{{bl;p8 z-x6J$m*~E^J@dX?q~~9z(p^>$4BBn~vL--7$2WVNJFq^Z=-KxG&I`@i2FQoHvGzQf zF1>Vm;n(MakFR>ZdjtfyyZ9vru$_8O2S8VXD)94jC0Ju4%- zBywb|1f89ik)2n7s+b$~EKN<%$ks}Viw;>FL~jjZf)|g1=LgVl2hqXv`nLqES&-cJ zvtNo7Qxyvp@0}~u3e{58ivDw)<%^(ji|mQa?ETmiG##DpX>=xg_qK3Pe)%D>v3v?R zy%bB0Nq4ulB)Rr!^s5YpAc{kJ>RlkpP>N3@|NQ<5=lcZ)Dmx)MF4f)E9_yIoKN{{* zq!MCt#atvv1_$?Y$X94rdl#vIfm4uVmNiEe185W!>H~#F89-;0W?we9jdtvdTAx4$ zRy!kFhgK`9nyYDLNH<$&gdZge6Xj2Gb8)fpsv*|WMEM%4DtW6)CG1Nicxxg24TTbL z-nzLy7uXcRY--f=`?qG3nXysR>3)}7j}cy|1soTZ6dM>?7v18gnt;|fRZ(i@4rnD% zQ;M^R_dSJdJQ8K?Zr7)v%mEHM%Fem>Z$`~9$2CDk95=WhGn{u`n?FM~UEzhcLd8ww zcnnq-BB~l%#R@IpEggMqp>JSWPRcSuJkT&U1|t9&E7WSZo>e>(62wfPip#eOOM*j6 zO18?%Q51DUV|6t{bCijBfT@X&+<3Cnv=ecI=sWEjvmqe)J8=ST+E(HdFUlP52bH&1 z1MBN+^r7pEC|RWmkAebyJss1+AGz^PguscOx9xS6Akw${ysP7qvYtivtTnzGRzXJV`&D_H#fWf08~R0&JAqBl(JF{j1}bX z8Q-#ImPl0xmh^&W6mvtP1i+Wjo{xvdG=@iOO)LAy$}0xPt6G`85Fs{Ju0a5#jVB_+ z+6ocuXTZzLi^&JZsb!m5`31TlQSP1y7w16ltMENNS{_^4p6RJ6dFqC+G+~dt^21BZ z7)ESR2~Q);8xsyl;@karZg)#fo&J83%v&fqDJn2q2v_!x5vIgh1nKDO=;zVEUmQA} zAYh1=?X5xU>FlgdhPl!jvJc0`{Gn?!s6cD`WTuZ}W?k-C*$BX2XT*HjI6=Z2>}XFJ zylM?ys=X5-o+oLk9ylibVhs?)&;?Jwhz@6_`HmQ8at1Wh+2;O_`0T{DX~{7_m)JA- z`tIRU|9W48uCS+J_*IC^U5H&1-Zsp0A3097_}ZK2puY@Fj~ zEa7fQn;xbg*;dFa7n2@CEY|GE^yz116^BiqhfUnv{hZw*oefU;bI*Gw--QyB$kV{p zL(tch=WMBk<>VBU_NZx%k>-k?2B8N`0nfdd@CRbujr9>$D(xS=@hO+_6XFrUF)M0P z*DshkwzcuByE*Bp+wt++R9g7{B953F?y6KDA2Uz33~q{uhd+^njU6h}$sJhB;LLsC z-$c_2pIS%5r`LssQKwr1ppnMkR=@q&ZfIs6X$CWdYN@xHGgozk#ejNQu(p|5ZZ>o3 zpx1~f4trkt^|Rk^Y50wnrpS_`S=#|Q4api^cesW%2o>KUme4@P4sy}G=84p^!{ zbeB+Acua+w+E>nTT4{diC^wHJ%jl|~{KJIUMjRIDiNiMHV|kYq^&tw15IrRYT>~#& zgS+aO&!PAnqzoZIr%71aR3ZOTD4yn_{n$lK+F8+nuU0tD!j%ZhDq2egXO*w(7JJt&|#t}1RN1Z5)C^VNbSe;XLzKC>Yi1>C*b^pL*`Ql1KG^WQCVJe)PsTX;K6(PW?~F~vLKOkj zQ-<$FwKtmkM5!5y;-kyXAfw4k3@~FKhhh~61~eVueEWKRvaJr|>iZXAd>L|bs(Phq zChbOs5o!x)*_Ek4{5*f&9qj})4}^{@M8VBs)^)CN4%WfUOoDucIZQF_NFN3P>o;;L z4G${I8?tjN8Y+s)%Ug1?E1N6&I2$mLpo*RbY#39qDd&rx@E?aHcWsYCkE;GEKep)L z{|3>UeP?ShPrNGapuk=rZ_^|>jT9UGv?x8XIT`5H%M6di#A_Kq4WK3=w{PDn&Y`+< zc^VrAVaOS=*t#NeUSn;k^2_6oZ)x@S^l$G=<|L>?jWp2iElsvf6Snxyf@swO2s&IN z`91;lI3_c_DmsXed_PMNT{D>Kon7bUC#;{9VZ-R6p@#U_to{aT=C?T z_~?czV%n1$V1drRuQ%QSBzalUaIYq-+eW7P5H(eZrM9LH)LTG+SAbVYpgq06u|M`- z2Q_rl_QtB_SIVnzZxS)mzYrcV)Hz7INu2XWnIwF~un^#;oJ@aSQ&o6E+arM~| zm&=%uX#<4ynE<{GZ^bu@CULvCL)^?Z1h`iZCxq0H!q-%Cu9BuQ%996w`+F!fA~>r=8lp>kZw@ za!Z%Iv)uC}V9}kt65UC==uRFw+`Wax@?|4@qA#I~zNOoBy}`psK-K^fJ~7M|Fw$5o zPBfUhcQbPOzNpS{At+*x(#2yC4S67H=?WlfO^8xrlx(j(hEvI;kzv>T8f08v7P)zF z8o;e!>QWRAw)C;d9#Kym9D06;O9^dsSiM1umCi)AiRr}3kzTT|@)$0*vdQEc$<7#d z0K#}U5RGtrK4UQ98c6~`Z?K#q{^`3LqqTo6OA<~w*@vjlz*G|}sW!;Q#ewO7x~4Tr z@V(KtDy{rqJ7{TyWKmVtn%r&W$*Rm7@qXw{odEpW05GwCZT1DgGZw{Aql=D=Xly-m zJfF6RH(G&XTYFm{FCUafFGNIERPlSI3HCTB7Ks71!>F+nHO0Fks6xJy$qV_qol?&Z z$i2n~5Or5&?=t}1?W0vud>Pz*UcWfz>ccL49nfDhYSB@yq#v~YqnC1cYicMq(v^q1 zgQzLN#{lF`>5rr~HWssmd z*Q2%hRBoJDI;cez2@iP4ajJN7XPO{j<0E3z!hnUG?m~^V?i$LFLxrx1SL0Em19<0o zz4lUf<;|zFGnLr3N2+b)zTemu7? z3qmPYZLFQoKNVMw0nb72;gg!-gcCoJ{Vfa0jSr3^N%wtra=F{j11Dd-HQaObP-`Xc zez>N)l(M_011=;O8zZ`?6U)Io$8$b@mKY9Xx?Sew9+?k~?AKg0UHjV@NhW{E`IwSX z@40iV>yD|qEQ(_S!u+K}fh;`@TA=Vzkk$lfU@RHl*ByHQY#`oJzxkRgSaDTTYcK=^)=fpJbU3ac z!>SZ+7=|6STF=K$>J^hOL>*QKP~$kj53&gotghB6+{^)~H}TJ}Gv)^v*bE4pPC;30 zn#a=?T*n|^RxXT0U9RDg11)dl_20e$&UfCmKLq;29mcz^rKe|usa8%bKaQJV13Snm z=lp~D-G6A%;I0H?C|3$r?wOi((PPtFTJxOpQqWP4PKDf;_^a$8qs=z40SGK_u<7v*{G6+Myk$qO z7L)=S00Hq`#D@+#taBSKay9t0`=UxX8|CaZAMY^3J;sAgXaj&U9I%UGVny#NU)D%6 zMc00;F!Gil!z%?l8+Is#Nnb7dzs5rizI`-Ib>cdWIw{$Rw@ZGFdwZb%7!Z zg+Ay)L|trlNeR%{+kX(E&aBLI!jau+wBT+@Kd)FdE(x$CRKg(;#VOYXj2e7t_%(p45s4|D`QA5<3u-nPRQJd z@|~(e@RlQ@asEy*-}0vSx@hM<-mLM6k%0HMgvby?N~9 zFN!k$03U{{m}V#0OQtsvv?SMYba{!gsDwXY5XQbbTic?jjBUEIg=BXG9OuZ#$HAx~ z0FIkklx3?aDh-ce>?go!3HQKy7>?|3Wc{nh*0(5@#a4AVdD+gZuaNmZ{GZO`CYF!@eT%+(gH4IFcW&nLQU1~Zj_>-wBiC3tL-@{ znA|)W#Co+?U`2WXHHnZF$$p^-Lq3Tvu$gttLH6+#fgEh!^z&+G|tZLTo|RkZk@`0fa^?SSJXNN{`E;wW`qx*1FJ)5bcfi$qK) z?RJTfox)C=-DiSy?)(TsMl)D4%Gk=CnvzWinJ5i!`9mIM@6UO0A>5bcIfW%!)?b9t zS)LbKmE_-ZQq$SdrfXLD@KSoH-j>6M6I#?Oaq+mHimVr5bZirzell)YJgsa&V*dJ2 zVAeLdwesj4j}{)$nLF|tgZv{){jWJ(G;@z{G*8zD$Xp9~G9u*s2uTJ}D`s&pbZ2SK zm>aE}vZc&JMoFQ*LzPw2C-JISLx{PX%vP&j|1jC(6-juqNJaQRz~2zsK60)!O-_=R zvlqhhl)T^W9rQahH zxlN|@C3U^Q0W0cC^zd@irWq1I{EQ?SoZcl7T((VCXZQ3n#m9a;MnNgCQ+1$`<}fAW zOH~lPF|}S8fyP~a)B9mmgQd2tGKrd$>Jtv-!z^GO9^jv}q`Qo!Y1(|@J~;Wg9QmFK z5nQ~egX%rtZl?=QZ5u&DK!#@0wE381&AhD@gje4qgetK!P&k>KrTb%U1uu(ix(8w) zJ>s!RC@@}Z@vQ=RxLMq&I#obWndHi?H68D9nQhTmx_?%^`_|)YOTI~e7#>A zgGb!2v76|UIO5XeJk9j>IX%7%b;Usj6*x))ZfWz&6$0D^xHg=#s6{L|l!lOnGd>>x zn1zffY^tnUpsIo3tF>L739s;P^dE1mf_V(mZADUS!?k(dNaSv|R z*}2%+3N2dMXRTQ&mU>>jB@piIj0xNLp@FXtHps0rX(7NGhzvdFqMNn7CORJNCj^lM z-wc9MCIG)%LZEQErBI~7H?6vRW*f8FY>javfW}n@RHL2XYGEm1R3D*pn;{_nJ`xa4 z0!2ii0;kI|Ig4~dk-6SwsxOw4W!b|eHjxK$%6i;j0yeo56oB^=NOVxg!3nm$m4#cI z+HBJ^(D<8b7I~b_?;aswwvZ@P06dS^7n8O6bMeeJ4h#Hr-rw>9Pvg*3(mqQ?5%+qm zJFR71=>h;ig?&&^JE0 z6SvH*k2d7e{DpH;aE{M}Q{#l2(M```4XR#BsX|+cXBXKAmQ_wWs`m@QcWzgn$KhOjgkw_$+K@qEmx!N43qtfYuDnY}@k#pud z&I$7cwm@J83>cp+E?CMI=NdB7EPih{6O%3YwMH~&pBeo!lc5H{t9_ zHhInT+t7|i*tU@o`(M}v8p>)g9eBhcCVdk)TW;il z)9E4vp;Q^d7gtB3#Tc8b?E1uHr!p!Hr$5WaDoZ3o3t%G zwJX@bL}|<|FZl#`V`KkxrdLjpnxdhHEUYoiVgG_RY}c?%`kX-wY)>Cv5i|45(fd0( z z000OuPMrh*tW!es|AN_L>s$S(lNDJ-lYY>SS2it*6z7h-jJ?tJUSSiHQe5d`PIJ6u zUnl63Mqy=4-&jr)_l>La@LrUY^%BMN(84HO*_+M|39aQLWr_slFf&qKbKT`X+RJQq z#Eolq(^k^JW+yp??|f+_LwnW|23BPRMdSE|{m~IfWor%c-T^UtA}{}ZC%GyrUsE)1 zBilXVkYtlN&$y z>p#{z7OBmCGwY%7FfcWERmBg@wa(DGBfG6gAMQvPoRrpGAtR}O%P_Hxs66M)&X2|q zqaw3TY32w`s`xC_8p5WHor*@V%(`!OwEM4a7?xK6y4Qp37o%3UWKhylMiiC-?&S)% zDpgrUs4E+{xM+NVAg|7bKTKmO4&{8b0DOSq_0SLirV<2{ zQvu|FW<1hc&UlRPzf!_qN@6_4R+~XX-27nqlAIB$My)3G%9Se9NXCr|SJfb+Q4<8M zR6My>wL(fG*uj+%T`+HO8pXCytls~OT}0{FH3~FtOv}W2my|QA3NcD2cBTA1oZ_q3 zpqy7Hf|V=WdGkxFw>RsQ3Q(&ys-@zr$k2jPzS?ROhU{WWp1fWuXO%{w!>dy^8c;2U zR;X}s^oA>v$h#mI`(`#Ma-*ghcOC;LI|@&%S$50@(w*2l8@%4tffRF1dyCdOm%ck{ z|17uK&Ta5i`Jw5S%JVToK0l$f+G%e5r1$VuU literal 0 HcmV?d00001 diff --git a/app/fonts/roboto-latin-bold-700.woff2 b/app/fonts/roboto-latin-bold-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..32b25eee7c5c3309ea53facaaf016256887ec0b2 GIT binary patch literal 15816 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 + + + 99F3B28B-E969-460E-887A-CDE6B30A5D8C + Created with sketchtool. + + + + + + + + \ No newline at end of file diff --git a/app/images/panel/insights-ribbon.svg b/app/images/panel/insights-ribbon.svg new file mode 100644 index 000000000..449008570 --- /dev/null +++ b/app/images/panel/insights-ribbon.svg @@ -0,0 +1,58 @@ + + + + C5C3BC26-5012-417D-8D3B-1AEC48935928 + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 684d21b4d..94e96057c 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -16,6 +16,8 @@ import Header from '../containers/HeaderContainer'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; +import { Modal } from '../../shared-components'; +import ModalExitButton from '../../shared-components/ModalExitButton/ModalExitButton'; /** * @class Implement base view with functionality common to all views. * @memberof PanelClasses @@ -23,6 +25,9 @@ import { setTheme } from '../utils/utils'; class Panel extends React.Component { constructor(props) { super(props); + this.state = { + showModal: true + }; // event bindings this.closeNotification = this.closeNotification.bind(this); @@ -58,6 +63,77 @@ class Panel extends React.Component { this._dynamicUIPort.disconnect(); } + /** + * Function to handle clicking yes on the Modal + */ + _answerModalYes = () => { + this._toggleModal(); + } + + /** + * Function to toggle the Modal + */ + _toggleModal = () => { + const { showModal } = this.state; + this.setState({ + showModal: !showModal, + }); + } + + _renderModalChildren() { + return ( +
+ + + ); + } + /** * Close banner notification * @param {Object} event @@ -148,6 +224,7 @@ class Panel extends React.Component { } } + /** * Helper render function for the notification callout * @return {JSX} JSX for the notification callout @@ -203,6 +280,7 @@ class Panel extends React.Component { return null; } + const { showModal } = this.state; const notificationText = this.props.notificationShown && this.renderNotification(); return ( @@ -220,6 +298,9 @@ class Panel extends React.Component {
+ + {this._renderModalChildren()} + { this.props.children } diff --git a/app/scss/panel.scss b/app/scss/panel.scss index c37affeec..089242bce 100644 --- a/app/scss/panel.scss +++ b/app/scss/panel.scss @@ -73,3 +73,10 @@ html body { @import './partials/_subscribe'; @import './partials/_stats'; @import './partials/_stats_graph'; +@import './partials/_insights_modal.scss'; + +// Imports from ../shared-components directory +@import './partials/shared_components_svgs'; +@import '../shared-components/Modal/Modal.scss'; +@import '../shared-components/ExitButton/ExitButton.scss'; +@import '../shared-components/ModalExitButton/ModalExitButton.scss'; diff --git a/app/scss/partials/_fonts.scss b/app/scss/partials/_fonts.scss index 0317018f2..65b9a34c2 100644 --- a/app/scss/partials/_fonts.scss +++ b/app/scss/partials/_fonts.scss @@ -8,7 +8,7 @@ * https://www.ghostery.com/ * * Copyright 2019 Ghostery, Inc. All rights reserved. - * + * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0 @@ -182,3 +182,45 @@ src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans-semibold-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; } +/* roboto-300 - latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: local('Roboto Light'), local('Roboto-Light'), url('../../../app/fonts/roboto-latin-bold-300.woff2') format('woff2'), +} +/* roboto-regular - latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url('../../../app/fonts/roboto-all-charsets.woff2') format('woff2'), +} +/* roboto-500 - latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url('../../../app/fonts/roboto-latin-bold-500.woff2') format('woff2'), +} +/* roboto-700 - latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + src: local('Roboto Bold'), local('Roboto-Bold'), url('../../../app/fonts/roboto-latin-bold-700.woff2') format('woff2'), +} +/* roboto-900 - latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 900; + src: local('Roboto Black'), local('Roboto-Black'), url('../../../app/fonts/roboto-latin-bold-900.woff2') format('woff2'), +} +/* roboto-condensed-700 - latin */ +@font-face { + font-family: 'Roboto Condensed'; + font-style: normal; + font-weight: 700; + src: local('Roboto Condensed Bold'), local('RobotoCondensed-Bold'), url('../../../app/fonts/roboto-condensed-latin-bold-700.woff2') format('woff2'), /* Super Modern Browsers */ +} diff --git a/app/scss/partials/_insights_modal.scss b/app/scss/partials/_insights_modal.scss new file mode 100644 index 000000000..fec8da1da --- /dev/null +++ b/app/scss/partials/_insights_modal.scss @@ -0,0 +1,106 @@ + +.InsightsModal__content { + background-color: #f7f7f7; + position: relative; + width: 518px; + min-height: 437px; + padding-top: 21px; + color: #4a4a4a; + border: 2px solid #325e97; + z-index: 10; +} + +.InsightsModal__image { + height: 94px; + width: 177px; + margin-bottom: 16px; + background-size: 177px 94px; + background-image: url('/app/images/panel/insights-ribbon.svg'); + background-position: center center; + background-repeat: no-repeat; +} +.InsightsModal__header { + font-family: 'Roboto'; + width: 348px; + height: 27.1px; + font-size: 20px; + font-weight: 900; + font-style: normal; + font-stretch: normal; + line-height: 1.35; + letter-spacing: normal; + text-align: center; + color: #4a4a4a; + margin-bottom: 8.9px; +} +.InsightsModal__description { + width: 372px; + height: 54.2px; + font-size: 18px; + font-weight: 700; + font-style: normal; + font-stretch: normal; + line-height: 1.5; + letter-spacing: normal; + text-align: center; + color: #4a4a4a; + margin-bottom: 12.8px; + font-family: 'Roboto'; +} +.InsightsModal__feature-column-1 { + width: 268px; + margin-left: 10px; +} +.InsightsModal__feature-column-2 { + width: 229.8px; +} +.InsightsModal__featureText { + font-family: 'Roboto'; + font-size: 14px; + line-height: 39px; + color: #333333; +} +.InsightsModal__checkedCircleIcon { + height: 18px; + width: 18px; + margin-right: 8px; + object-fit: contain; + background-image: url('/app/images/panel/checked-circle-icon.svg'); + padding-right: 10px; +} +.InsightsModal__callToActionContainer { + height: 107px; + width: 506px; + margin-top: 10px; + background-color: #e7ecee; +} +.InsightsModal__callToAction { + width: 176px; + height: 36px; + margin-top: 18px; + border-radius: 2px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.24), 0 0 2px 0 rgba(0, 0, 0, 0.12); + background-image: linear-gradient(101deg, #070e18, #1678a0); +} +.InsightsModal__callToActionText { + font-size: 14px; + font-family: 'Roboto Condensed'; + letter-spacing: .5px; + color: #FFFFFF; +} +.InsightsModal__link { + font-family: 'Roboto'; + font-size: 15px; + color: #4a4a4a; + +} +.InsightsModal__otherOptionsContainer { + margin-top: 23px; + padding: 0 10.5px; + text-decoration: underline; + font-size: 15px; + color: #4a4a4a; +} + + +//TODO: color variables diff --git a/app/shared-components/ModalExitButton/ModalExitButton.jsx b/app/shared-components/ModalExitButton/ModalExitButton.jsx new file mode 100644 index 000000000..ed516319d --- /dev/null +++ b/app/shared-components/ModalExitButton/ModalExitButton.jsx @@ -0,0 +1,40 @@ + +/** + * Modal Exit Button Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * A Functional React component for a Exit Button + * @return {JSX} JSX for rendering a Exit Button + * @memberof SharedComponents + */ +const ModalExitButton = (props) => { + const { + exitModal + } = props; + + return ( + + ); +}; + +// PropTypes ensure we pass required props of the correct type +ModalExitButton.propTypes = { + exitModal: PropTypes.func.isRequired +}; + +export default ModalExitButton; diff --git a/app/shared-components/ModalExitButton/ModalExitButton.scss b/app/shared-components/ModalExitButton/ModalExitButton.scss new file mode 100644 index 000000000..a22fdba22 --- /dev/null +++ b/app/shared-components/ModalExitButton/ModalExitButton.scss @@ -0,0 +1,36 @@ +/** + * Exit Button Sass + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +// Exit Button +.ModalExitButton__exit { + position: absolute; + top: -10px; + right: -10px; + width: 26px; + height: 26px; + border-radius: 15px; + border: solid 0.8px #325e97; + background-color: #f7f7f7; + @include transition(background-color 0.2s); +} +.ModalExitButton__exit:hover { + background-color: #efefef; +} +.ModalExitButton__exitIcon { + height: 11px; + width: 11px; + margin: 0 auto; + background-repeat: no-repeat; + background-position: center center; + background-image: buildIconX(#4a4a4a); +} From 5f920231ad14a775be5e3adb77130aeba21888e8 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 27 Sep 2019 10:47:32 -0400 Subject: [PATCH 02/39] Change feature language. Add exit button. --- app/panel/components/Panel.jsx | 10 +++++----- app/scss/partials/_insights_modal.scss | 6 +++--- .../ModalExitButton/ModalExitButton.scss | 1 + 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 94e96057c..ef8fa25c4 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -96,13 +96,13 @@ class Panel extends React.Component {
- Identify sources of poor performance + Audit marketing tags on a page
- Audit all marketing tags on a site + Trace sources of poor performance
@@ -110,13 +110,13 @@ class Panel extends React.Component {
- View requests firing in real-time + Watch pings fire in real-time
- View digital trends over time + Explore global digital trends
@@ -127,7 +127,7 @@ class Panel extends React.Component { diff --git a/app/scss/partials/_insights_modal.scss b/app/scss/partials/_insights_modal.scss index fec8da1da..b8c8d2695 100644 --- a/app/scss/partials/_insights_modal.scss +++ b/app/scss/partials/_insights_modal.scss @@ -37,7 +37,7 @@ width: 372px; height: 54.2px; font-size: 18px; - font-weight: 700; + font-weight: 500px; font-style: normal; font-stretch: normal; line-height: 1.5; @@ -48,8 +48,8 @@ font-family: 'Roboto'; } .InsightsModal__feature-column-1 { - width: 268px; - margin-left: 10px; + width: 260px; + margin-left: 28px; } .InsightsModal__feature-column-2 { width: 229.8px; diff --git a/app/shared-components/ModalExitButton/ModalExitButton.scss b/app/shared-components/ModalExitButton/ModalExitButton.scss index a22fdba22..955fdfaa1 100644 --- a/app/shared-components/ModalExitButton/ModalExitButton.scss +++ b/app/shared-components/ModalExitButton/ModalExitButton.scss @@ -25,6 +25,7 @@ } .ModalExitButton__exit:hover { background-color: #efefef; + cursor: pointer; } .ModalExitButton__exitIcon { height: 11px; From b229c9da66f5bb5d1fdc08504cecd97ac429d6a2 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 8 Oct 2019 10:40:14 -0400 Subject: [PATCH 03/39] Add insights modal that will appear if a user clicks 3x per day for 3 days. Users subscribed to insights will not be shown promos. --- app/panel/components/Panel.jsx | 172 ++++++++++++++----------- app/panel/containers/PanelContainer.js | 4 +- app/scss/partials/_insights_modal.scss | 7 + src/classes/ConfData.js | 1 + src/classes/Metrics.js | 56 +++++++- 5 files changed, 158 insertions(+), 82 deletions(-) diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index ef8fa25c4..fde0bc463 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -12,12 +12,14 @@ */ import React from 'react'; +import { NavLink } from 'react-router-dom'; import Header from '../containers/HeaderContainer'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; import { Modal } from '../../shared-components'; import ModalExitButton from '../../shared-components/ModalExitButton/ModalExitButton'; +import { login } from '../../Account/AccountActions'; /** * @class Implement base view with functionality common to all views. * @memberof PanelClasses @@ -26,7 +28,7 @@ class Panel extends React.Component { constructor(props) { super(props); this.state = { - showModal: true + hasEngagedFrequently: true }; // event bindings @@ -40,7 +42,6 @@ class Panel extends React.Component { */ componentDidMount() { sendMessage('ping', 'engaged'); - this._dynamicUIDataInitialized = false; this._dynamicUIPort = chrome.runtime.connect({ name: 'dynamicUIPanelPort' }); this._dynamicUIPort.onMessage.addListener((msg) => { @@ -54,6 +55,15 @@ class Panel extends React.Component { this.props.actions.updatePanelData(body); } }); + + chrome.runtime.onMessage.addListener((request) => { + if (request.name === 'hasEngagedFrequently') { + this.setState({ + ...this.state, + hasEngagedFrequently: true + }); + } + }); } /** @@ -63,75 +73,38 @@ class Panel extends React.Component { this._dynamicUIPort.disconnect(); } - /** - * Function to handle clicking yes on the Modal - */ - _answerModalYes = () => { - this._toggleModal(); - } - /** * Function to toggle the Modal */ - _toggleModal = () => { - const { showModal } = this.state; + toggleModal = () => { + const { hasEngagedFrequently } = this.state; this.setState({ - showModal: !showModal, + hasEngagedFrequently: !hasEngagedFrequently }); } - _renderModalChildren() { - return ( -
- -
-
- Try Ghostery Insights -
-
- Speed up and clean up digital user experience with our professional tag analytics tool. -
-
-
-
- -
- Audit marketing tags on a page -
-
-
- - - Trace sources of poor performance - -
-
-
-
- - - Watch pings fire in real-time - -
-
- - - Explore global digital trends - -
-
-
- -
- ); + generateModal = () => { + const { loggedIn, user } = this.props; + const { hasEngagedFrequently } = this.state; + if (!loggedIn && hasEngagedFrequently) { + return true; + } + return hasEngagedFrequently && loggedIn && !user.scopes.includes('subscriptions:insights'); + } + + clickSignIn = () => { + this.props.history.push('/login'); + this.setState({ hasEngagedFrequently: false }); + } + + /** + * Reload the current tab + * @param {Object} event + * @todo Why do we need explicit argument here? + */ + clickReloadBanner() { + sendMessage('reloadTab', { tab_id: +this.props.tab_id }); + window.close(); } /** @@ -156,16 +129,6 @@ class Panel extends React.Component { }); } - /** - * Reload the current tab - * @param {Object} event - * @todo Why do we need explicit argument here? - */ - clickReloadBanner() { - sendMessage('reloadTab', { tab_id: +this.props.tab_id }); - window.close(); - } - /** * Filter trackers when clicking on compatibility/slow * tracker notifications and trigger appropriate action. @@ -187,12 +150,12 @@ class Panel extends React.Component { this.closeNotification(); } - /** * Dynamic UI data port first payload handling * Called once, when we get the first message from the background through the port * @param {Object} payload the body of the message */ + _initializeData(payload) { this._dynamicUIDataInitialized = true; @@ -270,6 +233,60 @@ class Panel extends React.Component { return false; } + renderModalChildren() { + return ( +
+ +
+
+ Try Ghostery Insights +
+
+ Speed up and clean up digital user experience with our professional tag analytics tool. +
+
+
+
+ +
+ Audit marketing tags on a page +
+
+
+ + + Trace sources of poor performance + +
+
+
+
+ + + Watch pings fire in real-time + +
+
+ + + Explore global digital trends + +
+
+
+
+ + Try for free + +
+ Already a subscriber? Sign in + No thanks, maybe later +
+
+
+ ); + } + /** * React's required render function. Returns JSX * @return {JSX} JSX for rendering the Panel @@ -280,7 +297,6 @@ class Panel extends React.Component { return null; } - const { showModal } = this.state; const notificationText = this.props.notificationShown && this.renderNotification(); return ( @@ -298,8 +314,8 @@ class Panel extends React.Component {
- - {this._renderModalChildren()} + + {this.renderModalChildren()} { this.props.children } diff --git a/app/panel/containers/PanelContainer.js b/app/panel/containers/PanelContainer.js index ce4a744bb..9b2f73d8e 100644 --- a/app/panel/containers/PanelContainer.js +++ b/app/panel/containers/PanelContainer.js @@ -27,10 +27,10 @@ import { updateBlockingData } from '../actions/BlockingActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.panel, state.drawer, { +const mapStateToProps = state => Object.assign({}, state.panel, state.drawer, state.account, { paused_blocking: state.summary.paused_blocking, sitePolicy: state.summary.sitePolicy, - trackerCounts: state.summary.trackerCounts, + trackerCounts: state.summary.trackerCounts }); /** * Bind Panel view component action creators using Redux's bindActionCreators diff --git a/app/scss/partials/_insights_modal.scss b/app/scss/partials/_insights_modal.scss index b8c8d2695..c91ceb97a 100644 --- a/app/scss/partials/_insights_modal.scss +++ b/app/scss/partials/_insights_modal.scss @@ -87,11 +87,18 @@ font-family: 'Roboto Condensed'; letter-spacing: .5px; color: #FFFFFF; + line-height: 36px; } .InsightsModal__link { font-family: 'Roboto'; font-size: 15px; color: #4a4a4a; + &:hover { + cursor: pointer; + color: $ghosty-blue; + text-decoration: underline; + text-decoration-color: $link-blue; + } } .InsightsModal__otherOptionsContainer { diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 121c77ef2..47ae3af6f 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -115,6 +115,7 @@ class ConfData { _initProperty('hide_alert_trusted', false); _initProperty('ignore_first_party', true); _initProperty('import_callout_dismissed', true); + _initProperty('insights_promo_modal_last_seen', 0); _initProperty('install_random_number', 0); _initProperty('install_date', 0); _initProperty('is_expanded', false); diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 145e9239e..a021a4085 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -16,6 +16,8 @@ import conf from './Conf'; import { log, prefsSet, prefsGet } from '../utils/common'; import { getActiveTab, processUrlQuery } from '../utils/utils'; import rewards from './Rewards'; +import { sendMessage } from '../../app/panel/utils/msg'; +// import getUserSubscriptionData from './Account'; // CONSTANTS const FREQUENCIES = { // in milliseconds @@ -209,6 +211,7 @@ class Metrics { this._recordActive(); break; case 'engaged': + this._recordEngagedWithRepeats(); this._recordEngaged(); break; @@ -748,7 +751,7 @@ class Metrics { */ _recordEngaged() { const engaged_daily_velocity = conf.metrics.engaged_daily_velocity || []; - const today = Math.floor(Number(new Date().getTime()) / 86400000); + const today = Math.floor(Number(new Date().getTime()) / 86400000); // Today's time engaged_daily_velocity.sort(); if (!engaged_daily_velocity.includes(today)) { engaged_daily_velocity.push(today); @@ -756,11 +759,60 @@ class Metrics { engaged_daily_velocity.shift(); } } - conf.metrics.engaged_daily_velocity = engaged_daily_velocity; + conf.metrics.engaged_daily_velocity = engaged_daily_velocity; this._sendReq('engaged', ['daily', 'weekly', 'monthly']); } + /** + * Record Engaged event multiple times in a day + * TODO: Save engaged_daily_velocity_with_repeats to chrome extension storage. + * Current Result: engaged_daily_velocity_with_repeats is saved inside the chrome extension storage, but always is an array with 1 element (today). + * This suggests that conf.metrics.engaged_daily_velocity_with_repeats is an empty array in the beginning, but console.logs are showing correct logic. + * What I have tried: JSON.stringifying the object, using ES6 spread instead of the push since setting an array directly seems to work, checking to see if conf.metrics.engaged_daily_velocity_with_repeats is undefined + * @private + */ + _recordEngagedWithRepeats() { + const engaged_daily_velocity_with_repeats = conf.metrics.engaged_daily_velocity_with_repeats || []; + console.log('Accessing engaged_daily_velocity_with_repeats: ', engaged_daily_velocity_with_repeats); + const today = Math.floor(Number(new Date().getTime()) / 86400000); // Today's time + engaged_daily_velocity_with_repeats.push(today); + conf.metrics.engaged_daily_velocity_with_repeats = engaged_daily_velocity_with_repeats; + console.log('Accessing engaged_daily_velocity_with_repeats after pushing today: ', engaged_daily_velocity_with_repeats); + if (this._hasEngagedFrequently()) { + sendMessage('hasEngagedFrequently', '', 'metrics'); + } + } + + /** + * Toggle the insights promotion if a user has opened the panel 3 times per day for at least 3 days in the past 7 days + * @private + */ + _hasEngagedFrequently = () => { + const today = new Date().getTime(); + const THIRTY_DAYS = 1000 * 60 * 60 * 24 * 30; + const insights_promo_modal_last_seen = Number(conf.insights_promo_modal_last_seen) || null; // TODO: Add logic for plus_promotion_last_seen + const hasSeenPromotionInPastMonth = today - insights_promo_modal_last_seen <= THIRTY_DAYS; + if (!hasSeenPromotionInPastMonth) { + const { engaged_daily_velocity_with_repeats } = conf.metrics; + const pastSevenDays = Array.from(new Set(engaged_daily_velocity_with_repeats)); + let timesPerWeek = 0; + + for (let i = 0; i < pastSevenDays.length; i++) { + const engagementsEachDay = engaged_daily_velocity_with_repeats.filter(day => day === pastSevenDays[i]).length; + if (engagementsEachDay >= 3) { + timesPerWeek++; + } + } + + if (timesPerWeek >= 3) { + conf.insights_promo_modal_last_seen = today; + return true; + } + } + return false; + } + /** * Repeat sending active request every month * if computer is continuously on. From 3d24d703d76161638f801291cbe002dd26c21682 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 8 Oct 2019 18:47:41 -0400 Subject: [PATCH 04/39] Refactor insights modal into a component. Reshow modal after prompted to log in on summary page. --- app/panel/components/Login.jsx | 11 ++- app/panel/components/Panel.jsx | 14 ++- app/panel/components/Summary.jsx | 84 ++++++++++++------ .../InsightsPromoModal/InsightsPromoModal.jsx | 88 +++++++++++++++++++ .../InsightsPromoModal.scss | 0 .../InsightsPromoModal/index.js | 16 ++++ .../ModalExitButton/index.js | 16 ++++ app/shared-components/index.js | 6 +- 8 files changed, 197 insertions(+), 38 deletions(-) create mode 100644 app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx create mode 100644 app/shared-components/InsightsPromoModal/InsightsPromoModal.scss create mode 100644 app/shared-components/InsightsPromoModal/index.js create mode 100644 app/shared-components/ModalExitButton/index.js diff --git a/app/panel/components/Login.jsx b/app/panel/components/Login.jsx index 4ea3c807d..3e79fc84c 100644 --- a/app/panel/components/Login.jsx +++ b/app/panel/components/Login.jsx @@ -17,6 +17,7 @@ import ClassNames from 'classnames'; import RSVP from 'rsvp'; import { validateEmail } from '../utils/utils'; import { log } from '../../../src/utils/common'; +import history from '../utils/history'; /** * @class Implement Sign In view which opens from 'Sign In' CTA on the Header. @@ -72,7 +73,14 @@ class Login extends React.Component { }) .finally(() => { this.setState({ loading: false }, () => { - this.props.history.push(this.props.is_expert ? '/detail/blocking' : '/'); + // this.props.history.push({ + // pathname: this.props.is_expert ? '/detail/blocking' : '/', + // state: { showInsightsPromoModal: true } + // }); + history.push({ + pathname: this.props.is_expert ? '/detail/blocking' : '/', + state: { showInsightsPromoModal: true } + }); }); }); } else { @@ -92,6 +100,7 @@ class Login extends React.Component { email, password, emailError, passwordError, loading } = this.state; const buttonClasses = ClassNames('button ghostery-button', { loading }); + console.log('Login history: ', this.props.location.state); return (
diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index fde0bc463..68ac7a2bd 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -17,8 +17,7 @@ import Header from '../containers/HeaderContainer'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; -import { Modal } from '../../shared-components'; -import ModalExitButton from '../../shared-components/ModalExitButton/ModalExitButton'; +import { Modal, InsightsPromoModal, ModalExitButton } from '../../shared-components'; import { login } from '../../Account/AccountActions'; /** * @class Implement base view with functionality common to all views. @@ -86,17 +85,16 @@ class Panel extends React.Component { generateModal = () => { const { loggedIn, user } = this.props; const { hasEngagedFrequently } = this.state; + console.log('user', user); + const isInsightsSubscriber = (user && user.scopes != null) ? user.scopes.includes('subscriptions:insights') : false; if (!loggedIn && hasEngagedFrequently) { return true; } - return hasEngagedFrequently && loggedIn && !user.scopes.includes('subscriptions:insights'); - } - clickSignIn = () => { - this.props.history.push('/login'); - this.setState({ hasEngagedFrequently: false }); + return hasEngagedFrequently && loggedIn && isInsightsSubscriber; } + /** * Reload the current tab * @param {Object} event @@ -315,7 +313,7 @@ class Panel extends React.Component {
- {this.renderModalChildren()} + { this.props.children } diff --git a/app/panel/components/Summary.jsx b/app/panel/components/Summary.jsx index 766e84b0c..e95085c4f 100644 --- a/app/panel/components/Summary.jsx +++ b/app/panel/components/Summary.jsx @@ -17,6 +17,8 @@ import ClassNames from 'classnames'; import Tooltip from './Tooltip'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; +import { Modal, InsightsPromoModal } from '../../shared-components'; +import history from '../utils/history'; import globals from '../../../src/classes/Globals'; import { CliqzFeature, @@ -46,6 +48,7 @@ class Summary extends React.Component { this.state = { trackerLatencyTotal: 0, disableBlocking: false, + showInsightsPromoModal: false }; // Event Bindings @@ -78,6 +81,13 @@ class Summary extends React.Component { this._dynamicUIPort = this.context; this._dynamicUIPort.onMessage.addListener(this.handlePortMessage); this._dynamicUIPort.postMessage({ name: 'SummaryComponentDidMount' }); + + const showModal = (this.props.location.state && this.props.location.state.showInsightsPromoModal) !== undefined ? this.props.location.state.showInsightsPromoModal : false; + const isInsightsSubscriber = (this.props.user && this.props.user.scopes != null) ? this.props.user.scopes.includes('subscriptions:insights') : false; + + if (!isInsightsSubscriber && showModal) { + this.toggleModal(); + } } /** @@ -99,6 +109,16 @@ class Summary extends React.Component { this._dynamicUIPort.onMessage.removeListener(this.handlePortMessage); } + /** + * Function to toggle the Modal + */ + toggleModal = () => { + const { showInsightsPromoModal } = this.state; + this.setState({ + showInsightsPromoModal: !showInsightsPromoModal + }); + } + /** * Handles clicking on Cliqz Features: AntiTracking, AdBlocking, SmartBlocking * @param {Object} options options including: @@ -246,6 +266,7 @@ class Summary extends React.Component { } } + /** * Calculates total tracker latency and sets it to state * @param {Object} props Summary's props, either this.props or nextProps. @@ -792,39 +813,46 @@ class Summary extends React.Component { 'Summary--condensed': isCondensed, }); - return ( -
- {!isCondensed && disableBlocking && ()} - {!isCondensed && !disableBlocking && this._renderDonut()} - {!isCondensed && !disableBlocking && this._renderPageHostReadout()} + // console.log('history: ', this.props.location.state); - {isCondensed && !disableBlocking && this._renderTotalTrackersFound()} + return ( + <> + + + +
+ {!isCondensed && disableBlocking && ()} + {!isCondensed && !disableBlocking && this._renderDonut()} + {!isCondensed && !disableBlocking && this._renderPageHostReadout()} + + {isCondensed && !disableBlocking && this._renderTotalTrackersFound()} + +
+ {!disableBlocking && this._renderTotalTrackersBlocked()} + {!disableBlocking && this._renderTotalRequestsModified()} + {!disableBlocking && this._renderPageLoadTime()} +
-
- {!disableBlocking && this._renderTotalTrackersBlocked()} - {!disableBlocking && this._renderTotalRequestsModified()} - {!disableBlocking && this._renderPageLoadTime()} -
+ {isCondensed && disableBlocking && ( +
+ )} - {isCondensed && disableBlocking && ( -
- )} +
+ {this._renderGhosteryFeature('trust')} + {this._renderGhosteryFeature('restrict', 'Summary__ghosteryFeatureContainer--middle')} + {this._renderPauseButton()} +
+
+ {this._renderCliqzAntiTracking()} + {this._renderCliqzAdBlock()} + {this._renderCliqzSmartBlock()} +
+ {this._renderStatsNavicon()} + {enable_offers && this._renderRewardsNavicon()} -
- {this._renderGhosteryFeature('trust')} - {this._renderGhosteryFeature('restrict', 'Summary__ghosteryFeatureContainer--middle')} - {this._renderPauseButton()} -
-
- {this._renderCliqzAntiTracking()} - {this._renderCliqzAdBlock()} - {this._renderCliqzSmartBlock()} + {!isCondensed && this._renderPlusUpgradeBannerOrSubscriberIcon()}
- {this._renderStatsNavicon()} - {enable_offers && this._renderRewardsNavicon()} - - {!isCondensed && this._renderPlusUpgradeBannerOrSubscriberIcon()} -
+ ); } } diff --git a/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx b/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx new file mode 100644 index 000000000..9407b3358 --- /dev/null +++ b/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx @@ -0,0 +1,88 @@ +/** + * Insights Promo Modal Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import Modal from '../Modal/Modal'; +import history from '../../panel/utils/history'; +import ModalExitButton from '../ModalExitButton/ModalExitButton'; + +// A Functional React component for a Modal +const InsightsPromoModal = ({ toggleModal }) => { + const clickSignIn = () => { + history.push({ + pathname: '/login', + state: { showInsightsPromoModal: true } + }); + toggleModal(); + }; + return ( +
+ +
+
+ Try Ghostery Insights +
+
+ Speed up and clean up digital user experience with our professional tag analytics tool. +
+
+
+
+ +
+ Audit marketing tags on a page +
+
+
+ + + Trace sources of poor performance + +
+
+
+
+ + + Watch pings fire in real-time + +
+
+ + + Explore global digital trends + +
+
+
+
+ + Try for free + +
+ Already a subscriber? Sign in + {/* No thanks, maybe later */} +
+
+
+ ); +}; + +// PropTypes ensure we pass required props of the correct type +Modal.propTypes = { + show: PropTypes.bool.isRequired, + toggleModal: PropTypes.func.isRequired +}; + +export default InsightsPromoModal; diff --git a/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss b/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss new file mode 100644 index 000000000..e69de29bb diff --git a/app/shared-components/InsightsPromoModal/index.js b/app/shared-components/InsightsPromoModal/index.js new file mode 100644 index 000000000..af3281b9b --- /dev/null +++ b/app/shared-components/InsightsPromoModal/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Insights Promo Modal Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import InsightsPromoModal from './InsightsPromoModal'; + +export default InsightsPromoModal; diff --git a/app/shared-components/ModalExitButton/index.js b/app/shared-components/ModalExitButton/index.js new file mode 100644 index 000000000..6745028a1 --- /dev/null +++ b/app/shared-components/ModalExitButton/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Insights Promo Modal Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import ModalExitButton from './ModalExitButton'; + +export default ModalExitButton; diff --git a/app/shared-components/index.js b/app/shared-components/index.js index 7910b2ef5..e2dcdc47e 100644 --- a/app/shared-components/index.js +++ b/app/shared-components/index.js @@ -21,6 +21,8 @@ import SteppedNavigation from './SteppedNavigation'; import ToastMessage from './ToastMessage'; import ToggleCheckbox from './ToggleCheckbox'; import ToggleSwitch from './ToggleSwitch'; +import ModalExitButton from './ModalExitButton'; +import InsightsPromoModal from './InsightsPromoModal'; export { ExitButton, @@ -28,5 +30,7 @@ export { SteppedNavigation, ToastMessage, ToggleCheckbox, - ToggleSwitch + ToggleSwitch, + ModalExitButton, + InsightsPromoModal }; From d57c5b384d7d3e6dd0f0e9bda72f206ccb0f3bd6 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 9 Oct 2019 17:16:32 -0400 Subject: [PATCH 05/39] Clean up insights modal code --- _locales/en/messages.json | 27 ++++ app/panel/components/Login.jsx | 5 - app/panel/components/Panel.jsx | 63 +--------- app/scss/panel.scss | 5 +- app/scss/partials/_colors.scss | 1 + app/scss/partials/_insights_modal.scss | 113 ----------------- .../InsightsPromoModal/InsightsPromoModal.jsx | 89 ++++++------- .../InsightsPromoModal.scss | 118 ++++++++++++++++++ .../ModalExitButton/ModalExitButton.jsx | 6 +- src/classes/Metrics.js | 2 +- 10 files changed, 201 insertions(+), 228 deletions(-) delete mode 100644 app/scss/partials/_insights_modal.scss diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c3659b390..0fdb680f0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -574,6 +574,27 @@ "panel_help_setup": { "message": "Set Up Ghostery" }, + "panel_insights_audit_tags": { + "message": "Audit marketing tags on a page" + }, + "panel_insights_promotion_explore_trends": { + "message": "Explore global digital trends" + }, + "panel_insights_promotion_call_to_action": { + "message": "Try for free" + }, + "panel_insights_promotion_header": { + "message": "Try Ghostery Insights" + }, + "panel_insights_promotion_description": { + "message": "Speed up and clean up digital user experience with our professional tag analytics tool." + }, + "panel_insights_promotion_trace_poor_performance": { + "message": "Trace sources of poor performance" + }, + "panel_insights_promotion_watch_pings": { + "message": "Watch pings fire in real-time" + }, "panel_about_panel_header": { "message": "About Ghostery Browser Extension" }, @@ -2104,9 +2125,15 @@ "subscribe_pitch_button_label": { "message": "Get Ghostery Plus!" }, + "subscribe_pitch_no_thanks": { + "message": "No thanks, maybe later" + }, "subscribe_pitch_sign_here": { "message": "Already a subscriber? Sign in here" }, + "subscribe_pitch_sign_in": { + "message": "Already subscribed? Sign in" + }, "subscription_midnight_theme": { "message": "Midnight Theme" }, diff --git a/app/panel/components/Login.jsx b/app/panel/components/Login.jsx index 3e79fc84c..7dedeb0de 100644 --- a/app/panel/components/Login.jsx +++ b/app/panel/components/Login.jsx @@ -73,10 +73,6 @@ class Login extends React.Component { }) .finally(() => { this.setState({ loading: false }, () => { - // this.props.history.push({ - // pathname: this.props.is_expert ? '/detail/blocking' : '/', - // state: { showInsightsPromoModal: true } - // }); history.push({ pathname: this.props.is_expert ? '/detail/blocking' : '/', state: { showInsightsPromoModal: true } @@ -100,7 +96,6 @@ class Login extends React.Component { email, password, emailError, passwordError, loading } = this.state; const buttonClasses = ClassNames('button ghostery-button', { loading }); - console.log('Login history: ', this.props.location.state); return (
diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 68ac7a2bd..ce30e1e5c 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -17,7 +17,7 @@ import Header from '../containers/HeaderContainer'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; -import { Modal, InsightsPromoModal, ModalExitButton } from '../../shared-components'; +import { InsightsPromoModal } from '../../shared-components'; import { login } from '../../Account/AccountActions'; /** * @class Implement base view with functionality common to all views. @@ -85,12 +85,11 @@ class Panel extends React.Component { generateModal = () => { const { loggedIn, user } = this.props; const { hasEngagedFrequently } = this.state; - console.log('user', user); - const isInsightsSubscriber = (user && user.scopes != null) ? user.scopes.includes('subscriptions:insights') : false; if (!loggedIn && hasEngagedFrequently) { return true; } + const isInsightsSubscriber = (user && user.scopes != null) ? user.scopes.includes('subscriptions:insights') : false; return hasEngagedFrequently && loggedIn && isInsightsSubscriber; } @@ -231,60 +230,6 @@ class Panel extends React.Component { return false; } - renderModalChildren() { - return ( -
- -
-
- Try Ghostery Insights -
-
- Speed up and clean up digital user experience with our professional tag analytics tool. -
-
-
-
- -
- Audit marketing tags on a page -
-
-
- - - Trace sources of poor performance - -
-
-
-
- - - Watch pings fire in real-time - -
-
- - - Explore global digital trends - -
-
-
-
- - Try for free - -
- Already a subscriber? Sign in - No thanks, maybe later -
-
-
- ); - } - /** * React's required render function. Returns JSX * @return {JSX} JSX for rendering the Panel @@ -312,9 +257,7 @@ class Panel extends React.Component {
- - - + { this.props.children } diff --git a/app/scss/panel.scss b/app/scss/panel.scss index 089242bce..57d6827ac 100644 --- a/app/scss/panel.scss +++ b/app/scss/panel.scss @@ -73,10 +73,9 @@ html body { @import './partials/_subscribe'; @import './partials/_stats'; @import './partials/_stats_graph'; -@import './partials/_insights_modal.scss'; // Imports from ../shared-components directory @import './partials/shared_components_svgs'; -@import '../shared-components/Modal/Modal.scss'; -@import '../shared-components/ExitButton/ExitButton.scss'; @import '../shared-components/ModalExitButton/ModalExitButton.scss'; +@import '../shared-components/Modal/Modal.scss'; +@import '../shared-components/InsightsPromoModal/InsightsPromoModal.scss'; diff --git a/app/scss/partials/_colors.scss b/app/scss/partials/_colors.scss index 213fa9cab..d52e7f0ca 100644 --- a/app/scss/partials/_colors.scss +++ b/app/scss/partials/_colors.scss @@ -37,6 +37,7 @@ $ghosty-blue: #00AEF0; $active-blue: #48ACD3; //top_nav_active_tab $link-blue: #2092BF; //primary-color $button-primary: #3AA2CF; +$dark-cyan-blue: #325e97; //insights modal border /* MARKETING COLORS */ $red: #E74055; diff --git a/app/scss/partials/_insights_modal.scss b/app/scss/partials/_insights_modal.scss deleted file mode 100644 index c91ceb97a..000000000 --- a/app/scss/partials/_insights_modal.scss +++ /dev/null @@ -1,113 +0,0 @@ - -.InsightsModal__content { - background-color: #f7f7f7; - position: relative; - width: 518px; - min-height: 437px; - padding-top: 21px; - color: #4a4a4a; - border: 2px solid #325e97; - z-index: 10; -} - -.InsightsModal__image { - height: 94px; - width: 177px; - margin-bottom: 16px; - background-size: 177px 94px; - background-image: url('/app/images/panel/insights-ribbon.svg'); - background-position: center center; - background-repeat: no-repeat; -} -.InsightsModal__header { - font-family: 'Roboto'; - width: 348px; - height: 27.1px; - font-size: 20px; - font-weight: 900; - font-style: normal; - font-stretch: normal; - line-height: 1.35; - letter-spacing: normal; - text-align: center; - color: #4a4a4a; - margin-bottom: 8.9px; -} -.InsightsModal__description { - width: 372px; - height: 54.2px; - font-size: 18px; - font-weight: 500px; - font-style: normal; - font-stretch: normal; - line-height: 1.5; - letter-spacing: normal; - text-align: center; - color: #4a4a4a; - margin-bottom: 12.8px; - font-family: 'Roboto'; -} -.InsightsModal__feature-column-1 { - width: 260px; - margin-left: 28px; -} -.InsightsModal__feature-column-2 { - width: 229.8px; -} -.InsightsModal__featureText { - font-family: 'Roboto'; - font-size: 14px; - line-height: 39px; - color: #333333; -} -.InsightsModal__checkedCircleIcon { - height: 18px; - width: 18px; - margin-right: 8px; - object-fit: contain; - background-image: url('/app/images/panel/checked-circle-icon.svg'); - padding-right: 10px; -} -.InsightsModal__callToActionContainer { - height: 107px; - width: 506px; - margin-top: 10px; - background-color: #e7ecee; -} -.InsightsModal__callToAction { - width: 176px; - height: 36px; - margin-top: 18px; - border-radius: 2px; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.24), 0 0 2px 0 rgba(0, 0, 0, 0.12); - background-image: linear-gradient(101deg, #070e18, #1678a0); -} -.InsightsModal__callToActionText { - font-size: 14px; - font-family: 'Roboto Condensed'; - letter-spacing: .5px; - color: #FFFFFF; - line-height: 36px; -} -.InsightsModal__link { - font-family: 'Roboto'; - font-size: 15px; - color: #4a4a4a; - &:hover { - cursor: pointer; - color: $ghosty-blue; - text-decoration: underline; - text-decoration-color: $link-blue; - } - -} -.InsightsModal__otherOptionsContainer { - margin-top: 23px; - padding: 0 10.5px; - text-decoration: underline; - font-size: 15px; - color: #4a4a4a; -} - - -//TODO: color variables diff --git a/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx b/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx index 9407b3358..23b1dd476 100644 --- a/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx +++ b/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx @@ -18,7 +18,7 @@ import history from '../../panel/utils/history'; import ModalExitButton from '../ModalExitButton/ModalExitButton'; // A Functional React component for a Modal -const InsightsPromoModal = ({ toggleModal }) => { +const InsightsPromoModal = ({ show, toggleModal }) => { const clickSignIn = () => { history.push({ pathname: '/login', @@ -27,61 +27,64 @@ const InsightsPromoModal = ({ toggleModal }) => { toggleModal(); }; return ( -
- -
-
- Try Ghostery Insights -
-
- Speed up and clean up digital user experience with our professional tag analytics tool. -
-
-
-
- -
- Audit marketing tags on a page + +
+ +
+
+ {t('panel_insights_promotion_header')} +
+
+ {t('panel_insights_promotion_description')} +
+
+
+
+ +
+ { t('panel_insights_audit_tags') } +
+
+
+ +
+ { t('panel_insights_promotion_trace_poor_performance') } +
-
- - - Trace sources of poor performance - +
+
+ +
+ { t('panel_insights_promotion_watch_pings') } +
+
+
+ +
+ { t('panel_insights_promotion_explore_trends') } +
+
-
-
- - - Watch pings fire in real-time - +
+ -
- - - Explore global digital trends - +
+ {t('subscribe_pitch_sign_in')} + {t('subscribe_pitch_no_thanks')}
-
- - Try for free - -
- Already a subscriber? Sign in - {/* No thanks, maybe later */} -
-
-
+ ); }; // PropTypes ensure we pass required props of the correct type Modal.propTypes = { - show: PropTypes.bool.isRequired, toggleModal: PropTypes.func.isRequired }; diff --git a/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss b/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss index e69de29bb..9eb6bf0f3 100644 --- a/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss +++ b/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss @@ -0,0 +1,118 @@ +.InsightsModal__content { + background-color: $alabaster; + position: relative; + width: 518px; + min-height: 437px; + padding-top: 21px; + color: $tundora; + border: 2px solid $dark-cyan-blue; + z-index: 10; +} + +.InsightsModal__image { + height: 94px; + width: 177px; + margin-bottom: 16px; + background-size: 177px 94px; + background-image: url('/app/images/panel/insights-ribbon.svg'); + background-position: center center; + background-repeat: no-repeat; +} + +.InsightsModal__header { + font-family: 'Roboto'; + width: 348px; + height: 27.1px; + font-size: 20px; + font-weight: 900; + font-style: normal; + font-stretch: normal; + line-height: 1.35; + letter-spacing: normal; + text-align: center; + color: $tundora; + margin-bottom: 8.9px; +} + +.InsightsModal__description { + width: 372px; + height: 54.2px; + font-size: 18px; + font-weight: 500; + font-style: normal; + font-stretch: normal; + line-height: 1.5; + letter-spacing: normal; + text-align: center; + color: $tundora; + margin-bottom: 12.8px; + font-family: 'Roboto'; +} + +.InsightsModal__features { + &:nth-child(odd) { + margin-left: 28px; + width: 260px; + } + &:nth-child(even) { + width: 229.8px; + } +} + +.InsightsModal__feature-text { + font-family: 'Roboto'; + font-size: 14px; + line-height: 39px; + color: $medium-gray; +} + +.InsightsModal__checked-circle-icon { + height: 18px; + width: 18px; + margin-right: 8px; + object-fit: contain; + background-image: url('/app/images/panel/checked-circle-icon.svg'); + padding-right: 10px; +} + +.InsightsModal__call-to-action-container { + height: 107px; + width: 506px; + margin-top: 10px; + background-color: $mystic; +} + +.InsightsModal__call-to-action { + width: 176px; + height: 36px; + margin-top: 18px; + border-radius: 2px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.24), 0 0 2px 0 rgba(0, 0, 0, 0.12); + background-image: linear-gradient(101deg, #070e18, #1678a0); + font-size: 14px; + font-family: 'Roboto Condensed'; + letter-spacing: .5px; + color: $white; + line-height: 36px; +} + +.InsightsModal__link { + font-family: 'Roboto'; + font-size: 15px; + color: $tundora; + &:hover { + cursor: pointer; + } +} + +.InsightsModal__other-options-container { + margin-top: 23px; + padding: 0 10.5px; + text-decoration: underline; + font-size: 15px; + color: $tundora; +} + +.InsightsModal__call-to-action-container { + +} diff --git a/app/shared-components/ModalExitButton/ModalExitButton.jsx b/app/shared-components/ModalExitButton/ModalExitButton.jsx index ed516319d..92dad90e3 100644 --- a/app/shared-components/ModalExitButton/ModalExitButton.jsx +++ b/app/shared-components/ModalExitButton/ModalExitButton.jsx @@ -22,11 +22,11 @@ import PropTypes from 'prop-types'; */ const ModalExitButton = (props) => { const { - exitModal + toggleModal } = props; return ( - ); @@ -34,7 +34,7 @@ const ModalExitButton = (props) => { // PropTypes ensure we pass required props of the correct type ModalExitButton.propTypes = { - exitModal: PropTypes.func.isRequired + toggleModal: PropTypes.func.isRequired }; export default ModalExitButton; diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index a021a4085..772667449 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -788,7 +788,7 @@ class Metrics { * Toggle the insights promotion if a user has opened the panel 3 times per day for at least 3 days in the past 7 days * @private */ - _hasEngagedFrequently = () => { + _hasEngagedFrequently = () => { const today = new Date().getTime(); const THIRTY_DAYS = 1000 * 60 * 60 * 24 * 30; const insights_promo_modal_last_seen = Number(conf.insights_promo_modal_last_seen) || null; // TODO: Add logic for plus_promotion_last_seen From 2f7fcbc07b24dfb70a4261ba9a7da45160c3e564 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 16 Sep 2019 13:53:50 -0400 Subject: [PATCH 06/39] Show plus promo modal on first view of Home view of each Hub session --- app/hub/Views/HomeView/HomeViewActions.js | 8 +++- app/hub/Views/HomeView/HomeViewConstants.js | 1 + app/hub/Views/HomeView/HomeViewContainer.jsx | 49 ++++++++++++++++++-- app/hub/Views/HomeView/HomeViewReducer.js | 9 +++- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/app/hub/Views/HomeView/HomeViewActions.js b/app/hub/Views/HomeView/HomeViewActions.js index 20f3c5057..3de1b1976 100644 --- a/app/hub/Views/HomeView/HomeViewActions.js +++ b/app/hub/Views/HomeView/HomeViewActions.js @@ -12,7 +12,7 @@ */ import { log, sendMessageInPromise } from '../../utils'; -import { GET_HOME_PROPS, SET_METRICS } from './HomeViewConstants'; +import { GET_HOME_PROPS, MARK_PLUS_PROMO_MODAL_SHOWN, SET_METRICS } from './HomeViewConstants'; export function getHomeProps() { return function(dispatch) { @@ -39,3 +39,9 @@ export function setMetrics(actionData) { }); }; } + +export function markPlusPromoModalShown() { + return { + type: MARK_PLUS_PROMO_MODAL_SHOWN, + }; +} diff --git a/app/hub/Views/HomeView/HomeViewConstants.js b/app/hub/Views/HomeView/HomeViewConstants.js index 02218a87b..2defff14b 100644 --- a/app/hub/Views/HomeView/HomeViewConstants.js +++ b/app/hub/Views/HomeView/HomeViewConstants.js @@ -13,4 +13,5 @@ // Home View export const GET_HOME_PROPS = 'GET_HOME_PROPS'; +export const MARK_PLUS_PROMO_MODAL_SHOWN = 'MARK_PLUS_PROMO_MODAL_SHOWN'; export const SET_METRICS = 'SET_METRICS'; diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index b3660cf85..802bcfd39 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -14,7 +14,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import QueryString from 'query-string'; +import { NavLink } from 'react-router-dom'; import HomeView from './HomeView'; +import { Modal } from '../../../shared-components'; /** * @class Implement the Home View for the Ghostery Hub @@ -28,6 +30,7 @@ class HomeViewContainer extends Component { const { justInstalled } = QueryString.parse(window.location.search); this.state = { justInstalled: justInstalled === 'true', + showPlusPromoModal: !props.home.plus_promo_modal_shown, }; const title = t('hub_home_page_title'); @@ -45,12 +48,38 @@ class HomeViewContainer extends Component { this.props.actions.setMetrics({ enable_metrics }); } + _dismissModal = () => { + this.setState({ + showPlusPromoModal: false, + }); + this.props.actions.markPlusPromoModalShown(); + } + + _renderModalChildren = () => ( +
+
+
+ {t('hub_setup_enter_modal_text')} +
+
+ Select Basic +
+
+
+ + {t('hub_setup_modal_button_no')} + +
+
+
+ ); + /** * React's required render function. Returns JSX * @return {JSX} JSX for rendering the Home View of the Hub app */ render() { - const { justInstalled } = this.state; + const { justInstalled, showPlusPromoModal } = this.state; const { home, user } = this.props; const { setup_complete, @@ -67,7 +96,14 @@ class HomeViewContainer extends Component { isPlus: user && user.subscriptionsPlus || false, }; - return ; + return ( +
+ + {this._renderModalChildren()} + + +
+ ); } } @@ -75,9 +111,10 @@ class HomeViewContainer extends Component { // Note: isRequired is not needed when a prop has a default value HomeViewContainer.propTypes = { home: PropTypes.shape({ + enable_metrics: PropTypes.bool, + plus_promo_modal_shown: PropTypes.bool, setup_complete: PropTypes.bool, tutorial_complete: PropTypes.bool, - enable_metrics: PropTypes.bool, }), user: PropTypes.shape({ email: PropTypes.string, @@ -85,17 +122,19 @@ HomeViewContainer.propTypes = { }), actions: PropTypes.shape({ getHomeProps: PropTypes.func.isRequired, - setMetrics: PropTypes.func.isRequired, getUser: PropTypes.func.isRequired, + markPlusPromoModalShown: PropTypes.func.isRequired, + setMetrics: PropTypes.func.isRequired, }).isRequired, }; // Default props used on the Home View HomeViewContainer.defaultProps = { home: { + enable_metrics: false, + plus_promo_modal_shown: false, setup_complete: false, tutorial_complete: false, - enable_metrics: false, }, user: { email: '', diff --git a/app/hub/Views/HomeView/HomeViewReducer.js b/app/hub/Views/HomeView/HomeViewReducer.js index e14aaa559..f60c13123 100644 --- a/app/hub/Views/HomeView/HomeViewReducer.js +++ b/app/hub/Views/HomeView/HomeViewReducer.js @@ -11,7 +11,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { GET_HOME_PROPS, SET_METRICS } from './HomeViewConstants'; +import { GET_HOME_PROPS, MARK_PLUS_PROMO_MODAL_SHOWN, SET_METRICS } from './HomeViewConstants'; const initialState = {}; @@ -31,6 +31,13 @@ function HomeViewReducer(state = initialState, action) { }), }); } + case MARK_PLUS_PROMO_MODAL_SHOWN: { + return Object.assign({}, state, { + home: Object.assign({}, state.home, { + plus_promo_modal_shown: true, + }) + }); + } case SET_METRICS: { const { enable_metrics } = action.data; return Object.assign({}, state, { From e94a749efd73a9d0a32e39b3c5d7e9821e4256d8 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 16 Sep 2019 18:27:58 -0400 Subject: [PATCH 07/39] Stub out intro hub plus promo modal layout and implement its buttons --- app/hub/Views/HomeView/HomeView.scss | 45 ++++++++++++ app/hub/Views/HomeView/HomeViewContainer.jsx | 77 ++++++++++++++------ 2 files changed, 98 insertions(+), 24 deletions(-) diff --git a/app/hub/Views/HomeView/HomeView.scss b/app/hub/Views/HomeView/HomeView.scss index 00fbf9b47..86f1d26e8 100644 --- a/app/hub/Views/HomeView/HomeView.scss +++ b/app/hub/Views/HomeView/HomeView.scss @@ -152,6 +152,51 @@ } } +.PlusPromoModal__content { + position: relative; + width: 646px; + height: 553px; + background-color: #f7f7f7; + border: 1px solid #930194; + z-index: 10; +} + +.PlusPromoModal__thanks-for-download { + padding-top: 38px; + font-size: 24px; + font-weight: bold; +} + +.PlusPromoModal__choose-your-plan { + padding-top: 10px; + font-size: 18px; + font-weight: 500; +} + +.PlusPromoModal__option-boxes-container { + width: 100%; +} + +.PlusPromoModal__basic-box { + float: left; + margin-left: 55px; + width: 240px; + height: 298px; + border: solid 3px #1dafed; + background-color: #ffffff; +} + +.PlusPromoModal__plus-box { + float: right; + margin-right: 55px; + width: 240px; + height: 298px; + border-style: solid; + border-width: 3px; + border-image-source: linear-gradient(39deg, #f2daa2, #eab968); + border-image-slice: 1; +} + // Firefox Font Size Override and Image Size Override @-moz-document url-prefix() { .HomeView--firefoxFontSize { diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index 802bcfd39..717de1893 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -29,58 +29,77 @@ class HomeViewContainer extends Component { const { justInstalled } = QueryString.parse(window.location.search); this.state = { + getUserResolved: false, justInstalled: justInstalled === 'true', - showPlusPromoModal: !props.home.plus_promo_modal_shown, + plusPromoModalShown: props.home.plus_promo_modal_shown, }; const title = t('hub_home_page_title'); window.document.title = title; props.actions.getHomeProps(); - props.actions.getUser(); + + // Prevent flickering in of user's email if getUser() returns after initial render, + // as well as flickering of plus promo modal if user is already a subscriber + props.actions.getUser() + .then(() => { + this.setState({ + getUserResolved: true, + }); + }); } /** - * Function to handle toggling Metrics Opt-In - */ + * @private + * Function to handle toggling Metrics Opt-In + */ _handleToggleMetrics = () => { const enable_metrics = !this.props.home.enable_metrics; this.props.actions.setMetrics({ enable_metrics }); } + /** + * @private + * Dismisses the Plus promo modal if user opts to stick with the basic plan + */ _dismissModal = () => { this.setState({ - showPlusPromoModal: false, + plusPromoModalShown: true, }); this.props.actions.markPlusPromoModalShown(); } _renderModalChildren = () => ( -
-
-
- {t('hub_setup_enter_modal_text')} +
+
+ Thanks for downloading Ghostery! +
+
+ Choose your privacy plan
-
- Select Basic +
+
+ Ghostery Basic +
+
+ Ghostery Plus +
-
-
- - {t('hub_setup_modal_button_no')} - +
+
+ Select Basic
+ + Select Plus +
); - /** - * React's required render function. Returns JSX - * @return {JSX} JSX for rendering the Home View of the Hub app - */ - render() { - const { justInstalled, showPlusPromoModal } = this.state; + _render() { + const { justInstalled, plusPromoModalShown } = this.state; const { home, user } = this.props; + const isPlus = user && user.subscriptionsPlus || false; const { setup_complete, tutorial_complete, @@ -93,18 +112,28 @@ class HomeViewContainer extends Component { enable_metrics, changeMetrics: this._handleToggleMetrics, email: user ? user.email : '', - isPlus: user && user.subscriptionsPlus || false, + isPlus, }; return (
- + {this._renderModalChildren()}
); } + + /** + * React's required render function. Returns JSX + * @return {JSX} JSX for rendering the Home View of the Hub app + */ + render() { + const { getUserResolved } = this.state; + + return (getUserResolved ? this._render() : null); + } } // PropTypes ensure we pass required props of the correct type From 894d74225deaacacfac4fe067bf2960b35e7b1bc Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 17 Sep 2019 09:59:39 -0400 Subject: [PATCH 08/39] Remove some unnecessary plus promo modal related CSS --- app/hub/Views/HomeView/HomeView.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/hub/Views/HomeView/HomeView.scss b/app/hub/Views/HomeView/HomeView.scss index 86f1d26e8..fbc1e4f04 100644 --- a/app/hub/Views/HomeView/HomeView.scss +++ b/app/hub/Views/HomeView/HomeView.scss @@ -173,10 +173,6 @@ font-weight: 500; } -.PlusPromoModal__option-boxes-container { - width: 100%; -} - .PlusPromoModal__basic-box { float: left; margin-left: 55px; From ab4c58ab34a821983b92cec1a2f444b746c050bb Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 17 Sep 2019 16:49:05 -0400 Subject: [PATCH 09/39] Continue adding styling for plus promo modal --- app/hub/Views/HomeView/HomeView.scss | 39 ++++++++++++++++++++ app/hub/Views/HomeView/HomeViewContainer.jsx | 18 +++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/app/hub/Views/HomeView/HomeView.scss b/app/hub/Views/HomeView/HomeView.scss index fbc1e4f04..19e5b99b0 100644 --- a/app/hub/Views/HomeView/HomeView.scss +++ b/app/hub/Views/HomeView/HomeView.scss @@ -173,6 +173,33 @@ font-weight: 500; } +.PlusPromoModal__option-header { + margin-top: 10px; + font-size: 19px; + font-weight: bold; + text-align: center; +} + +.PlusPromoModal__price-text { + text-align: center; +} + +.PlusPromoModal__currency-sign { + font-size: 30px; + font-weight: bold; +} + +.PlusPromoModal__amount { + vertical-align: middle; + font-size: 58px; +} + +.PlusPromoModal__per-month { + vertical-align: sub; + font-size: 18px; + font-weight: bold; +} + .PlusPromoModal__basic-box { float: left; margin-left: 55px; @@ -180,6 +207,12 @@ height: 298px; border: solid 3px #1dafed; background-color: #ffffff; + + .PlusPromoModal__option-header, + .PlusPromoModal__price-text, + .PlusPromoModal__amount { + color: #1dafed; + } } .PlusPromoModal__plus-box { @@ -191,6 +224,12 @@ border-width: 3px; border-image-source: linear-gradient(39deg, #f2daa2, #eab968); border-image-slice: 1; + + .PlusPromoModal__option-header, + .PlusPromoModal__price-text, + .PlusPromoModal__amount { + color: #ebbf73; + } } // Firefox Font Size Override and Image Size Override diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index 717de1893..c1ee9db24 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -69,7 +69,7 @@ class HomeViewContainer extends Component { this.props.actions.markPlusPromoModalShown(); } - _renderModalChildren = () => ( + _renderPlusPromoModal = () => (
Thanks for downloading Ghostery! @@ -79,10 +79,20 @@ class HomeViewContainer extends Component {
- Ghostery Basic +
Ghostery Basic
+
+ $ + 0 + per month +
- Ghostery Plus +
Ghostery Plus
+
+ $ + 2 + per month +
@@ -118,7 +128,7 @@ class HomeViewContainer extends Component { return (
- {this._renderModalChildren()} + {this._renderPlusPromoModal()}
From 3cd79ce9132bec1de78d1cdf12b03132989bd337 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Wed, 18 Sep 2019 11:32:24 -0400 Subject: [PATCH 10/39] Continue styling plus promo modal --- app/hub/Views/HomeView/HomeView.scss | 18 ++++++++++++++++++ app/hub/Views/HomeView/HomeViewContainer.jsx | 20 +++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/app/hub/Views/HomeView/HomeView.scss b/app/hub/Views/HomeView/HomeView.scss index 19e5b99b0..8040ccd5e 100644 --- a/app/hub/Views/HomeView/HomeView.scss +++ b/app/hub/Views/HomeView/HomeView.scss @@ -200,6 +200,11 @@ font-weight: bold; } +.PlusPromoModal__option-boxes-container { + margin-top: 37px; + margin-bottom: 30px; +} + .PlusPromoModal__basic-box { float: left; margin-left: 55px; @@ -232,6 +237,19 @@ } } +.PlusPromoModal__option-description-item { + font-size: 16px; + text-align: center; + color: #333333; +} + +.PlusPromoModal__button-container { + width: 98.6%; + margin: 0 auto; + height: 72px; + background-color: #e7ecee; +} + // Firefox Font Size Override and Image Size Override @-moz-document url-prefix() { .HomeView--firefoxFontSize { diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index c1ee9db24..2857a7885 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -77,7 +77,7 @@ class HomeViewContainer extends Component {
Choose your privacy plan
-
+
Ghostery Basic
@@ -85,6 +85,18 @@ class HomeViewContainer extends Component { 0 per month
+
+

+ Protection for + + one + + browser +

+

Blocks Ads

+

Blocks Trackers

+

Fast Browsing

+
Ghostery Plus
@@ -93,6 +105,12 @@ class HomeViewContainer extends Component { 2 per month
+
+

Ghostery Basic

+

Historical Tracker Stats

+

Priority Support

+

New Color Themes

+
From b476ce1a7d4dd1570d9f5683782021df59c91a29 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Fri, 20 Sep 2019 09:46:25 -0400 Subject: [PATCH 11/39] Refactor plus promo modal CSS to improve alignment of elements --- app/hub/Views/HomeView/HomeView.scss | 78 +++++++++++++------ app/hub/Views/HomeView/HomeViewContainer.jsx | 80 +++++++++++--------- 2 files changed, 97 insertions(+), 61 deletions(-) diff --git a/app/hub/Views/HomeView/HomeView.scss b/app/hub/Views/HomeView/HomeView.scss index 8040ccd5e..0ad3b3145 100644 --- a/app/hub/Views/HomeView/HomeView.scss +++ b/app/hub/Views/HomeView/HomeView.scss @@ -173,6 +173,18 @@ font-weight: 500; } +.PlusPromoModal__options-container { + width: 100%; + display: flex; + justify-content: space-around; +} + +.PlusPromoModal__option-container { + display: flex; + justify-content: center; + flex-direction: column; +} + .PlusPromoModal__option-header { margin-top: 10px; font-size: 19px; @@ -205,28 +217,22 @@ margin-bottom: 30px; } -.PlusPromoModal__basic-box { - float: left; - margin-left: 55px; +.PlusPromoModal__option-description-box { width: 240px; height: 298px; - border: solid 3px #1dafed; - background-color: #ffffff; + border-style: solid; + border-width: 3px; + background-color: #FFFFFF; +} - .PlusPromoModal__option-header, - .PlusPromoModal__price-text, - .PlusPromoModal__amount { - color: #1dafed; - } +.PlusPromoModal__option-description-box--basic { + border-color: #1dafed; + color: #1dafed; } .PlusPromoModal__plus-box { float: right; margin-right: 55px; - width: 240px; - height: 298px; - border-style: solid; - border-width: 3px; border-image-source: linear-gradient(39deg, #f2daa2, #eab968); border-image-slice: 1; @@ -248,20 +254,44 @@ margin: 0 auto; height: 72px; background-color: #e7ecee; + display: flex; + justify-content: space-around; + align-items: center; + + .PlusPromoModal__button { + background-color: white; + color: #2cbcf4; + border: solid 2px #15b4f2; + } +} + +.PlusPromoModal__button { + margin: 0; + font-size: 12px; + font-weight: 500; + width: 135px; + background-color: white; + height: 40px; + align-items: center; + display: flex; + justify-content: center; + color: #2cbcf4; + border: solid 2px #15b4f2; + box-shadow: none; } // Firefox Font Size Override and Image Size Override @-moz-document url-prefix() { - .HomeView--firefoxFontSize { - font-size: 75%; - } +.HomeView--firefoxFontSize { + font-size: 75%; +} +.HomeView--firefoxImageSize { + height: 113.875px; +} +@media only screen and (max-width: 740px) { .HomeView--firefoxImageSize { - height: 113.875px; - } - @media only screen and (max-width: 740px) { - .HomeView--firefoxImageSize { - min-width: 126px !important; - max-width: 146px !important; - } + min-width: 126px !important; + max-width: 146px !important; } } +} diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index 2857a7885..26928aa2e 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -77,50 +77,56 @@ class HomeViewContainer extends Component {
Choose your privacy plan
-
-
-
Ghostery Basic
-
- $ - 0 - per month -
-
-

- Protection for - +

+
+
+
Ghostery Basic
+
+ $ + 0 + per month +
+
+

+ Protection for + one - - browser -

-

Blocks Ads

-

Blocks Trackers

-

Fast Browsing

+ + browser +

+

Blocks Ads

+

Blocks Trackers

+

Fast Browsing

+
+
+
+
+ Select Basic +
-
-
Ghostery Plus
-
- $ - 2 - per month +
+
+
Ghostery Plus
+
+ $ + 2 + per month +
+
+

Ghostery Basic

+

Historical Tracker Stats

+

Priority Support

+

New Color Themes

+
-
-

Ghostery Basic

-

Historical Tracker Stats

-

Priority Support

-

New Color Themes

+
-
-
- Select Basic -
- - Select Plus - -
); From 4666877a48d05e572e01c40c1dd1d255498ae8e3 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Fri, 20 Sep 2019 18:04:03 -0400 Subject: [PATCH 12/39] Continue CSS work for plus promo modal --- app/hub/Views/HomeView/HomeView.scss | 69 ++++++++++---------- app/hub/Views/HomeView/HomeViewContainer.jsx | 20 +++--- 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/app/hub/Views/HomeView/HomeView.scss b/app/hub/Views/HomeView/HomeView.scss index 0ad3b3145..0deb40690 100644 --- a/app/hub/Views/HomeView/HomeView.scss +++ b/app/hub/Views/HomeView/HomeView.scss @@ -174,15 +174,34 @@ } .PlusPromoModal__options-container { - width: 100%; + height: 100%; + margin-top: 32px; display: flex; - justify-content: space-around; } .PlusPromoModal__option-container { display: flex; - justify-content: center; + width: 50%; + align-items: center; flex-direction: column; + justify-content: space-between; +} + +.PlusPromoModal__option-description-box { + width: 240px; + height: 298px; + border-style: solid; + border-width: 3px; + background-color: #FFFFFF; + + &.basic { + border-color: #1dafed; + } + + &.plus { + border-image-source: linear-gradient(39deg, #f2daa2, #eab968); + border-image-slice: 1; + } } .PlusPromoModal__option-header { @@ -190,10 +209,16 @@ font-size: 19px; font-weight: bold; text-align: center; + + &.basic { color: #1dafed; } + &.plus { color: #ebbf73; } } .PlusPromoModal__price-text { text-align: center; + + &.basic { color: #1dafed; } + &.plus { color: #ebbf73; } } .PlusPromoModal__currency-sign { @@ -212,37 +237,6 @@ font-weight: bold; } -.PlusPromoModal__option-boxes-container { - margin-top: 37px; - margin-bottom: 30px; -} - -.PlusPromoModal__option-description-box { - width: 240px; - height: 298px; - border-style: solid; - border-width: 3px; - background-color: #FFFFFF; -} - -.PlusPromoModal__option-description-box--basic { - border-color: #1dafed; - color: #1dafed; -} - -.PlusPromoModal__plus-box { - float: right; - margin-right: 55px; - border-image-source: linear-gradient(39deg, #f2daa2, #eab968); - border-image-slice: 1; - - .PlusPromoModal__option-header, - .PlusPromoModal__price-text, - .PlusPromoModal__amount { - color: #ebbf73; - } -} - .PlusPromoModal__option-description-item { font-size: 16px; text-align: center; @@ -250,14 +244,17 @@ } .PlusPromoModal__button-container { - width: 98.6%; - margin: 0 auto; + width: 100%; + margin-bottom: 3px; height: 72px; background-color: #e7ecee; display: flex; justify-content: space-around; align-items: center; + &.left { margin-left: 3px; } + &.right { margin-right: 3px; } + .PlusPromoModal__button { background-color: white; color: #2cbcf4; diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index 26928aa2e..7228058a0 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -78,10 +78,10 @@ class HomeViewContainer extends Component { Choose your privacy plan
-
-
-
Ghostery Basic
-
+
+
+
Ghostery Basic
+
$ 0 per month @@ -99,16 +99,16 @@ class HomeViewContainer extends Component {

Fast Browsing

-
+
Select Basic
-
-
-
Ghostery Plus
-
+
+
+
Ghostery Plus
+
$ 2 per month @@ -120,7 +120,7 @@ class HomeViewContainer extends Component {

New Color Themes

-
+
Select Plus From ff4d45582b7e4c107fdf028d74dc7dde18e934b4 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 23 Sep 2019 10:18:16 -0400 Subject: [PATCH 13/39] Implement recommended gold banner in plus promo modal --- app/hub/Views/HomeView/HomeView.scss | 19 ++++++++++++++++++- app/hub/Views/HomeView/HomeViewContainer.jsx | 4 ++++ app/images/hub/home/recommended-banner.svg | 13 +++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 app/images/hub/home/recommended-banner.svg diff --git a/app/hub/Views/HomeView/HomeView.scss b/app/hub/Views/HomeView/HomeView.scss index 0deb40690..22be9116f 100644 --- a/app/hub/Views/HomeView/HomeView.scss +++ b/app/hub/Views/HomeView/HomeView.scss @@ -157,7 +157,7 @@ width: 646px; height: 553px; background-color: #f7f7f7; - border: 1px solid #930194; + border: 1.9px solid #930194; z-index: 10; } @@ -199,11 +199,28 @@ } &.plus { + position: relative; // so that the recommended banner is positioned relative to this element border-image-source: linear-gradient(39deg, #f2daa2, #eab968); border-image-slice: 1; } } +.PlusPromoModal__recommended-banner { + position: absolute; + left: -30px; + top: -30px; +} + +.PlusPromoModal__recommended-banner-text { + position: relative; + left: 14px; + top: -46px; + transform: rotate(-13deg); + color: white; + font-size: 16px; + font-weight: bold; +} + .PlusPromoModal__option-header { margin-top: 10px; font-size: 19px; diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index 7228058a0..0e5ae94ac 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -107,6 +107,10 @@ class HomeViewContainer extends Component {
+
+ +
Recommended
+
Ghostery Plus
$ diff --git a/app/images/hub/home/recommended-banner.svg b/app/images/hub/home/recommended-banner.svg new file mode 100644 index 000000000..4777a7728 --- /dev/null +++ b/app/images/hub/home/recommended-banner.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file From 446d137568e221fc8626537c573b0a1550aec871 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Fri, 27 Sep 2019 15:56:37 -0400 Subject: [PATCH 14/39] Update and tweak plus promo modal design --- app/hub/Views/HomeView/HomeView.scss | 89 ++++++++++++-------- app/hub/Views/HomeView/HomeViewContainer.jsx | 34 +++----- app/scss/hub.scss | 1 + 3 files changed, 68 insertions(+), 56 deletions(-) diff --git a/app/hub/Views/HomeView/HomeView.scss b/app/hub/Views/HomeView/HomeView.scss index 22be9116f..d587e2629 100644 --- a/app/hub/Views/HomeView/HomeView.scss +++ b/app/hub/Views/HomeView/HomeView.scss @@ -161,35 +161,44 @@ z-index: 10; } +.PlusPromoModal__buttons-background { + background-color: #e7ecee; + position: absolute; + bottom: 3px; + width: 99%; + height: 72px; + z-index: -1; +} + .PlusPromoModal__thanks-for-download { - padding-top: 38px; - font-size: 24px; + padding-top: 30px; + font-size: 26px; font-weight: bold; } .PlusPromoModal__choose-your-plan { padding-top: 10px; font-size: 18px; - font-weight: 500; + font-weight: bold; } .PlusPromoModal__options-container { height: 100%; - margin-top: 32px; + margin-top: 44px; display: flex; + justify-content: space-evenly; } .PlusPromoModal__option-container { display: flex; - width: 50%; align-items: center; flex-direction: column; - justify-content: space-between; } .PlusPromoModal__option-description-box { width: 240px; height: 298px; + margin-bottom: 45px; border-style: solid; border-width: 3px; background-color: #FFFFFF; @@ -213,18 +222,19 @@ .PlusPromoModal__recommended-banner-text { position: relative; - left: 14px; - top: -46px; - transform: rotate(-13deg); + left: 18px; + top: -47px; + transform: rotate(-15deg); color: white; font-size: 16px; font-weight: bold; } .PlusPromoModal__option-header { - margin-top: 10px; + margin-top: 20px; font-size: 19px; font-weight: bold; + letter-spacing: -0.5px; text-align: center; &.basic { color: #1dafed; } @@ -232,6 +242,7 @@ } .PlusPromoModal__price-text { + line-height: 1.3; text-align: center; &.basic { color: #1dafed; } @@ -245,6 +256,7 @@ .PlusPromoModal__amount { vertical-align: middle; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 58px; } @@ -258,40 +270,47 @@ font-size: 16px; text-align: center; color: #333333; -} -.PlusPromoModal__button-container { - width: 100%; - margin-bottom: 3px; - height: 72px; - background-color: #e7ecee; - display: flex; - justify-content: space-around; - align-items: center; - - &.left { margin-left: 3px; } - &.right { margin-right: 3px; } - - .PlusPromoModal__button { - background-color: white; - color: #2cbcf4; - border: solid 2px #15b4f2; + &.bold { + font-weight: bold; + color: #4a4a4a; } } .PlusPromoModal__button { margin: 0; - font-size: 12px; - font-weight: 500; - width: 135px; - background-color: white; - height: 40px; - align-items: center; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.5px; display: flex; + align-items: center; justify-content: center; - color: #2cbcf4; - border: solid 2px #15b4f2; box-shadow: none; + + &.basic { + width: 135px; + height: 40px; + border: solid 2px #15b4f2; + background-color: white; + color: #2cbcf4; + } + &.basic:hover { + background-color: #2cbcf4; + color: white; + } + + &.plus { + width: 163px; + height: 38px; + border: none; + background-image: linear-gradient(to bottom, #2fdbfa, #15b4f2); + color: white; + font-weight: 600; + letter-spacing: 0.5px; + } + &.plus:hover { + background-image: linear-gradient(to bottom, #1fcbea, #05a4e2); + } } // Firefox Font Size Override and Image Size Override diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index 0e5ae94ac..51743392a 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -14,7 +14,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import QueryString from 'query-string'; -import { NavLink } from 'react-router-dom'; import HomeView from './HomeView'; import { Modal } from '../../../shared-components'; @@ -71,6 +70,7 @@ class HomeViewContainer extends Component { _renderPlusPromoModal = () => (
+
Thanks for downloading Ghostery!
@@ -84,25 +84,18 @@ class HomeViewContainer extends Component {
$ 0 + per month
-

- Protection for - - one - - browser -

+

Protection for this browser

Blocks Ads

Blocks Trackers

Fast Browsing

-
-
- Select Basic -
+
+ Select Basic
@@ -115,20 +108,19 @@ class HomeViewContainer extends Component {
$ 2 + per month
-

Ghostery Basic

-

Historical Tracker Stats

-

Priority Support

-

New Color Themes

+

All basic features, plus:

+

Historical Tracker Stats

+

Priority Support

+

New Color Themes

- + + Select Plus +
diff --git a/app/scss/hub.scss b/app/scss/hub.scss index 1faa03903..0c55b41a4 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -53,6 +53,7 @@ html, body, #root { @import './partials/_hub_mixins'; @import './partials/_hub_svgs'; @import './partials/_shared_components_svgs'; +@import './partials/_fonts'; // Imports from ../hub directory @import '../hub/Views/SideNavigationView/SideNavigationView.scss'; From 5afd2ce63c947290412a64ed8aeddfb84c77ea6b Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 1 Oct 2019 14:26:01 -0400 Subject: [PATCH 15/39] Fix path bug in i18n-checker tool. Consolidate redundant entries in messages. Add check icon to plus promo modal. Modify plus promo modal css. --- _locales/en/messages.json | 322 +++++++----------- app/content-scripts/rewards/OfferCard.jsx | 2 +- .../CreateAccountView/CreateAccountView.jsx | 12 +- app/hub/Views/HomeView/HomeView.jsx | 8 +- app/hub/Views/HomeView/HomeView.scss | 15 +- app/hub/Views/HomeView/HomeViewContainer.jsx | 43 ++- app/hub/Views/LogInView/LogInView.jsx | 8 +- app/hub/Views/PlusView/PlusView.jsx | 22 +- app/hub/Views/RewardsView/RewardsView.jsx | 2 +- .../SetupAntiSuiteViewContainer.jsx | 4 +- .../SetupBlockingDropdown.jsx | 2 +- .../SetupBlockingViewContainer.jsx | 2 +- .../SetupDoneView/SetupDoneViewContainer.jsx | 10 +- .../SetupHumanWebViewContainer.jsx | 4 +- .../SideNavigationViewContainer.jsx | 10 +- .../TutorialAntiSuiteView.jsx | 8 +- .../TutorialAntiSuiteViewContainer.jsx | 4 +- .../TutorialBlockingView.jsx | 4 +- .../TutorialBlockingViewContainer.jsx | 4 +- .../TutorialLayoutView/TutorialLayoutView.jsx | 4 +- .../TutorialLayoutViewContainer.jsx | 4 +- .../TutorialTrackerListViewContainer.jsx | 4 +- .../TutorialTrustView/TutorialTrustView.jsx | 8 +- .../TutorialTrustViewContainer.jsx | 4 +- .../TutorialVideoViewContainer.jsx | 2 +- app/images/hub/home/check-icon.svg | 12 + .../BuildingBlocks/CliqzFeature.jsx | 2 +- app/panel/components/CreateAccount.jsx | 12 +- app/panel/components/DetailMenu.jsx | 2 +- app/panel/components/ForgotPassword.jsx | 2 +- app/panel/components/Header.jsx | 6 +- app/panel/components/HeaderMenu.jsx | 6 +- app/panel/components/Help.jsx | 2 +- app/panel/components/Login.jsx | 6 +- app/panel/components/Rewards.jsx | 2 +- app/panel/components/StatsView.jsx | 2 +- app/panel/components/Subscribe.jsx | 2 +- .../Subscription/PrioritySupport.jsx | 2 +- .../Subscription/SubscriptionInfo.jsx | 6 +- .../Subscription/SubscriptionMenu.jsx | 4 +- app/panel/components/Summary.jsx | 2 +- app/panel/reducers/panel.js | 2 +- tools/i18n-checker.js | 14 +- 43 files changed, 271 insertions(+), 327 deletions(-) create mode 100644 app/images/hub/home/check-icon.svg diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 0fdb680f0..2b5368b39 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -394,12 +394,6 @@ } } }, - "email_confirm_field_label": { - "message": "Confirm Email:" - }, - "email_field_label": { - "message": "Email:" - }, "error_email_forgot": { "message": "Hmm, I can’t find that email. Maybe try another?" }, @@ -409,12 +403,6 @@ "forgot_password_message": { "message": "Forgot your password? Hey, it happens to everyone. We'll email you instructions to reset your password." }, - "invalid_email_confirmation": { - "message": "Your emails do not match." - }, - "invalid_email_create": { - "message": "Please enter a valid email." - }, "invalid_email_forgot": { "message": "Sorry, you have to enter a real email address." }, @@ -532,9 +520,6 @@ "description": "as in '5 (trackers) WHITELISTED'", "message": "Whitelisted" }, - "panel_create_account": { - "message": "Create Account" - }, "panel_email_verification_sent": { "message": "$BOLD_HTML_STARTS$ Success! $BOLD_HTML_END$ An account verification email has been sent to $EMAIL$", "placeholders": { @@ -568,9 +553,6 @@ "panel_help_questions_header": { "message": "Questions and Comments" }, - "panel_help_support": { - "message": "Support" - }, "panel_help_setup": { "message": "Set Up Ghostery" }, @@ -644,15 +626,6 @@ "panel_menu_about": { "message": "About" }, - "panel_menu_ghostery_subscriber": { - "message": "Ghostery Plus" - }, - "panel_menu_signin": { - "message": "Sign in" - }, - "panel_menu_signout": { - "message": "Sign out" - }, "global_settings_saved": { "message": "Global Settings Saved" }, @@ -712,9 +685,6 @@ "panel_signin_success_title": { "message": "Congrats! Your Ghostery account is ready to go." }, - "panel_title_create_account": { - "message": "Create Account" - }, "panel_tracker_found_sources_title": { "message": "Detected tracker URLs:" }, @@ -760,15 +730,9 @@ "panel_detail_menu_rewards_title": { "message": "Rewards" }, - "panel_detail_menu_stats_title": { - "message": "Historical stats" - }, "panel_detail_menu_premium_title": { "message": "Premium" }, - "panel_detail_learn_more": { - "message": "Learn More" - }, "panel_detail_history_title": { "message": "History" }, @@ -823,27 +787,15 @@ "notification_library_update_link": { "message": "Check out what's new" }, - "panel_header_sign_in": { - "message": "Sign In" - }, "panel_header_verify_account": { "message": "Verify Account" }, - "panel_header_simple_view": { - "message": "Simple View" - }, - "panel_header_detailed_view": { - "message": "Detailed View" - }, "password_characters_requirements": { "message": "Only these special characters are allowed: !@#$%^&*=+()<>{}[];:,./?" }, "password_field_label": { "message": "Password" }, - "create_password_field_label": { - "message": "Password:" - }, "password_requirements": { "message": "Use between 8 and 50 characters." }, @@ -1192,12 +1144,6 @@ "drawer_status_enable_smart_block": { "message": "Smart Blocking is" }, - "drawer_on": { - "message": "On" - }, - "drawer_off": { - "message": "Off" - }, "blocking_unblock_all": { "message": "Unblock All" }, @@ -1315,12 +1261,6 @@ "tooltip_resume": { "message": "Resume Ghostery" }, - "tooltip_expert": { - "message": "Detailed View" - }, - "tooltip_simple": { - "message": "Simple View" - }, "license_module": { "message": "Module" }, @@ -1371,30 +1311,15 @@ "hub_side_navigation_home": { "message": "Home" }, - "hub_side_navigation_setup": { - "message": "Customize Setup" - }, "hub_side_navigation_tutorial": { "message": "Take a Tutorial" }, - "hub_side_navigation_supporter": { - "message": "Get Ghostery Plus" - }, "hub_side_navigation_rewards": { "message": "Try Ghostery Rewards" }, "hub_side_navigation_products": { "message": "See More Ghostery Products" }, - "hub_side_navigation_create_account": { - "message": "Create Account" - }, - "hub_side_navigation_log_in": { - "message": "Sign In" - }, - "hub_side_navigation_log_out": { - "message": "Sign Out" - }, "hub_home_page_title": { "message": "Ghostery Hub - Home" }, @@ -1425,24 +1350,15 @@ "hub_home_subheader_optimize": { "message": "Optimize your Ghostery experience" }, - "hub_home_subheader_create_account": { - "message": "Create Account" - }, "hub_home_feature_tutorial_title": { "message": "Take a Tutorial" }, "hub_home_feature_tutorial_text": { "message": "Walk through Ghostery's main features." }, - "hub_home_feature_tutorial_button": { - "message": "Start" - }, "hub_home_feature_tutorial_button_alt": { "message": "Tutorial Complete" }, - "hub_home_feature_setup_title": { - "message": "Customize Setup" - }, "hub_home_feature_setup_text": { "message": "Edit your settings and blocking preferences." }, @@ -1455,12 +1371,6 @@ "hub_home_feature_supporter_text": { "message": "Upgrade to Ghostery Plus and unlock special features." }, - "hub_home_feature_supporter_button": { - "message": "Get Ghostery Plus" - }, - "hub_home_feature_supporter_button_alt": { - "message": "Already Subscribed" - }, "hub_setup_page_title": { "message": "Ghostery Hub - Setup" }, @@ -1496,15 +1406,6 @@ "hub_setup_exit_flow": { "message": "Exit Custom Setup" }, - "hub_setup_nav_previous": { - "message": "Previous" - }, - "hub_setup_nav_next": { - "message": "Next" - }, - "hub_setup_nav_done": { - "message": "Done" - }, "hub_setup_enter_modal_text": { "message": "Entering custom setup will override your previous Ghostery settings. Do you want to continue?" }, @@ -1602,18 +1503,9 @@ "hub_setup_feature_tutorial_description": { "message": "Walk through Ghostery's main features." }, - "hub_setup_feature_tutorial_button": { - "message": "Start" - }, - "hub_setup_feature_supporter_title": { - "message": "Get Ghostery Plus" - }, "hub_setup_feature_supporter_description": { "message": "Support Ghostery and unlock special perks." }, - "hub_setup_feature_supporter_button": { - "message": "Support" - }, "hub_setup_feature_products_title": { "message": "See more Ghostery Products" }, @@ -1647,21 +1539,6 @@ "hub_tutorial_exit_flow": { "message": "Exit Tutorial" }, - "hub_tutorial_nav_previous": { - "message": "Previous" - }, - "hub_tutorial_nav_next": { - "message": "Next" - }, - "hub_tutorial_nav_done": { - "message": "Done" - }, - "hub_tutorial_simple_view": { - "message": "Simple View" - }, - "hub_tutorial_detailed_view": { - "message": "Detailed View" - }, "hub_tutorial_detailed_expanded_view": { "message": "Detailed Expanded View" }, @@ -1767,9 +1644,6 @@ "hub_rewards_header_description": { "message": "Our new Rewards feature offers a powerful, completely private new way to receive real discounts online. Turn it on and check it out!" }, - "hub_rewards_header_learn_more": { - "message": "Learn More" - }, "hub_rewards_experience_title": { "message": "A Smarter Shopping Experience" }, @@ -1800,12 +1674,6 @@ "hub_supporter_header_description": { "message": "Show your support and unlock special perks!" }, - "hub_supporter_button_text": { - "message": "Get Ghostery Plus" - }, - "hub_supporter_button_text_alt": { - "message": "Already Subscribed" - }, "hub_supporter_price": { "message": "$SUP_START$$$SUP_END$$SPAN_START$2$SPAN_END$ per month", "placeholders": { @@ -1823,15 +1691,9 @@ } } }, - "hub_supporter_perk_themes_title": { - "message": "New Themes" - }, "hub_supporter_perk_themes_description": { "message": "Customize the Ghostery colors - try our Midnight theme!" }, - "hub_supporter_perk_stats_title": { - "message": "Historical Blocking Stats" - }, "hub_supporter_perk_stats_description": { "message": "View your blocking statistics and see how Ghostery is working for you" }, @@ -1844,21 +1706,12 @@ "hub_supporter_manifesto": { "message": "We strive to deliver the best privacy protection services to our users free of cost. While we do not charge for our privacy suite, you may choose to support us through a small monthly subscription. Join us in our mission by upgrading to Ghostery Plus - and unlock cool perks along the way!" }, - "hub_supporter_feature_theme_title": { - "message": "New Themes" - }, "hub_supporter_feature_theme_description": { "message": "Customize the Ghostery colors for a new visual experience! Introduced through popular request. Check out our special Midnight theme, and more to come." }, - "hub_supporter_feature_stats_title": { - "message": "Historical Blocking Stats" - }, "hub_supporter_feature_stats_description": { "message": "View historical blocking results with graphs and statistics to reveal trends and learn how Ghostery has affected your browsing." }, - "hub_supporter_feature_support_title": { - "message": "Priority Support" - }, "hub_supporter_feature_support_description": { "message": "Need assistance? Find a broken page? Have your questions answered and issues resolved fast with our Priority help desk service, accessible only when signed in with a Plus account." }, @@ -1880,15 +1733,6 @@ "hub_login_header_title": { "message": "Sign in to your account." }, - "hub_login_label_email": { - "message": "Email:" - }, - "hub_login_label_password": { - "message": "Password:" - }, - "hub_login_label_email_invalid": { - "message": "Please enter a valid email." - }, "hub_login_label_password_invalid": { "message": "Use between 8 and 50 characters." }, @@ -1898,9 +1742,6 @@ "hub_login_link_create_account": { "message": "Sign up." }, - "hub_login_button_submit": { - "message": "Sign In" - }, "hub_login_toast_success": { "message": "Sign in successful! Your account settings have been imported." }, @@ -1910,27 +1751,12 @@ "hub_create_account_header_title": { "message": "Would you like to create a Ghostery account to sync settings across browsers and devices?" }, - "hub_create_account_label_email": { - "message": "Email:" - }, - "hub_create_account_label_email_confirm": { - "message": "Confirm Email:" - }, "hub_create_account_label_first_name": { "message": "First Name (Optional):" }, "hub_create_account_label_email_last_name": { "message": "Last Name (Optional):" }, - "hub_create_account_label_password": { - "message": "Password:" - }, - "hub_create_account_label_email_invalid": { - "message": "Please enter a valid email." - }, - "hub_create_account_label_email_confirm_invalid": { - "message": "Your emails do not match." - }, "hub_create_account_label_password_invalid": { "message": "Only these special characters are allowed: !@#$%^&*=+()<>{}[];:,./?" }, @@ -1946,9 +1772,6 @@ "hub_create_account_link_login": { "message": "Sign in here." }, - "hub_create_account_button_submit": { - "message": "Create Account" - }, "hub_create_account_toast_success": { "message": "Account Successfully Created" }, @@ -2080,9 +1903,6 @@ "rewards_code_copied_toast_notification": { "message": "Rewards code copied!" }, - "rewards_learn_more": { - "message": "Learn More" - }, "offers_hub_copy_btn": { "message": "Copy code" }, @@ -2101,21 +1921,9 @@ "subscription_themes": { "message": "Ghostery Themes" }, - "subscription_info_title": { - "message": "Ghostery Plus" - }, "subscription_themes_title": { "message": "Themes" }, - "subscription_priority_support_title": { - "message": "Priority Support" - }, - "subscription_priority_support": { - "message": "Priority Support" - }, - "subscription_history_stats": { - "message": "Historical Stats" - }, "subscribe_pitch": { "message": "While Ghostery is free, you can choose to support us through a small subscription of $2 per month in exchange for cool perks, such as color themes, priority help service, and more. Join our mission and subscribe!" }, @@ -2152,9 +1960,6 @@ "subscription_themes_tooltip": { "message": "Change the color scheme of Ghostery!" }, - "subscription_tracker_stats": { - "message": "Historical Stats" - }, "subscription_charge_date" : { "message": "Next Payment Date" }, @@ -2264,9 +2069,6 @@ "panel_stats_pitch_modal_sign_in": { "message": "Sign in here" }, - "panel_stats_pitch_modal_subscribe": { - "message": "Get Ghostery Plus!" - }, "panel_stats_pitch_modal_tooltip": { "message": "Erase all statistics history up until this point in time." }, @@ -2288,12 +2090,6 @@ "ads": { "message": "ads" }, - "cliqz_feature_status_on": { - "message": "On" - }, - "cliqz_feature_status_off": { - "message": "Off" - }, "create_account_form_legal_consent_checkbox_label": { "message": "I accept the $LINK_TERMS_START$Terms and Conditions$LINK_END$, the $LINK_LICENSE_START$Public License Agreement$LINK_END$, and consent to data practices found in the $LINK_PRIVACY_START$Privacy Policy$LINK_END$.", "placeholders": { @@ -2322,5 +2118,123 @@ }, "unknown_description": { "message": "Unknown trackers scrubbed by Anti-Tracking" + }, + "thanks_for_downloading_ghostery": { + "message": "Thanks for downloading Ghostery!" + }, + "choose_your_privacy_plan": { + "message": "Choose your privacy plan" + }, + "ghostery_basic": { + "message": "Ghostery Basic" + }, + "ghostery_plus": { + "message": "Ghostery Plus" + }, + "locale_appropriate_currency_icon": { + "message": "$$", + "description": "Use '$$' to mean '$'. '$' has a special meaning in the code and the first '$' tells the browser to ignore the special meaning of the second '$' and print it literally." + }, + "plus_monthly_subscription_price_number": { + "message": "2" + }, + "per_month": { + "message": "per month" + }, + "protection_for_this_browser": { + "message": "Protection for this browser" + }, + "blocks_ads": { + "message": "Blocks Ads" + }, + "blocks_trackers": { + "message": "Blocks Trackers" + }, + "fast_browsing": { + "message": "Fast Browsing" + }, + "select_basic": { + "message": "Select Basic" + }, + "recommended": { + "message": "Recommended" + }, + "get_ghostery_plus": { + "message": "Get Ghostery Plus" + }, + "get_ghostery_plus_bang": { + "message": "Get Ghostery Plus!" + }, + "already_subscribed": { + "message": "Already Subscribed" + }, + "new_themes": { + "message": "New Themes" + }, + "historical_blocking_stats": { + "message": "Historical Blocking Stats" + }, + "sign_in": { + "message": "Sign In" + }, + "sign_out": { + "message": "Sign Out" + }, + "create_account": { + "message": "Create Account" + }, + "email_colon": { + "message": "Email:" + }, + "confirm_email_colon": { + "message": "Confirm Email:" + }, + "next": { + "message": "Next" + }, + "previous": { + "message": "Previous" + }, + "done": { + "message": "Done" + }, + "simple_view": { + "message": "Simple View" + }, + "detailed_view": { + "message": "Detailed View" + }, + "support": { + "message": "Support" + }, + "start": { + "message": "Start" + }, + "password_colon": { + "message": "Password:" + }, + "please_enter_a_valid_email": { + "message": "Please enter a valid email." + }, + "your_email_do_not_match": { + "message": "Your emails do not match." + }, + "customize_setup": { + "message": "Customize Setup" + }, + "learn_more": { + "message": "Learn More" + }, + "historical_stats": { + "message": "Historical Stats" + }, + "priority_support": { + "message": "Priority Support" + }, + "on": { + "message": "On" + }, + "off": { + "message": "Off" } } diff --git a/app/content-scripts/rewards/OfferCard.jsx b/app/content-scripts/rewards/OfferCard.jsx index 9e26138b5..c97e906e9 100644 --- a/app/content-scripts/rewards/OfferCard.jsx +++ b/app/content-scripts/rewards/OfferCard.jsx @@ -74,7 +74,7 @@ class OfferCard extends Component { message: t('rewards_first_prompt_extended'), textLink: { href: 'https://www.ghostery.com/faqs/what-is-ghostery-rewards/', - text: t('rewards_learn_more'), + text: t('learn_more'), callback: () => { this.props.actions.sendSignal('offer_first_learn'); sendMessage('ping', 'rewards_first_learn_more'); diff --git a/app/hub/Views/CreateAccountView/CreateAccountView.jsx b/app/hub/Views/CreateAccountView/CreateAccountView.jsx index 432463ee7..9bd342680 100644 --- a/app/hub/Views/CreateAccountView/CreateAccountView.jsx +++ b/app/hub/Views/CreateAccountView/CreateAccountView.jsx @@ -73,7 +73,7 @@ const CreateAccountView = (props) => {
{ /> {emailError && (
- {t('hub_create_account_label_email_invalid')} + {t('please_enter_a_valid_email')}
)}
{ /> {confirmEmailError && (
- {t('hub_create_account_label_email_confirm_invalid')} + {t('your_emails_do_not_match')}
)}
@@ -147,7 +147,7 @@ const CreateAccountView = (props) => {
{
diff --git a/app/hub/Views/HomeView/HomeView.jsx b/app/hub/Views/HomeView/HomeView.jsx index b22261ce3..e19d54d45 100644 --- a/app/hub/Views/HomeView/HomeView.jsx +++ b/app/hub/Views/HomeView/HomeView.jsx @@ -106,7 +106,7 @@ const HomeView = (props) => { ) : ( - {t('hub_home_subheader_create_account')} + {t('create_account')} )}
@@ -120,7 +120,7 @@ const HomeView = (props) => { {t('hub_home_feature_tutorial_text')}
- {tutorial_complete ? t('hub_home_feature_tutorial_button_alt') : t('hub_home_feature_tutorial_button')} + {tutorial_complete ? t('hub_home_feature_tutorial_button_alt') : t('start')}
@@ -128,7 +128,7 @@ const HomeView = (props) => {
- {t('hub_home_feature_setup_title')} + {t('customize_setup')}
{t('hub_home_feature_setup_text')} @@ -146,7 +146,7 @@ const HomeView = (props) => {
- {isPlus ? t('hub_home_feature_supporter_button_alt') : t('hub_home_feature_supporter_button')} + {isPlus ? t('Aready_Subscribed') : t('get_ghostery_plus')}
diff --git a/app/hub/Views/HomeView/HomeView.scss b/app/hub/Views/HomeView/HomeView.scss index d587e2629..e8ac4227b 100644 --- a/app/hub/Views/HomeView/HomeView.scss +++ b/app/hub/Views/HomeView/HomeView.scss @@ -271,12 +271,21 @@ text-align: center; color: #333333; - &.bold { - font-weight: bold; - color: #4a4a4a; + &.italic { + font-style: italic; } } +.PlusPromoModal__check-icon { + padding-right: 5px; + margin-top: -18px; +} + +.PlusPromoModal__plus-option-description-item-container { + display: flex; + justify-content: center; +} + .PlusPromoModal__button { margin: 0; font-size: 11px; diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index 51743392a..54c63194a 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -72,50 +72,59 @@ class HomeViewContainer extends Component {
- Thanks for downloading Ghostery! + {t('thanks_for_downloading_ghostery')}
- Choose your privacy plan + {t('choose_your_privacy_plan')}
-
Ghostery Basic
+
{t('ghostery_basic')}
- $ + {t('locale_appropriate_currency_icon')} 0 - per month + {t('per_month')}
-

Protection for this browser

-

Blocks Ads

-

Blocks Trackers

-

Fast Browsing

+

{t('protection_for_this_browser')}

+

{t('blocks_ads')}

+

{t('blocks_trackers')}

+

{t('fast_browsing')}

- Select Basic + {t('select_basic')}
-
Recommended
+
{t('recommended')}
Ghostery Plus
- $ - 2 + {t('locale_appropriate_currency_icon')} + {t('plus_monthly_subscription_price_number')} per month
-

All basic features, plus:

-

Historical Tracker Stats

-

Priority Support

-

New Color Themes

+

All basic features, plus:

+
+ +

Historical Tracker Stats

+
+
+ +

Priority Support

+
+
+ +

New Color Themes

+
diff --git a/app/hub/Views/LogInView/LogInView.jsx b/app/hub/Views/LogInView/LogInView.jsx index 2147985b6..6e76b3462 100644 --- a/app/hub/Views/LogInView/LogInView.jsx +++ b/app/hub/Views/LogInView/LogInView.jsx @@ -56,7 +56,7 @@ const LogInView = (props) => {
{ /> {emailError && (
- {t('hub_login_label_email_invalid')} + {t('please_enter_a_valid_email')}
)} {
diff --git a/app/hub/Views/PlusView/PlusView.jsx b/app/hub/Views/PlusView/PlusView.jsx index 9f6d0d89c..6457f84fe 100644 --- a/app/hub/Views/PlusView/PlusView.jsx +++ b/app/hub/Views/PlusView/PlusView.jsx @@ -36,11 +36,11 @@ class PlusView extends Component { return isPlus ? (
- {t('hub_supporter_button_text_alt')} + {t('already_subscribed')}
) : (
- {t('hub_supporter_button_text')} + {t('get_ghostery_plus')} ); } @@ -74,7 +74,7 @@ class PlusView extends Component {
- {t('hub_supporter_perk_themes_title')} + {t('new_themes')}
{t('hub_supporter_perk_themes_description')} @@ -83,7 +83,7 @@ class PlusView extends Component {
- {t('hub_supporter_perk_stats_title')} + {t('historical_blocking_stats')}
{t('hub_supporter_perk_stats_description')} @@ -110,7 +110,7 @@ class PlusView extends Component {
- {t('hub_supporter_feature_theme_title')} + {t('new_themes')}
{t('hub_supporter_feature_theme_description')} @@ -121,7 +121,7 @@ class PlusView extends Component { {t('hub_supporter_feature_theme_title')}
@@ -139,12 +139,12 @@ class PlusView extends Component { {t('hub_supporter_feature_stats_title')}
- {t('hub_supporter_feature_stats_title')} + {t('historical_blocking_stats')}
{t('hub_supporter_feature_stats_description')} @@ -155,7 +155,7 @@ class PlusView extends Component { {t('hub_supporter_feature_stats_title')}
@@ -171,7 +171,7 @@ class PlusView extends Component {
- {t('hub_supporter_feature_support_title')} + {t('priority_support')}
{t('hub_supporter_feature_support_description')} @@ -182,7 +182,7 @@ class PlusView extends Component { {t('hub_supporter_feature_support_title')}
diff --git a/app/hub/Views/RewardsView/RewardsView.jsx b/app/hub/Views/RewardsView/RewardsView.jsx index 345922a29..7dbe50a43 100644 --- a/app/hub/Views/RewardsView/RewardsView.jsx +++ b/app/hub/Views/RewardsView/RewardsView.jsx @@ -46,7 +46,7 @@ class RewardsView extends Component {
diff --git a/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx b/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx index 08302f49b..b497bb5b3 100644 --- a/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx +++ b/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx @@ -36,8 +36,8 @@ class SetupAntiSuiteViewContainer extends Component { hrefPrev: `/setup/${index - 1}`, hrefNext: `/setup/${index + 1}`, hrefDone: '/', - textPrev: t('hub_setup_nav_previous'), - textNext: t('hub_setup_nav_next'), + textPrev: t('previous'), + textNext: t('next'), textDone: t('hub_setup_exit_flow'), }); diff --git a/app/hub/Views/SetupViews/SetupBlockingDropdown/SetupBlockingDropdown.jsx b/app/hub/Views/SetupViews/SetupBlockingDropdown/SetupBlockingDropdown.jsx index f876ad30b..1944171e1 100644 --- a/app/hub/Views/SetupViews/SetupBlockingDropdown/SetupBlockingDropdown.jsx +++ b/app/hub/Views/SetupViews/SetupBlockingDropdown/SetupBlockingDropdown.jsx @@ -80,7 +80,7 @@ class SetupBlockingDropdown extends Component {
- {t('hub_setup_nav_done')} + {t('done')}
diff --git a/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewContainer.jsx b/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewContainer.jsx index fdc2e5124..e1b7ac71c 100644 --- a/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewContainer.jsx +++ b/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewContainer.jsx @@ -40,7 +40,7 @@ class SetupBlockingViewContainer extends Component { hrefNext: `/setup/${index + 1}`, hrefDone: '/', textPrev: false, - textNext: t('hub_setup_nav_next'), + textNext: t('next'), textDone: t('hub_setup_exit_flow'), }); diff --git a/app/hub/Views/SetupViews/SetupDoneView/SetupDoneViewContainer.jsx b/app/hub/Views/SetupViews/SetupDoneView/SetupDoneViewContainer.jsx index 1416b0388..3ee04fcb9 100644 --- a/app/hub/Views/SetupViews/SetupDoneView/SetupDoneViewContainer.jsx +++ b/app/hub/Views/SetupViews/SetupDoneView/SetupDoneViewContainer.jsx @@ -33,8 +33,8 @@ class SetupDoneViewContainer extends Component { hrefPrev: `/setup/${index - 1}`, hrefNext: '/', hrefDone: '/', - textPrev: t('hub_setup_nav_previous'), - textNext: t('hub_setup_nav_done'), + textPrev: t('previous'), + textNext: t('done'), textDone: t('hub_setup_exit_flow'), }); @@ -56,14 +56,14 @@ class SetupDoneViewContainer extends Component { id: 'tutorial', title: t('hub_setup_feature_tutorial_title'), description: t('hub_setup_feature_tutorial_description'), - buttonText: t('hub_setup_feature_tutorial_button'), + buttonText: t('start'), buttonHref: '/tutorial/1', }, { id: 'plus', - title: t('hub_setup_feature_supporter_title'), + title: t('get_ghostery_plus'), description: t('hub_setup_feature_supporter_description'), - buttonText: t('hub_setup_feature_supporter_button'), + buttonText: t('support'), buttonHref: '/plus', }, { diff --git a/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx b/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx index c7dc7ee8f..3e6415082 100644 --- a/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx +++ b/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx @@ -33,8 +33,8 @@ class SetupHumanWebViewContainer extends Component { hrefPrev: `/setup/${index - 1}`, hrefNext: `/setup/${index + 1}`, hrefDone: '/', - textPrev: t('hub_setup_nav_previous'), - textNext: t('hub_setup_nav_next'), + textPrev: t('previous'), + textNext: t('next'), textDone: t('hub_setup_exit_flow'), }); diff --git a/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx b/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx index f3526640f..7bd78abcf 100644 --- a/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx +++ b/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx @@ -49,21 +49,21 @@ class SideNavigationViewContainer extends Component { const menuItems = [ { href: '/', icon: 'home', text: t('hub_side_navigation_home') }, - { href: '/setup', icon: 'setup', text: t('hub_side_navigation_setup') }, + { href: '/setup', icon: 'setup', text: t('customize_setup') }, { href: '/tutorial', icon: 'tutorial', text: t('hub_side_navigation_tutorial') }, - { href: '/plus', icon: 'plus', text: t('hub_side_navigation_supporter') }, + { href: '/plus', icon: 'plus', text: t('get_ghostery_plus') }, ...(IS_CLIQZ ? [] : [{ href: '/rewards', icon: 'rewards', text: t('hub_side_navigation_rewards') }]), { href: '/products', icon: 'products', text: t('hub_side_navigation_products') } ]; const bottomItems = user ? [ { id: 'email', href: `https://account.${globals.GHOSTERY_DOMAIN}.com/`, text: user.email }, - { id: 'logout', text: t('hub_side_navigation_log_out'), clickHandler: this._handleLogoutClick }, + { id: 'logout', text: t('sign_out'), clickHandler: this._handleLogoutClick }, ] : [ - { id: 'create-account', href: '/create-account', text: t('hub_side_navigation_create_account') }, + { id: 'create-account', href: '/create-account', text: t('create_account') }, { id: 'log-id', href: '/log-in', - text: t('hub_side_navigation_log_in'), + text: t('sign_in'), icon: 'profile', }, ]; diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx index ed5776b48..2d442232a 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx @@ -22,20 +22,20 @@ const TutorialAntiSuiteView = () => (
- {t('hub_tutorial_simple_view')} + {t('simple_view')}
{t('hub_tutorial_simple_view')}
- {t('hub_tutorial_detailed_view')} + {t('detailed_view')}
{t('hub_tutorial_detailed_view')}
diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx index bd8a4af68..a08f8708c 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx @@ -33,8 +33,8 @@ class TutorialAntiSuiteViewContainer extends Component { hrefPrev: `/tutorial/${index - 1}`, hrefNext: '/', hrefDone: '/', - textPrev: t('hub_tutorial_nav_previous'), - textNext: t('hub_tutorial_nav_done'), + textPrev: t('previous'), + textNext: t('done'), textDone: t('hub_tutorial_exit_flow'), }); diff --git a/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingView.jsx b/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingView.jsx index eb3d9e4a7..ca0199702 100644 --- a/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingView.jsx +++ b/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingView.jsx @@ -22,12 +22,12 @@ const TutorialBlockingView = () => (
- {t('hub_tutorial_detailed_view')} + {t('detailed_view')}
{t('hub_tutorial_detailed_view')}
{t('hub_tutorial_detailed_expanded_view')} diff --git a/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx index d85fc5dfd..246ac22d1 100644 --- a/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx @@ -33,8 +33,8 @@ class TutorialBlockingViewContainer extends Component { hrefPrev: `/tutorial/${index - 1}`, hrefNext: `/tutorial/${index + 1}`, hrefDone: '/', - textPrev: t('hub_tutorial_nav_previous'), - textNext: t('hub_tutorial_nav_next'), + textPrev: t('previous'), + textNext: t('next'), textDone: t('hub_tutorial_exit_flow'), }); } diff --git a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx index f869c252e..56a7bac57 100644 --- a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx +++ b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx @@ -24,12 +24,12 @@ const TutorialLayoutView = () => ( {t('hub_tutorial_simple_view')} {t('hub_tutorial_detailed_view')}
diff --git a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx index e8e276dea..407bf8fa9 100644 --- a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx @@ -33,8 +33,8 @@ class TutorialLayoutViewContainer extends Component { hrefPrev: `/tutorial/${index - 1}`, hrefNext: `/tutorial/${index + 1}`, hrefDone: '/', - textPrev: t('hub_tutorial_nav_previous'), - textNext: t('hub_tutorial_nav_next'), + textPrev: t('previous'), + textNext: t('next'), textDone: t('hub_tutorial_exit_flow'), }); } diff --git a/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx index f09f35815..b59c40b1d 100644 --- a/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx @@ -33,8 +33,8 @@ class TutorialTrackerListViewContainer extends Component { hrefPrev: `/tutorial/${index - 1}`, hrefNext: `/tutorial/${index + 1}`, hrefDone: '/', - textPrev: t('hub_tutorial_nav_previous'), - textNext: t('hub_tutorial_nav_next'), + textPrev: t('previous'), + textNext: t('next'), textDone: t('hub_tutorial_exit_flow'), }); } diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx index 31c66ff5a..605aaa9cb 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx @@ -22,20 +22,20 @@ const TutorialTrustView = () => (
- {t('hub_tutorial_simple_view')} + {t('simple_view')}
{t('hub_tutorial_simple_view')}
- {t('hub_tutorial_detailed_view')} + {t('detailed_view')}
{t('hub_tutorial_detailed_view')}
diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx index 373233096..f48598ad0 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx @@ -33,8 +33,8 @@ class TutorialTrustViewContainer extends Component { hrefPrev: `/tutorial/${index - 1}`, hrefNext: `/tutorial/${index + 1}`, hrefDone: '/', - textPrev: t('hub_tutorial_nav_previous'), - textNext: t('hub_tutorial_nav_next'), + textPrev: t('previous'), + textNext: t('next'), textDone: t('hub_tutorial_exit_flow'), }); } diff --git a/app/hub/Views/TutorialViews/TutorialVideoView/TutorialVideoViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialVideoView/TutorialVideoViewContainer.jsx index 65b4a5ee2..a6af3c5ce 100644 --- a/app/hub/Views/TutorialViews/TutorialVideoView/TutorialVideoViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialVideoView/TutorialVideoViewContainer.jsx @@ -34,7 +34,7 @@ class TutorialVideoViewContainer extends Component { hrefNext: `/tutorial/${index + 1}`, hrefDone: '/', textPrev: false, - textNext: t('hub_tutorial_nav_next'), + textNext: t('next'), textDone: t('hub_tutorial_exit_flow'), }); } diff --git a/app/images/hub/home/check-icon.svg b/app/images/hub/home/check-icon.svg new file mode 100644 index 000000000..7e2e16aad --- /dev/null +++ b/app/images/hub/home/check-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/panel/components/BuildingBlocks/CliqzFeature.jsx b/app/panel/components/BuildingBlocks/CliqzFeature.jsx index 72cfe1372..093ebf0b1 100644 --- a/app/panel/components/BuildingBlocks/CliqzFeature.jsx +++ b/app/panel/components/BuildingBlocks/CliqzFeature.jsx @@ -51,7 +51,7 @@ class CliqzFeature extends React.Component { } _getStatus(active) { - return active ? t('cliqz_feature_status_on') : t('cliqz_feature_status_off'); + return active ? t('on') : t('off'); } _getTooltipBodyText(active, isTooltipBody, type) { diff --git a/app/panel/components/CreateAccount.jsx b/app/panel/components/CreateAccount.jsx index db5fe2f4a..8c615c5f5 100644 --- a/app/panel/components/CreateAccount.jsx +++ b/app/panel/components/CreateAccount.jsx @@ -152,21 +152,21 @@ class CreateAccount extends React.Component {

- { t('invalid_email_create') } + { t('please_enter_a_valid_email') }

-

{ t('invalid_email_confirmation') }

+

{ t('your_emails_do_not_match') }

@@ -194,7 +194,7 @@ class CreateAccount extends React.Component {
@@ -240,7 +240,7 @@ class CreateAccount extends React.Component {
diff --git a/app/panel/components/DetailMenu.jsx b/app/panel/components/DetailMenu.jsx index ecec07c98..9443029d5 100644 --- a/app/panel/components/DetailMenu.jsx +++ b/app/panel/components/DetailMenu.jsx @@ -74,7 +74,7 @@ class DetailMenu extends React.Component { - { t('panel_detail_menu_stats_title') } + { t('historical_stats') }
diff --git a/app/panel/components/ForgotPassword.jsx b/app/panel/components/ForgotPassword.jsx index f187ff21d..b1d0f7bbe 100644 --- a/app/panel/components/ForgotPassword.jsx +++ b/app/panel/components/ForgotPassword.jsx @@ -83,7 +83,7 @@ class ForgotPassword extends React.Component {
diff --git a/app/panel/components/Header.jsx b/app/panel/components/Header.jsx index 8768b72c2..a8be9d5ff 100644 --- a/app/panel/components/Header.jsx +++ b/app/panel/components/Header.jsx @@ -101,7 +101,7 @@ class Header extends React.Component { let text = ''; let handleOnClick = null; if (!loggedIn) { - text = t('panel_header_sign_in'); + text = t('sign_in'); handleOnClick = this.handleSignin; } else if (loggedIn && user && !user.emailValidated) { text = t('panel_header_verify_account'); @@ -186,7 +186,7 @@ class Header extends React.Component { const simpleTab = (
- {t('panel_header_simple_view')} + {t('simple_view')}
); @@ -194,7 +194,7 @@ class Header extends React.Component { const detailedTab = (
- {t('panel_header_detailed_view')} + {t('detailed_view')}
); diff --git a/app/panel/components/HeaderMenu.jsx b/app/panel/components/HeaderMenu.jsx index 5b0dcc2d7..bc03910c0 100644 --- a/app/panel/components/HeaderMenu.jsx +++ b/app/panel/components/HeaderMenu.jsx @@ -236,7 +236,7 @@ class HeaderMenu extends React.Component { - { t('panel_menu_ghostery_subscriber') } + { t('ghostery_plus') }
@@ -251,10 +251,10 @@ class HeaderMenu extends React.Component { { email }
- { t('panel_menu_signin') } + { t('sign_in') }
- { t('panel_menu_signout') } + { t('sign_out') }
diff --git a/app/panel/components/Help.jsx b/app/panel/components/Help.jsx index 057a47736..c436a5b06 100644 --- a/app/panel/components/Help.jsx +++ b/app/panel/components/Help.jsx @@ -33,7 +33,7 @@ const Help = () => {

{ t('panel_help_questions_header') }

{t('panel_help_faq')} {t('panel_help_feedback')} - { t('panel_help_support') } + { t('support') }

{ t('panel_help_contact_header') }

diff --git a/app/panel/components/Login.jsx b/app/panel/components/Login.jsx index 7dedeb0de..428fa5a7b 100644 --- a/app/panel/components/Login.jsx +++ b/app/panel/components/Login.jsx @@ -103,7 +103,7 @@ class Login extends React.Component {
@@ -125,7 +125,7 @@ class Login extends React.Component {
@@ -140,7 +140,7 @@ class Login extends React.Component { { t('panel_forgot_password') }
- { t('panel_create_account') } + { t('create_account') }
diff --git a/app/panel/components/Rewards.jsx b/app/panel/components/Rewards.jsx index fee5e4fc1..8ba483f27 100644 --- a/app/panel/components/Rewards.jsx +++ b/app/panel/components/Rewards.jsx @@ -224,7 +224,7 @@ class Rewards extends React.Component {
{ t('panel_detail_rewards_cliqz_text') }

- { t('panel_detail_learn_more') } + { t('learn_more') }
); diff --git a/app/panel/components/StatsView.jsx b/app/panel/components/StatsView.jsx index 312d8fa20..e850cac14 100644 --- a/app/panel/components/StatsView.jsx +++ b/app/panel/components/StatsView.jsx @@ -196,7 +196,7 @@ const StatsView = (props) => {
-
{t('panel_stats_pitch_modal_subscribe')}
+
{t('Get_Ghostery_Plus_bang')}
{ !loggedIn && (
diff --git a/app/panel/components/Subscribe.jsx b/app/panel/components/Subscribe.jsx index dab8ecd5f..e21fdf240 100644 --- a/app/panel/components/Subscribe.jsx +++ b/app/panel/components/Subscribe.jsx @@ -40,7 +40,7 @@ const Subscribe = (props) => { {t('subscribe_pitch_learn_more')}
- {t('subscribe_pitch_button_label')} + {t('Get_Ghostery_Plus_bang')}
{(loggedIn === 'false') && ( diff --git a/app/panel/components/Subscription/PrioritySupport.jsx b/app/panel/components/Subscription/PrioritySupport.jsx index 57b63ec52..94ddd15aa 100644 --- a/app/panel/components/Subscription/PrioritySupport.jsx +++ b/app/panel/components/Subscription/PrioritySupport.jsx @@ -24,7 +24,7 @@ const PrioritySupport = () => (
-

{ t('subscription_priority_support_title') }

+

{ t('priority_support') }

{ t('subscription_support') }
diff --git a/app/panel/components/Subscription/SubscriptionInfo.jsx b/app/panel/components/Subscription/SubscriptionInfo.jsx index 3090992c8..93c1161a4 100644 --- a/app/panel/components/Subscription/SubscriptionInfo.jsx +++ b/app/panel/components/Subscription/SubscriptionInfo.jsx @@ -45,7 +45,7 @@ const SubscriptionInfo = (props) => {
-

{ t('subscription_info_title') }

+

{ t('ghostery_plus') }

{loading ? (
) : ( @@ -75,8 +75,8 @@ const SubscriptionInfo = (props) => {
  • {t('subscription_midnight_theme')}
  • -
  • {t('subscription_tracker_stats')}
  • -
  • {t('subscription_priority_support')}
  • +
  • {t('historical_stats')}
  • +
  • {t('priority_support')}
diff --git a/app/panel/components/Subscription/SubscriptionMenu.jsx b/app/panel/components/Subscription/SubscriptionMenu.jsx index b718e67be..279ac3322 100644 --- a/app/panel/components/Subscription/SubscriptionMenu.jsx +++ b/app/panel/components/Subscription/SubscriptionMenu.jsx @@ -33,13 +33,13 @@ const SubscriptionMenu = () => (
  • - { t('subscription_priority_support') } + { t('priority_support') }
  • - { t('subscription_history_stats') } + { t('historical_stats') }
  • diff --git a/app/panel/components/Summary.jsx b/app/panel/components/Summary.jsx index e95085c4f..d54ad73fb 100644 --- a/app/panel/components/Summary.jsx +++ b/app/panel/components/Summary.jsx @@ -728,7 +728,7 @@ class Summary extends React.Component { return (
    - +
    ); } diff --git a/app/panel/reducers/panel.js b/app/panel/reducers/panel.js index 194f16c73..20b873259 100644 --- a/app/panel/reducers/panel.js +++ b/app/panel/reducers/panel.js @@ -139,7 +139,7 @@ export default (state = initialState, action) => { errorText = t('email_address_in_use'); break; case '10080': - errorText = t('invalid_email_confirmation'); + errorText = t('your_emails_do_not_match'); break; default: errorText = t('server_error_message'); diff --git a/tools/i18n-checker.js b/tools/i18n-checker.js index dbf91e0af..0f5d96f36 100644 --- a/tools/i18n-checker.js +++ b/tools/i18n-checker.js @@ -24,7 +24,7 @@ const oboe = require('oboe'); const LOCALES_FOLDER = './_locales'; const GATHER_FILE_PATHS_EXCEPTIONS = ['.DS_Store']; const LANG_FILES_COUNT = 14; -const DEFAULT_LOCALE_PATH = '../_locales/en/messages.json'; +const DEFAULT_LOCALE_PATH = './_locales/en/messages.json'; const DUPLICATE_TOKENS_FILE = './tools/i18n_results/duplicate_tokens.txt'; const MISSING_TOKENS_FILE = './tools/i18n_results/missing_tokens.txt'; const EXTRA_TOKENS_FILE = './tools/i18n_results/extra_tokens.txt'; @@ -175,7 +175,7 @@ function findMissingKeys(paths) { let hasMissingKeys = false; const missingKeys = {}; paths.forEach((path) => { - const localeJson = jsonfile.readFileSync(`.${path}`); + const localeJson = jsonfile.readFileSync(`${path}`); const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; missingKeys[locale] = []; Object.keys(defaultLocaleJson).forEach((key) => { @@ -210,7 +210,7 @@ function findExtraKeys(paths) { let hasExtraKeys = false; const extraKeys = {}; paths.forEach((path) => { - const localeJson = jsonfile.readFileSync(`.${path}`); + const localeJson = jsonfile.readFileSync(`${path}`); const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; extraKeys[locale] = []; Object.keys(localeJson).forEach((key) => { @@ -243,7 +243,7 @@ function findMalformedKeys(paths) { let hasMalformedKeys = false; const malformedKeys = {}; paths.forEach((path) => { - const localeJson = jsonfile.readFileSync(`.${path}`); + const localeJson = jsonfile.readFileSync(`${path}`); const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; malformedKeys[locale] = []; Object.keys(localeJson).forEach((key) => { @@ -278,7 +278,7 @@ function findMissingPlaceholders(paths) { let hasMissingPlaceholders = false; const missingPlaceholders = {}; paths.forEach((path) => { - const localeJson = jsonfile.readFileSync(`.${path}`); + const localeJson = jsonfile.readFileSync(`${path}`); const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; missingPlaceholders[locale] = []; Object.keys(defaultLocaleJson).forEach((key) => { @@ -322,7 +322,7 @@ function findExtraPlaceholders(paths) { let hasExtraPlaceholders = false; const extraPlaceholders = {}; paths.forEach((path) => { - const localeJson = jsonfile.readFileSync(`.${path}`); + const localeJson = jsonfile.readFileSync(`${path}`); const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; extraPlaceholders[locale] = []; Object.keys(localeJson).forEach((key) => { @@ -364,7 +364,7 @@ function findMalformedPlaceholders(paths) { let hasMalformedPlaceholders = false; const malformedPlaceholders = []; paths.forEach((path) => { - const localeJson = jsonfile.readFileSync(`.${path}`); + const localeJson = jsonfile.readFileSync(`${path}`); const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; malformedPlaceholders[locale] = []; Object.keys(localeJson).forEach((key) => { From 475143c516951186b6f05012acab2979e9c420c7 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Wed, 2 Oct 2019 16:02:30 -0400 Subject: [PATCH 16/39] Additional consolidation of redundant and unused entries in messages --- _locales/en/messages.json | 159 +++--------------- .../Views/LogInView/LogInViewContainer.jsx | 2 +- .../SetupAntiSuiteViewContainer.jsx | 10 +- .../TutorialAntiSuiteView.jsx | 10 +- .../BuildingBlocks/CliqzFeature.jsx | 48 +++++- app/panel/reducers/panel.js | 2 +- 6 files changed, 76 insertions(+), 155 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2b5368b39..e5ab96c38 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -22,16 +22,9 @@ } } }, - "all": { - "description": "Default setting in the 'filter by type' dropdown for the tag browser. Doesn't filter anything out. Should read as 'Show all'.", - "message": "all" - }, "banner_check_your_email_title": { "message": "Please check your email shortly for a link to reset your password." }, - "banner_no_such_account_message": { - "message": "Looks like we can't find this email and password combo. Give it another shot." - }, "banner_email_not_in_system_message": { "message": "Sorry, that email isn't in our system." }, @@ -41,10 +34,6 @@ "blacklist_error_whitelist_url": { "message": "This site has been removed from your Trusted Sites list and added to your Restricted Sites list" }, - "blocked": { - "description": "Select this to show only blocked (checked) tags in the tag browser. Should read as 'Show blocked'.", - "message": "blocked" - }, "blocked_redirect_action_always_title": { "description": "Title for the 'Allow always and reload' button on the blocked redirect page.", "message": "Always allow and reload." @@ -375,25 +364,6 @@ } } }, - "copyright": { - "description": "Copyright message, shown in page footers.", - "message": "$C$ $YEAR$ $GHOSTERY$, a part of $COMPANY_ADDRESS$", - "placeholders": { - "c": { - "content": "©" - }, - "company_address": { - "content": "$1", - "example": "GHOSTERY, Inc., Such-and-such Street, Such-and-such Floor, New York, NY, Zip Code" - }, - "ghostery": { - "content": "Ghostery" - }, - "year": { - "content": "2015" - } - } - }, "error_email_forgot": { "message": "Hmm, I can’t find that email. Maybe try another?" }, @@ -415,15 +385,6 @@ "password_field_label_required": { "message": "Password required." }, - "name": { - "description": "The title of this extension (Ghostery).", - "message": "$EXTENSION_NAME$ – Privacy Ad Blocker", - "placeholders": { - "extension_name": { - "content": "Ghostery" - } - } - }, "none": { "description": "Used to say that this item does not belong to any of the groups, as in \"Groups: None\" (as opposed to \"Groups: Group A, Group B, Group C\").", "message": "None" @@ -1099,51 +1060,6 @@ "blocking_category_tracker_found": { "message": "Found" }, - "drawer_title_enable_anti_tracking": { - "message": "Enhanced Anti-Tracking" - }, - "drawer_tooltip_enable_anti_tracking": { - "message": "Anonymize unblocked and unknown trackers for greater browsing protection" - }, - "drawer_label_enable_anti_tracking": { - "message": "Trackers Anonymized" - }, - "drawer_desc_enable_anti_tracking": { - "message": "Private data points have been removed." - }, - "drawer_status_enable_anti_tracking": { - "message": "Anti-Tracking is" - }, - "drawer_title_enable_ad_block": { - "message": "Enhanced Ad Blocking" - }, - "drawer_tooltip_enable_ad_block": { - "message": "Block advertisements on websites you visit" - }, - "drawer_label_enable_ad_block": { - "message": "Ads Blocked" - }, - "drawer_desc_enable_ad_block": { - "message": "Ad spots have been blocked." - }, - "drawer_status_enable_ad_block": { - "message": "Ad Blocking is" - }, - "drawer_title_enable_smart_block": { - "message": "Smart Blocking" - }, - "drawer_tooltip_enable_smart_block": { - "message": "Automatically block and unblock trackers to optimize page performance" - }, - "drawer_label_enable_smart_block": { - "message": "Trackers Adjusted" - }, - "drawer_desc_enable_smart_block": { - "message": "Trackers have been temporarily blocked or unblocked." - }, - "drawer_status_enable_smart_block": { - "message": "Smart Blocking is" - }, "blocking_unblock_all": { "message": "Unblock All" }, @@ -1213,34 +1129,16 @@ "alert_site_restricted_off": { "message": "Site no longer blacklisted." }, - "tooltip_anti_track": { - "message": "Enhanced Anti-Tracking" - }, - "tooltip_anti_track_body": { - "message": "Anonymize personal data for greater protection." - }, - "tooltip_anti_track_body_on": { + "tooltip_anti_track_on": { "message": "Personal data points anonymized." }, "tooltip_body_in_cliqz": { "message": "Feature already active in Cliqz by default." }, - "tooltip_ad_block": { - "message": "Enhanced Ad Blocking" - }, - "tooltip_ad_block_body": { - "message": "Block advertisements." - }, - "tooltip_ad_block_body_on": { + "tooltip_ad_block_on": { "message": "Advertisements have been blocked." }, - "tooltip_smart_block": { - "message": "Smart Blocking" - }, - "tooltip_smart_block_body": { - "message": "Automatically block and unblock trackers to optimize page performance." - }, - "tooltip_smart_block_body_on": { + "tooltip_smart_block_on": { "message": "Tracker blocking adjusted to optimize page performance." }, "tooltip_trust": { @@ -1442,24 +1340,9 @@ "hub_setup_blocking_description_custom": { "message": "Choose which trackers to block" }, - "hub_setup_antisuite_name_antitracking": { - "message": "Enhanced Anti-Tracking" - }, "hub_setup_antisuite_description_antitracking": { "message": "Anonymize unblocked and unknown trackers for greater browsing protection." }, - "hub_setup_adblock_name_adblocking": { - "message": "Enhanced Ad Blocking" - }, - "hub_setup_adblock_description_adblocking": { - "message": "Block advertisements on websites you visit." - }, - "hub_setup_smartblocking_name_smartblocking": { - "message": "Smart Blocking" - }, - "hub_setup_smartblocking_description_smartblocking": { - "message": "Automatically block and unblock trackers to optimize page performance." - }, "hub_setup_feature_already_active": { "message": "This feature is already active in Cliqz by default. $LINK_LM_START$Learn More$LINK_LM_END$", "placeholders": { @@ -1590,24 +1473,9 @@ "hub_tutorial_antisuite_title": { "message": "Get extra protection and automated control with our enhanced privacy features:" }, - "hub_tutorial_antisuite_antitracking_title": { - "message": "Enhanced Anti-Tracking" - }, "hub_tutorial_antisuite_antitracking_description": { "message": "Remove data points that websites can use to identify you." }, - "hub_tutorial_antisuite_adblocking_title": { - "message": "Enhanced Ad Blocking" - }, - "hub_tutorial_antisuite_adblocking_description": { - "message": "Block advertisements on the pages you visit." - }, - "hub_tutorial_antisuite_smartblocking_title": { - "message": "Smart Blocking" - }, - "hub_tutorial_antisuite_smartblocking_description": { - "message": "Automatically block and unblock trackers to optimize page performance." - }, "hub_products_page_title" : { "message": "Ghostery Hub - Try Other Ghostery Products" }, @@ -1745,9 +1613,6 @@ "hub_login_toast_success": { "message": "Sign in successful! Your account settings have been imported." }, - "hub_login_toast_error": { - "message": "Looks like we can't find this email and password combo. Give it another shot." - }, "hub_create_account_header_title": { "message": "Would you like to create a Ghostery account to sync settings across browsers and devices?" }, @@ -2236,5 +2101,23 @@ }, "off": { "message": "Off" + }, + "enhanced_ad_blocking": { + "message": "Enhanced Ad Blocking" + }, + "smart_blocking": { + "message": "Smart Blocking" + }, + "smart_blocking_DESC": { + "message": "Automatically block and unblock trackers to optimize page performance." + }, + "ad_blocking_DESC": { + "message": "Block advertisements on the pages you visit." + }, + "anti_tracking_DESC": { + "message": "Anonymize personal data for greater protection." + }, + "no_such_email_password_combo": { + "message": "Looks like we can't find this email and password combo. Give it another shot." } } diff --git a/app/hub/Views/LogInView/LogInViewContainer.jsx b/app/hub/Views/LogInView/LogInViewContainer.jsx index b5d9c616a..fb9cd1289 100644 --- a/app/hub/Views/LogInView/LogInViewContainer.jsx +++ b/app/hub/Views/LogInView/LogInViewContainer.jsx @@ -109,7 +109,7 @@ class LogInViewContainer extends Component { this.props.history.push('/'); } else { this.props.actions.setToast({ - toastMessage: t('hub_login_toast_error'), + toastMessage: t('no_such_email_password_combo'), toastClass: 'alert' }); } diff --git a/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx b/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx index b497bb5b3..42c9f03fe 100644 --- a/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx +++ b/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx @@ -103,7 +103,7 @@ class SetupAntiSuiteViewContainer extends Component { const features = [ { id: 'anti-tracking', - name: t('hub_setup_antisuite_name_antitracking'), + name: t('enhanced_anti_tracking'), enabled: anti_tracking_enabled, locked: IS_CLIQZ, toggle: IS_CLIQZ ? @@ -113,20 +113,20 @@ class SetupAntiSuiteViewContainer extends Component { }, { id: 'ad-block', - name: t('hub_setup_adblock_name_adblocking'), + name: t('enhanced_ad_blocking'), enabled: ad_block_enabled, locked: IS_CLIQZ, toggle: IS_CLIQZ ? () => {} : () => this._handleToggle('ad-block'), - description: IS_CLIQZ ? t('hub_setup_feature_already_active') : t('hub_setup_adblock_description_adblocking'), + description: IS_CLIQZ ? t('hub_setup_feature_already_active') : t('ad_blocking_DESC'), }, { id: 'smart-blocking', - name: t('hub_setup_smartblocking_name_smartblocking'), + name: t('smart_blocking'), enabled: enable_smart_block, toggle: () => this._handleToggle('smart-blocking'), - description: t('hub_setup_smartblocking_description_smartblocking'), + description: t('smart_blocking_DESC'), } ]; diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx index 2d442232a..5b4ec4e55 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx @@ -48,7 +48,7 @@ const TutorialAntiSuiteView = () => (
    - {t('hub_tutorial_antisuite_antitracking_title')} + {t('enhanced_anti_tracking')}
    {t('hub_tutorial_antisuite_antitracking_description')} @@ -59,10 +59,10 @@ const TutorialAntiSuiteView = () => (
    - {t('hub_tutorial_antisuite_adblocking_title')} + {t('enhanced_ad_blocking')}
    - {t('hub_tutorial_antisuite_adblocking_description')} + {t('ad_blocking_DESC')}
    @@ -70,10 +70,10 @@ const TutorialAntiSuiteView = () => (
    - {t('hub_tutorial_antisuite_smartblocking_title')} + {t('smart_blocking')}
    - {t('hub_tutorial_antisuite_smartblocking_description')} + {t('smart_blocking_DESC')}
    diff --git a/app/panel/components/BuildingBlocks/CliqzFeature.jsx b/app/panel/components/BuildingBlocks/CliqzFeature.jsx index 093ebf0b1..b699e387a 100644 --- a/app/panel/components/BuildingBlocks/CliqzFeature.jsx +++ b/app/panel/components/BuildingBlocks/CliqzFeature.jsx @@ -57,13 +57,44 @@ class CliqzFeature extends React.Component { _getTooltipBodyText(active, isTooltipBody, type) { if (!isTooltipBody) return false; - return active ? - t(`tooltip_${type}_body_on`) : - t(`tooltip_${type}_body`); + if (active) { + switch (type) { + case 'ad_block': + return t('tooltip_ad_block_on'); + case 'anti_track': + return t('tooltip_anti_track_on'); + case 'smart_block': + return t('tooltip_smart_block_on'); + default: + return false; + } + } else { + switch (type) { + case 'ad_block': + return t('ad_blocking_DESC'); + case 'anti_track': + return t('anti_tracking_DESC'); + case 'smart_block': + return t('smart_blocking_DESC'); + default: + return false; + } + } } _getTooltipHeaderText(isTooltipHeader, type) { - return isTooltipHeader ? t(`tooltip_${type}`) : false; + if (!isTooltipHeader) return false; + + switch (type) { + case 'ad_block': + return t('enhanced_ad_blocking'); + case 'anti_track': + return t('enhanced_anti_tracking'); + case 'smart_block': + return t('smart_blocking'); + default: + return false; + } } _getAlertText(active, type) { @@ -103,7 +134,14 @@ class CliqzFeature extends React.Component { const iconClassNames = ClassNames('CliqzFeature__icon', cssTypeName, 'g-tooltip'); const featureType = type === 'anti_track' ? 'anti_tracking' : type; - const featureName = t(`drawer_title_enable_${featureType}`); + let featureName; + if (featureType === 'anti_tracking') { + featureName = t('enhanced_anti_tracking'); + } else if (featureType === 'ad_block') { + featureName = t('enhanced_ad_blocking'); + } else if (featureType === 'smart_block') { + featureName = t('smart_blocking'); + } return (
    diff --git a/app/panel/reducers/panel.js b/app/panel/reducers/panel.js index 20b873259..b249b6e70 100644 --- a/app/panel/reducers/panel.js +++ b/app/panel/reducers/panel.js @@ -108,7 +108,7 @@ export default (state = initialState, action) => { switch (err.code) { case '10050': case '10110': - errorText = t('banner_no_such_account_message'); + errorText = t('no_such_email_password_combo'); break; default: errorText = t('server_error_message'); From 2f912b736a053e44c71a89708d85fe9e56c5b0a7 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Sun, 6 Oct 2019 16:42:57 -0400 Subject: [PATCH 17/39] Factor plus promo modal rendering out to shared component. Implement rendering in panel. Implement display context CSS adjustments. --- _locales/en/messages.json | 12 ++ .../CreateAccountView.test.jsx.snap | 20 +- app/hub/Views/HomeView/HomeView.scss | 170 --------------- app/hub/Views/HomeView/HomeViewContainer.jsx | 71 +------ .../__snapshots__/HomeView.test.jsx.snap | 12 +- .../__snapshots__/LogInView.test.jsx.snap | 14 +- .../__snapshots__/PlusView.test.jsx.snap | 68 +++--- .../__snapshots__/RewardsView.test.jsx.snap | 2 +- .../SetupBlockingDropdown.test.jsx.snap | 2 +- .../TutorialAntiSuiteView.test.jsx.snap | 18 +- .../TutorialBlockingView.test.jsx.snap | 4 +- .../TutorialLayoutView.test.jsx.snap | 4 +- .../TutorialTrustView.test.jsx.snap | 8 +- app/panel/components/Panel.jsx | 8 + app/shared-components/Modal/Modal.jsx | 99 +++++++++ app/shared-components/Modal/Modal.scss | 200 ++++++++++++++++++ 16 files changed, 397 insertions(+), 315 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e5ab96c38..25137c89f 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -385,6 +385,15 @@ "password_field_label_required": { "message": "Password required." }, + "name": { + "description": "The title of this extension (Ghostery).", + "message": "$EXTENSION_NAME$ – Privacy Ad Blocker", + "placeholders": { + "extension_name": { + "content": "Ghostery" + } + } + }, "none": { "description": "Used to say that this item does not belong to any of the groups, as in \"Groups: None\" (as opposed to \"Groups: Group A, Group B, Group C\").", "message": "None" @@ -1990,6 +1999,9 @@ "choose_your_privacy_plan": { "message": "Choose your privacy plan" }, + "choose_your_ghostery_privacy_plan": { + "message": "Choose your Ghostery privacy plan" + }, "ghostery_basic": { "message": "Ghostery Basic" }, diff --git a/app/hub/Views/CreateAccountView/__tests__/__snapshots__/CreateAccountView.test.jsx.snap b/app/hub/Views/CreateAccountView/__tests__/__snapshots__/CreateAccountView.test.jsx.snap index 61e4288d1..d1f69a18d 100644 --- a/app/hub/Views/CreateAccountView/__tests__/__snapshots__/CreateAccountView.test.jsx.snap +++ b/app/hub/Views/CreateAccountView/__tests__/__snapshots__/CreateAccountView.test.jsx.snap @@ -40,7 +40,7 @@ exports[`app/hub/Views/CreateAccount component Snapshot tests with react-test-re className="CreateAccountView__inputLabel" htmlFor="create-account-email" > - hub_create_account_label_email + email_colon - hub_create_account_label_email_confirm + confirm_email_colon - hub_create_account_label_password + password_colon - hub_create_account_button_submit + create_account
    @@ -288,7 +288,7 @@ exports[`app/hub/Views/CreateAccount component Snapshot tests with react-test-re className="CreateAccountView__inputLabel" htmlFor="create-account-email" > - hub_create_account_label_email + email_colon - hub_create_account_label_email_invalid + please_enter_a_valid_email
    - hub_create_account_label_email_confirm + confirm_email_colon - hub_create_account_label_email_confirm_invalid + your_emails_do_not_match
    @@ -386,7 +386,7 @@ exports[`app/hub/Views/CreateAccount component Snapshot tests with react-test-re className="CreateAccountView__inputLabel" htmlFor="create-account-password" > - hub_create_account_label_password + password_colon - hub_create_account_button_submit + create_account
    diff --git a/app/hub/Views/HomeView/HomeView.scss b/app/hub/Views/HomeView/HomeView.scss index e8ac4227b..7a228b9cc 100644 --- a/app/hub/Views/HomeView/HomeView.scss +++ b/app/hub/Views/HomeView/HomeView.scss @@ -152,176 +152,6 @@ } } -.PlusPromoModal__content { - position: relative; - width: 646px; - height: 553px; - background-color: #f7f7f7; - border: 1.9px solid #930194; - z-index: 10; -} - -.PlusPromoModal__buttons-background { - background-color: #e7ecee; - position: absolute; - bottom: 3px; - width: 99%; - height: 72px; - z-index: -1; -} - -.PlusPromoModal__thanks-for-download { - padding-top: 30px; - font-size: 26px; - font-weight: bold; -} - -.PlusPromoModal__choose-your-plan { - padding-top: 10px; - font-size: 18px; - font-weight: bold; -} - -.PlusPromoModal__options-container { - height: 100%; - margin-top: 44px; - display: flex; - justify-content: space-evenly; -} - -.PlusPromoModal__option-container { - display: flex; - align-items: center; - flex-direction: column; -} - -.PlusPromoModal__option-description-box { - width: 240px; - height: 298px; - margin-bottom: 45px; - border-style: solid; - border-width: 3px; - background-color: #FFFFFF; - - &.basic { - border-color: #1dafed; - } - - &.plus { - position: relative; // so that the recommended banner is positioned relative to this element - border-image-source: linear-gradient(39deg, #f2daa2, #eab968); - border-image-slice: 1; - } -} - -.PlusPromoModal__recommended-banner { - position: absolute; - left: -30px; - top: -30px; -} - -.PlusPromoModal__recommended-banner-text { - position: relative; - left: 18px; - top: -47px; - transform: rotate(-15deg); - color: white; - font-size: 16px; - font-weight: bold; -} - -.PlusPromoModal__option-header { - margin-top: 20px; - font-size: 19px; - font-weight: bold; - letter-spacing: -0.5px; - text-align: center; - - &.basic { color: #1dafed; } - &.plus { color: #ebbf73; } -} - -.PlusPromoModal__price-text { - line-height: 1.3; - text-align: center; - - &.basic { color: #1dafed; } - &.plus { color: #ebbf73; } -} - -.PlusPromoModal__currency-sign { - font-size: 30px; - font-weight: bold; -} - -.PlusPromoModal__amount { - vertical-align: middle; - font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 58px; -} - -.PlusPromoModal__per-month { - vertical-align: sub; - font-size: 18px; - font-weight: bold; -} - -.PlusPromoModal__option-description-item { - font-size: 16px; - text-align: center; - color: #333333; - - &.italic { - font-style: italic; - } -} - -.PlusPromoModal__check-icon { - padding-right: 5px; - margin-top: -18px; -} - -.PlusPromoModal__plus-option-description-item-container { - display: flex; - justify-content: center; -} - -.PlusPromoModal__button { - margin: 0; - font-size: 11px; - font-weight: 600; - letter-spacing: 0.5px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: none; - - &.basic { - width: 135px; - height: 40px; - border: solid 2px #15b4f2; - background-color: white; - color: #2cbcf4; - } - &.basic:hover { - background-color: #2cbcf4; - color: white; - } - - &.plus { - width: 163px; - height: 38px; - border: none; - background-image: linear-gradient(to bottom, #2fdbfa, #15b4f2); - color: white; - font-weight: 600; - letter-spacing: 0.5px; - } - &.plus:hover { - background-image: linear-gradient(to bottom, #1fcbea, #05a4e2); - } -} - // Firefox Font Size Override and Image Size Override @-moz-document url-prefix() { .HomeView--firefoxFontSize { diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index 54c63194a..fc8ddbd43 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -59,7 +59,7 @@ class HomeViewContainer extends Component { /** * @private - * Dismisses the Plus promo modal if user opts to stick with the basic plan + * Dismisses the Plus promo modal when user clicks either the Select Plus or Select Basic button */ _dismissModal = () => { this.setState({ @@ -68,73 +68,6 @@ class HomeViewContainer extends Component { this.props.actions.markPlusPromoModalShown(); } - _renderPlusPromoModal = () => ( -
    -
    -
    - {t('thanks_for_downloading_ghostery')} -
    -
    - {t('choose_your_privacy_plan')} -
    -
    -
    -
    -
    {t('ghostery_basic')}
    -
    - {t('locale_appropriate_currency_icon')} - 0 - - {t('per_month')} -
    -
    -

    {t('protection_for_this_browser')}

    -

    {t('blocks_ads')}

    -

    {t('blocks_trackers')}

    -

    {t('fast_browsing')}

    -
    -
    -
    - {t('select_basic')} -
    -
    -
    -
    -
    - -
    {t('recommended')}
    -
    -
    Ghostery Plus
    -
    - {t('locale_appropriate_currency_icon')} - {t('plus_monthly_subscription_price_number')} - - per month -
    -
    -

    All basic features, plus:

    -
    - -

    Historical Tracker Stats

    -
    -
    - -

    Priority Support

    -
    -
    - -

    New Color Themes

    -
    -
    -
    - - Select Plus - -
    -
    -
    - ); - _render() { const { justInstalled, plusPromoModalShown } = this.state; const { home, user } = this.props; @@ -157,7 +90,7 @@ class HomeViewContainer extends Component { return (
    - {this._renderPlusPromoModal()} + {Modal.renderPlusPromo('inHub', this._dismissModal)}
    diff --git a/app/hub/Views/HomeView/__tests__/__snapshots__/HomeView.test.jsx.snap b/app/hub/Views/HomeView/__tests__/__snapshots__/HomeView.test.jsx.snap index 1b14914b8..808de5ec5 100644 --- a/app/hub/Views/HomeView/__tests__/__snapshots__/HomeView.test.jsx.snap +++ b/app/hub/Views/HomeView/__tests__/__snapshots__/HomeView.test.jsx.snap @@ -75,7 +75,7 @@ exports[`app/hub/Views/HomeView component Snapshot tests with react-test-rendere href="/create-account" onClick={[Function]} > - hub_home_subheader_create_account + create_account
    - hub_home_feature_tutorial_button + start
    - hub_home_feature_setup_title + customize_setup
    - hub_home_feature_supporter_button + get_ghostery_plus
    @@ -295,7 +295,7 @@ exports[`app/hub/Views/HomeView component Snapshot tests with react-test-rendere
    - hub_home_feature_setup_title + customize_setup
    - hub_home_feature_supporter_button_alt + Aready_Subscribed
    diff --git a/app/hub/Views/LogInView/__tests__/__snapshots__/LogInView.test.jsx.snap b/app/hub/Views/LogInView/__tests__/__snapshots__/LogInView.test.jsx.snap index 82823ec9d..d6b6f0e0f 100644 --- a/app/hub/Views/LogInView/__tests__/__snapshots__/LogInView.test.jsx.snap +++ b/app/hub/Views/LogInView/__tests__/__snapshots__/LogInView.test.jsx.snap @@ -40,7 +40,7 @@ exports[`app/hub/Views/LogIn component Snapshot tests with react-test-renderer l className="LogInView__inputLabel" htmlFor="login-email" > - hub_login_label_email + email_colon - hub_login_label_password + password_colon - hub_login_button_submit + sign_in
    @@ -140,7 +140,7 @@ exports[`app/hub/Views/LogIn component Snapshot tests with react-test-renderer l className="LogInView__inputLabel" htmlFor="login-email" > - hub_login_label_email + email_colon - hub_login_label_email_invalid + please_enter_a_valid_email
    - hub_login_button_submit + sign_in
    diff --git a/app/hub/Views/PlusView/__tests__/__snapshots__/PlusView.test.jsx.snap b/app/hub/Views/PlusView/__tests__/__snapshots__/PlusView.test.jsx.snap index dff7f3f49..c673637ca 100644 --- a/app/hub/Views/PlusView/__tests__/__snapshots__/PlusView.test.jsx.snap +++ b/app/hub/Views/PlusView/__tests__/__snapshots__/PlusView.test.jsx.snap @@ -29,7 +29,7 @@ exports[`app/hub/Views/PlusView component Snapshot tests with react-test-rendere
    - hub_supporter_button_text_alt + already_subscribed
    - hub_supporter_perk_themes_title + new_themes
    - hub_supporter_perk_stats_title + historical_blocking_stats
    - hub_supporter_feature_theme_title + new_themes
    - hub_supporter_button_text_alt + already_subscribed
    hub_supporter_feature_theme_title @@ -157,7 +157,7 @@ exports[`app/hub/Views/PlusView component Snapshot tests with react-test-rendere
    - hub_supporter_button_text_alt + already_subscribed
    hub_supporter_feature_stats_title @@ -186,7 +186,7 @@ exports[`app/hub/Views/PlusView component Snapshot tests with react-test-rendere
    - hub_supporter_feature_stats_title + historical_blocking_stats
    - hub_supporter_button_text_alt + already_subscribed
    hub_supporter_feature_stats_title @@ -221,7 +221,7 @@ exports[`app/hub/Views/PlusView component Snapshot tests with react-test-rendere
    - hub_supporter_button_text_alt + already_subscribed
    - hub_supporter_feature_support_title + priority_support
    - hub_supporter_button_text_alt + already_subscribed
    hub_supporter_feature_support_title @@ -276,7 +276,7 @@ exports[`app/hub/Views/PlusView component Snapshot tests with react-test-rendere
    - hub_supporter_button_text_alt + already_subscribed
    - hub_supporter_button_text_alt + already_subscribed
    - hub_supporter_button_text + get_ghostery_plus
    - hub_supporter_perk_themes_title + new_themes
    - hub_supporter_perk_stats_title + historical_blocking_stats
    - hub_supporter_feature_theme_title + new_themes
    - hub_supporter_button_text + get_ghostery_plus
    hub_supporter_feature_theme_title @@ -502,7 +502,7 @@ exports[`app/hub/Views/PlusView component Snapshot tests with react-test-rendere rel="noopener noreferrer" target="_blank" > - hub_supporter_button_text + get_ghostery_plus
    hub_supporter_feature_stats_title @@ -531,7 +531,7 @@ exports[`app/hub/Views/PlusView component Snapshot tests with react-test-rendere
    - hub_supporter_feature_stats_title + historical_blocking_stats
    - hub_supporter_button_text + get_ghostery_plus
    hub_supporter_feature_stats_title @@ -574,7 +574,7 @@ exports[`app/hub/Views/PlusView component Snapshot tests with react-test-rendere rel="noopener noreferrer" target="_blank" > - hub_supporter_button_text + get_ghostery_plus
    - hub_supporter_feature_support_title + priority_support
    - hub_supporter_button_text + get_ghostery_plus
    hub_supporter_feature_support_title @@ -637,7 +637,7 @@ exports[`app/hub/Views/PlusView component Snapshot tests with react-test-rendere rel="noopener noreferrer" target="_blank" > - hub_supporter_button_text + get_ghostery_plus
    - hub_supporter_button_text + get_ghostery_plus
    - hub_rewards_header_learn_more + learn_more
    diff --git a/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/__snapshots__/SetupBlockingDropdown.test.jsx.snap b/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/__snapshots__/SetupBlockingDropdown.test.jsx.snap index 0fe67c4a8..1bbb9a35c 100644 --- a/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/__snapshots__/SetupBlockingDropdown.test.jsx.snap +++ b/app/hub/Views/SetupViews/SetupBlockingDropdown/__tests__/__snapshots__/SetupBlockingDropdown.test.jsx.snap @@ -14,7 +14,7 @@ exports[`app/hub/Views/SetupViews/SetupBlockingDropdown component Snapshot tests className="button success" onClick={[Function]} > - hub_setup_nav_done + done
    diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap index a99bef9d4..83a6fdc3e 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap @@ -10,20 +10,20 @@ exports[`app/hub/Views/TutorialViews/TutorialAntiSuiteView component Snapshot te
    - hub_tutorial_simple_view + simple_view
    hub_tutorial_simple_view
    - hub_tutorial_detailed_view + detailed_view
    hub_tutorial_detailed_view @@ -49,7 +49,7 @@ exports[`app/hub/Views/TutorialViews/TutorialAntiSuiteView component Snapshot te
    - hub_tutorial_antisuite_antitracking_title + enhanced_anti_tracking
    - hub_tutorial_antisuite_adblocking_title + enhanced_ad_blocking
    - hub_tutorial_antisuite_adblocking_description + ad_blocking_DESC
    @@ -87,12 +87,12 @@ exports[`app/hub/Views/TutorialViews/TutorialAntiSuiteView component Snapshot te
    - hub_tutorial_antisuite_smartblocking_title + smart_blocking
    - hub_tutorial_antisuite_smartblocking_description + smart_blocking_DESC
    diff --git a/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap b/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap index a86a12011..f314105f1 100644 --- a/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap +++ b/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap @@ -10,10 +10,10 @@ exports[`app/hub/Views/TutorialViews/TutorialBlockingView component Snapshot tes
    - hub_tutorial_detailed_view + detailed_view
    hub_tutorial_detailed_view diff --git a/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/__snapshots__/TutorialLayoutView.test.jsx.snap b/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/__snapshots__/TutorialLayoutView.test.jsx.snap index 4bfd63c38..1b7dcb048 100644 --- a/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/__snapshots__/TutorialLayoutView.test.jsx.snap +++ b/app/hub/Views/TutorialViews/TutorialLayoutView/__tests__/__snapshots__/TutorialLayoutView.test.jsx.snap @@ -8,12 +8,12 @@ exports[`app/hub/Views/TutorialViews/TutorialLayoutView component Snapshot tests className="columns small-10 medium-8 large-6 small-offset-1" > hub_tutorial_simple_view hub_tutorial_detailed_view diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap b/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap index a472a37fe..27e9a3973 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap +++ b/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap @@ -10,20 +10,20 @@ exports[`app/hub/Views/TutorialViews/TutorialTrustView component Snapshot tests
    - hub_tutorial_simple_view + simple_view
    hub_tutorial_simple_view
    - hub_tutorial_detailed_view + detailed_view
    hub_tutorial_detailed_view diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index ce30e1e5c..da1858312 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -14,6 +14,7 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; import Header from '../containers/HeaderContainer'; +import Modal from '../../shared-components/Modal'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; @@ -230,6 +231,10 @@ class Panel extends React.Component { return false; } + _plusPromoClickHandlerPlaceholder = () => { + console.error('IVZ Panfel#_plusPromoClickHandlerPlaceholder'); + } + /** * React's required render function. Returns JSX * @return {JSX} JSX for rendering the Panel @@ -244,6 +249,9 @@ class Panel extends React.Component { return (
    + + {Modal.renderPlusPromo('inPanel', this._plusPromoClickHandlerPlaceholder)} +
    diff --git a/app/shared-components/Modal/Modal.jsx b/app/shared-components/Modal/Modal.jsx index 108ce320f..bf0d2b158 100644 --- a/app/shared-components/Modal/Modal.jsx +++ b/app/shared-components/Modal/Modal.jsx @@ -13,6 +13,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import ClassNames from 'classnames'; /** * A Functional React component for a Modal @@ -32,6 +33,104 @@ const Modal = props => (
    ); +Modal.renderPlusPromo = (location, clickHandler) => { + const isInHub = location === 'inHub'; + const isInPanel = location === 'inPanel'; + + const locationClassName = { + 'in-hub': isInHub, + 'in-panel': isInPanel + }; + const contentClassNames = ClassNames( + 'PlusPromoModal__content', + 'flex-container', + 'flex-dir-column', + 'align-middle', + locationClassName + ); + const chooseYourPlanClassNames = ClassNames( + 'PlusPromoModal__choose-your-plan', + locationClassName + ); + const recommendedBannerClassNames = ClassNames( + 'PlusPromoModal__recommended-banner', + locationClassName + ); + const optionDescriptionBoxClassNames = ClassNames( + 'PlusPromoModal__option-description-box', + locationClassName + ); + + return ( +
    +
    + {isInHub && ( +
    + {t('thanks_for_downloading_ghostery')} +
    + )} +
    + {isInHub ? t('choose_your_privacy_plan') : t('choose_your_ghostery_privacy_plan')} +
    +
    +
    +
    +
    {t('ghostery_basic')}
    +
    + {t('locale_appropriate_currency_icon')} + 0 + + {t('per_month')} +
    +
    +
    {t('protection_for_this_browser')}
    +
    {t('blocks_ads')}
    +
    {t('blocks_trackers')}
    +
    {t('fast_browsing')}
    +
    +
    +
    + {t('select_basic')} +
    +
    +
    +
    +
    + +
    {t('recommended')}
    +
    +
    Ghostery Plus
    +
    + {t('locale_appropriate_currency_icon')} + {t('plus_monthly_subscription_price_number')} + + per month +
    +
    +
    All basic features, plus:
    +
    + +
    Historical Tracker Stats
    +
    +
    + +
    Priority Support
    +
    +
    + +
    New Color Themes
    +
    +
    +
    + + Select Plus + +
    +
    +
    + ); +}; + // PropTypes ensure we pass required props of the correct type Modal.propTypes = { show: PropTypes.bool.isRequired, diff --git a/app/shared-components/Modal/Modal.scss b/app/shared-components/Modal/Modal.scss index 518bcb9fe..f8c3ba7db 100644 --- a/app/shared-components/Modal/Modal.scss +++ b/app/shared-components/Modal/Modal.scss @@ -24,3 +24,203 @@ background-color: rgba(#000000, 0.25); z-index: 9; } + +.PlusPromoModal__content { + position: relative; + background-color: #f7f7f7; + border: 1.9px solid #930194; + z-index: 10; + + &.in-hub { + width: 646px; + height: 553px; + } + + &.in-panel { + width: 556px; + height: 471px; + } +} + +.PlusPromoModal__buttons-background { + background-color: #e7ecee; + position: absolute; + bottom: 3px; + width: 99%; + height: 72px; + z-index: -1; +} + +.PlusPromoModal__thanks-for-download { + padding-top: 30px; + font-size: 26px; + font-weight: bold; +} + +.PlusPromoModal__choose-your-plan { + font-size: 18px; + font-weight: bold; + + &.in-hub { padding-top: 10px; } + &.in-panel { padding-top: 17px; } +} + +.PlusPromoModal__options-container { + height: 100%; + margin-top: 44px; + display: flex; + justify-content: space-evenly; +} + +.PlusPromoModal__option-container { + display: flex; + align-items: center; + flex-direction: column; +} + +.PlusPromoModal__option-description-box { + border-style: solid; + border-width: 3px; + background-color: #FFFFFF; + + &.basic { + border-color: #1dafed; + } + + &.plus { + position: relative; // so that the recommended banner is positioned relative to this element + border-image-source: linear-gradient(39deg, #f2daa2, #eab968); + border-image-slice: 1; + } + + &.in-hub { + margin-bottom: 45px; + width: 240px; + height: 298px; + } + + &.in-panel { + margin-bottom: 30px; + width: 221px; + height: 294px; + } +} + +.PlusPromoModal__recommended-banner { + position: absolute; + + &.in-hub { + left: -30px; + top: -30px; + } + + &.in-panel { + left: -10px; + top: -32px; + } +} + +.PlusPromoModal__recommended-banner-text { + position: relative; + left: 18px; + top: -47px; + transform: rotate(-15deg); + color: white; + font-size: 16px; + font-weight: bold; +} + +.PlusPromoModal__option-header { + margin-top: 20px; + font-size: 19px; + font-weight: bold; + letter-spacing: -0.5px; + text-align: center; + + &.basic { color: #1dafed; } + &.plus { color: #ebbf73; } +} + +.PlusPromoModal__price-text { + line-height: 1.3; + text-align: center; + + &.basic { color: #1dafed; } + &.plus { color: #ebbf73; } +} + +.PlusPromoModal__currency-sign { + font-size: 30px; + font-weight: bold; +} + +.PlusPromoModal__amount { + vertical-align: middle; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 58px; +} + +.PlusPromoModal__per-month { + vertical-align: sub; + font-size: 18px; + font-weight: bold; +} + +.PlusPromoModal__option-description-item { + padding-bottom: 15px; + font-size: 16px; + text-align: center; + color: #333333; + + &.italic { + font-style: italic; + } +} + +.PlusPromoModal__check-icon { + padding-right: 5px; + margin-top: -18px; +} + +.PlusPromoModal__plus-option-description-item-container { + display: flex; + justify-content: center; +} + +.PlusPromoModal__button { + margin: 0; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: none; + + &.basic { + width: 135px; + height: 40px; + border: solid 2px #15b4f2; + background-color: white; + color: #2cbcf4; + } + &.basic:hover { + background-color: #2cbcf4; + color: white; + } + + &.plus { + width: 163px; + height: 38px; + border: none; + background-image: linear-gradient(to bottom, #2fdbfa, #15b4f2); + color: white; + font-weight: 600; + letter-spacing: 0.5px; + } + &.plus:hover { + background-image: linear-gradient(to bottom, #1fcbea, #05a4e2); + } +} + From 6e7be93c49b7eaa550d99d2d3e9f912c48b761b2 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 7 Oct 2019 12:23:57 -0400 Subject: [PATCH 18/39] Move Plus Promo modal rendering to a PlusPromoModal shared component. Begin to implement conditional panel display logic --- app/hub/Views/HomeView/HomeViewContainer.jsx | 19 +- app/hub/utils/index.js | 8 +- app/panel/components/Panel.jsx | 14 +- app/scss/hub.scss | 1 + app/scss/panel.scss | 1 + app/shared-components/Modal/Modal.jsx | 99 --------- app/shared-components/Modal/Modal.scss | 200 ------------------ .../PlusPromoModal/PlusPromoModal.jsx | 138 ++++++++++++ .../PlusPromoModal/PlusPromoModal.scss | 200 ++++++++++++++++++ app/shared-components/PlusPromoModal/index.js | 16 ++ app/shared-components/index.js | 2 + src/background.js | 4 + src/classes/ConfData.js | 1 + src/classes/PanelData.js | 3 +- 14 files changed, 390 insertions(+), 316 deletions(-) create mode 100644 app/shared-components/PlusPromoModal/PlusPromoModal.jsx create mode 100644 app/shared-components/PlusPromoModal/PlusPromoModal.scss create mode 100644 app/shared-components/PlusPromoModal/index.js diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index fc8ddbd43..42bf75371 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -15,7 +15,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import QueryString from 'query-string'; import HomeView from './HomeView'; -import { Modal } from '../../../shared-components'; +import { PlusPromoModal } from '../../../shared-components'; +import { sendMessage } from '../../utils'; /** * @class Implement the Home View for the Ghostery Hub @@ -30,7 +31,6 @@ class HomeViewContainer extends Component { this.state = { getUserResolved: false, justInstalled: justInstalled === 'true', - plusPromoModalShown: props.home.plus_promo_modal_shown, }; const title = t('hub_home_page_title'); @@ -62,17 +62,16 @@ class HomeViewContainer extends Component { * Dismisses the Plus promo modal when user clicks either the Select Plus or Select Basic button */ _dismissModal = () => { - this.setState({ - plusPromoModalShown: true, - }); this.props.actions.markPlusPromoModalShown(); + sendMessage('SET_PLUS_PROMO_MODAL_SEEN', {}); } _render() { - const { justInstalled, plusPromoModalShown } = this.state; + const { justInstalled } = this.state; const { home, user } = this.props; const isPlus = user && user.subscriptionsPlus || false; const { + plus_promo_modal_shown, setup_complete, tutorial_complete, enable_metrics, @@ -89,9 +88,11 @@ class HomeViewContainer extends Component { return (
    - - {Modal.renderPlusPromo('inHub', this._dismissModal)} - +
    ); diff --git a/app/hub/utils/index.js b/app/hub/utils/index.js index 0766b4f07..156704369 100644 --- a/app/hub/utils/index.js +++ b/app/hub/utils/index.js @@ -14,10 +14,14 @@ // Imports utilities from elsewhere in the codebase to reduce duplicate code import { log } from '../../../src/utils/common'; -import { sendMessageInPromise as importedSMIP } from '../../panel/utils/msg'; +import { sendMessage as importedSM, sendMessageInPromise as importedSMIP } from '../../panel/utils/msg'; const sendMessageInPromise = function(name, message) { return importedSMIP(name, message, 'ghostery-hub'); }; -export { log, sendMessageInPromise }; +const sendMessage = function(name, message) { + return importedSM(name, message, 'ghostery-hub'); +}; + +export { log, sendMessage, sendMessageInPromise }; diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index da1858312..047cfff32 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -14,7 +14,7 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; import Header from '../containers/HeaderContainer'; -import Modal from '../../shared-components/Modal'; +import { PlusPromoModal } from '../../shared-components'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; @@ -232,7 +232,7 @@ class Panel extends React.Component { } _plusPromoClickHandlerPlaceholder = () => { - console.error('IVZ Panfel#_plusPromoClickHandlerPlaceholder'); + console.error('IVZ Panel#_plusPromoClickHandlerPlaceholder'); } /** @@ -247,11 +247,15 @@ class Panel extends React.Component { const notificationText = this.props.notificationShown && this.renderNotification(); + const { plusPromoModalSeen } = this.props; + return (
    - - {Modal.renderPlusPromo('inPanel', this._plusPromoClickHandlerPlaceholder)} - +
    diff --git a/app/scss/hub.scss b/app/scss/hub.scss index 0c55b41a4..a59365f96 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -76,6 +76,7 @@ html, body, #root { // Imports from ../shared-components directory @import '../shared-components/ExitButton/ExitButton.scss'; @import '../shared-components/Modal/Modal.scss'; +@import '../shared-components/PlusPromoModal/PlusPromoModal.scss'; @import '../shared-components/SteppedNavigation/SteppedNavigation.scss'; @import '../shared-components/ToastMessage/ToastMessage.scss'; @import '../shared-components/ToggleCheckbox/ToggleCheckbox.scss'; diff --git a/app/scss/panel.scss b/app/scss/panel.scss index 57d6827ac..f79b4a723 100644 --- a/app/scss/panel.scss +++ b/app/scss/panel.scss @@ -79,3 +79,4 @@ html body { @import '../shared-components/ModalExitButton/ModalExitButton.scss'; @import '../shared-components/Modal/Modal.scss'; @import '../shared-components/InsightsPromoModal/InsightsPromoModal.scss'; +@import '../shared-components/PlusPromoModal/PlusPromoModal.scss'; diff --git a/app/shared-components/Modal/Modal.jsx b/app/shared-components/Modal/Modal.jsx index bf0d2b158..108ce320f 100644 --- a/app/shared-components/Modal/Modal.jsx +++ b/app/shared-components/Modal/Modal.jsx @@ -13,7 +13,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ClassNames from 'classnames'; /** * A Functional React component for a Modal @@ -33,104 +32,6 @@ const Modal = props => (
    ); -Modal.renderPlusPromo = (location, clickHandler) => { - const isInHub = location === 'inHub'; - const isInPanel = location === 'inPanel'; - - const locationClassName = { - 'in-hub': isInHub, - 'in-panel': isInPanel - }; - const contentClassNames = ClassNames( - 'PlusPromoModal__content', - 'flex-container', - 'flex-dir-column', - 'align-middle', - locationClassName - ); - const chooseYourPlanClassNames = ClassNames( - 'PlusPromoModal__choose-your-plan', - locationClassName - ); - const recommendedBannerClassNames = ClassNames( - 'PlusPromoModal__recommended-banner', - locationClassName - ); - const optionDescriptionBoxClassNames = ClassNames( - 'PlusPromoModal__option-description-box', - locationClassName - ); - - return ( -
    -
    - {isInHub && ( -
    - {t('thanks_for_downloading_ghostery')} -
    - )} -
    - {isInHub ? t('choose_your_privacy_plan') : t('choose_your_ghostery_privacy_plan')} -
    -
    -
    -
    -
    {t('ghostery_basic')}
    -
    - {t('locale_appropriate_currency_icon')} - 0 - - {t('per_month')} -
    -
    -
    {t('protection_for_this_browser')}
    -
    {t('blocks_ads')}
    -
    {t('blocks_trackers')}
    -
    {t('fast_browsing')}
    -
    -
    -
    - {t('select_basic')} -
    -
    -
    -
    -
    - -
    {t('recommended')}
    -
    -
    Ghostery Plus
    -
    - {t('locale_appropriate_currency_icon')} - {t('plus_monthly_subscription_price_number')} - - per month -
    -
    -
    All basic features, plus:
    -
    - -
    Historical Tracker Stats
    -
    -
    - -
    Priority Support
    -
    -
    - -
    New Color Themes
    -
    -
    -
    - - Select Plus - -
    -
    -
    - ); -}; - // PropTypes ensure we pass required props of the correct type Modal.propTypes = { show: PropTypes.bool.isRequired, diff --git a/app/shared-components/Modal/Modal.scss b/app/shared-components/Modal/Modal.scss index f8c3ba7db..518bcb9fe 100644 --- a/app/shared-components/Modal/Modal.scss +++ b/app/shared-components/Modal/Modal.scss @@ -24,203 +24,3 @@ background-color: rgba(#000000, 0.25); z-index: 9; } - -.PlusPromoModal__content { - position: relative; - background-color: #f7f7f7; - border: 1.9px solid #930194; - z-index: 10; - - &.in-hub { - width: 646px; - height: 553px; - } - - &.in-panel { - width: 556px; - height: 471px; - } -} - -.PlusPromoModal__buttons-background { - background-color: #e7ecee; - position: absolute; - bottom: 3px; - width: 99%; - height: 72px; - z-index: -1; -} - -.PlusPromoModal__thanks-for-download { - padding-top: 30px; - font-size: 26px; - font-weight: bold; -} - -.PlusPromoModal__choose-your-plan { - font-size: 18px; - font-weight: bold; - - &.in-hub { padding-top: 10px; } - &.in-panel { padding-top: 17px; } -} - -.PlusPromoModal__options-container { - height: 100%; - margin-top: 44px; - display: flex; - justify-content: space-evenly; -} - -.PlusPromoModal__option-container { - display: flex; - align-items: center; - flex-direction: column; -} - -.PlusPromoModal__option-description-box { - border-style: solid; - border-width: 3px; - background-color: #FFFFFF; - - &.basic { - border-color: #1dafed; - } - - &.plus { - position: relative; // so that the recommended banner is positioned relative to this element - border-image-source: linear-gradient(39deg, #f2daa2, #eab968); - border-image-slice: 1; - } - - &.in-hub { - margin-bottom: 45px; - width: 240px; - height: 298px; - } - - &.in-panel { - margin-bottom: 30px; - width: 221px; - height: 294px; - } -} - -.PlusPromoModal__recommended-banner { - position: absolute; - - &.in-hub { - left: -30px; - top: -30px; - } - - &.in-panel { - left: -10px; - top: -32px; - } -} - -.PlusPromoModal__recommended-banner-text { - position: relative; - left: 18px; - top: -47px; - transform: rotate(-15deg); - color: white; - font-size: 16px; - font-weight: bold; -} - -.PlusPromoModal__option-header { - margin-top: 20px; - font-size: 19px; - font-weight: bold; - letter-spacing: -0.5px; - text-align: center; - - &.basic { color: #1dafed; } - &.plus { color: #ebbf73; } -} - -.PlusPromoModal__price-text { - line-height: 1.3; - text-align: center; - - &.basic { color: #1dafed; } - &.plus { color: #ebbf73; } -} - -.PlusPromoModal__currency-sign { - font-size: 30px; - font-weight: bold; -} - -.PlusPromoModal__amount { - vertical-align: middle; - font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 58px; -} - -.PlusPromoModal__per-month { - vertical-align: sub; - font-size: 18px; - font-weight: bold; -} - -.PlusPromoModal__option-description-item { - padding-bottom: 15px; - font-size: 16px; - text-align: center; - color: #333333; - - &.italic { - font-style: italic; - } -} - -.PlusPromoModal__check-icon { - padding-right: 5px; - margin-top: -18px; -} - -.PlusPromoModal__plus-option-description-item-container { - display: flex; - justify-content: center; -} - -.PlusPromoModal__button { - margin: 0; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: none; - - &.basic { - width: 135px; - height: 40px; - border: solid 2px #15b4f2; - background-color: white; - color: #2cbcf4; - } - &.basic:hover { - background-color: #2cbcf4; - color: white; - } - - &.plus { - width: 163px; - height: 38px; - border: none; - background-image: linear-gradient(to bottom, #2fdbfa, #15b4f2); - color: white; - font-weight: 600; - letter-spacing: 0.5px; - } - &.plus:hover { - background-image: linear-gradient(to bottom, #1fcbea, #05a4e2); - } -} - diff --git a/app/shared-components/PlusPromoModal/PlusPromoModal.jsx b/app/shared-components/PlusPromoModal/PlusPromoModal.jsx new file mode 100644 index 000000000..3ee37bdd1 --- /dev/null +++ b/app/shared-components/PlusPromoModal/PlusPromoModal.jsx @@ -0,0 +1,138 @@ +/** + * Plus Promo Modal Component + * renders Plus Promo inside of the shared Modal component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import ClassNames from 'classnames'; +import Modal from '../Modal/Modal'; + +/** + * A Functional React component for a Plus Promo Modal + * @return {JSX} JSX for rendering a Plus Promo Modal + * @memberof SharedComponents + */ +const PlusPromoModal = (props) => { + const { show, location, clickHandler } = props; + + const isInHub = location === 'hub'; + + const locationClassName = { + 'in-hub': isInHub, + 'in-panel': location === 'panel' + }; + const contentClassNames = ClassNames( + 'PlusPromoModal__content', + 'flex-container', + 'flex-dir-column', + 'align-middle', + locationClassName + ); + const optionsContainerClassNames = ClassNames( + 'PlusPromoModal__options-container', + 'full-width', + locationClassName + ); + const chooseYourPlanClassNames = ClassNames( + 'PlusPromoModal__choose-your-plan', + locationClassName + ); + const recommendedBannerClassNames = ClassNames( + 'PlusPromoModal__recommended-banner', + locationClassName + ); + const optionDescriptionBoxClassNames = ClassNames( + 'PlusPromoModal__option-description-box', + locationClassName + ); + + return ( + +
    +
    + {isInHub && ( +
    + {t('thanks_for_downloading_ghostery')} +
    + )} +
    + {isInHub ? t('choose_your_privacy_plan') : t('choose_your_ghostery_privacy_plan')} +
    +
    +
    +
    +
    {t('ghostery_basic')}
    +
    + {t('locale_appropriate_currency_icon')} + 0 + + {t('per_month')} +
    +
    +
    {t('protection_for_this_browser')}
    +
    {t('blocks_ads')}
    +
    {t('blocks_trackers')}
    +
    {t('fast_browsing')}
    +
    +
    +
    + {t('select_basic')} +
    +
    +
    +
    +
    + +
    {t('recommended')}
    +
    +
    Ghostery Plus
    +
    + {t('locale_appropriate_currency_icon')} + {t('plus_monthly_subscription_price_number')} + + per month +
    +
    +
    All basic features, plus:
    +
    + +
    Historical Tracker Stats
    +
    +
    + +
    Priority Support
    +
    +
    + +
    New Color Themes
    +
    +
    +
    + + Select Plus + +
    +
    +
    + + ); +}; + +// PropTypes ensure we pass required props of the correct type +PlusPromoModal.propTypes = { + show: PropTypes.bool.isRequired, + location: PropTypes.string.isRequired, + clickHandler: PropTypes.func.isRequired, +}; + +export default PlusPromoModal; diff --git a/app/shared-components/PlusPromoModal/PlusPromoModal.scss b/app/shared-components/PlusPromoModal/PlusPromoModal.scss new file mode 100644 index 000000000..7aea1c077 --- /dev/null +++ b/app/shared-components/PlusPromoModal/PlusPromoModal.scss @@ -0,0 +1,200 @@ +.PlusPromoModal__content { + position: relative; + background-color: #f7f7f7; + border: 1.9px solid #930194; + z-index: 10; + + &.in-hub { + width: 646px; + height: 553px; + } + + &.in-panel { + width: 556px; + height: 471px; + } +} + +.PlusPromoModal__buttons-background { + background-color: #e7ecee; + position: absolute; + bottom: 3px; + width: 99%; + height: 72px; + z-index: -1; +} + +.PlusPromoModal__thanks-for-download { + padding-top: 30px; + font-size: 26px; + font-weight: bold; +} + +.PlusPromoModal__choose-your-plan { + font-size: 18px; + font-weight: bold; + + &.in-hub { padding-top: 10px; } + &.in-panel { padding-top: 17px; } +} + +.PlusPromoModal__options-container { + height: 100%; + margin-top: 44px; + display: flex; + + &.in-hub { justify-content: space-evenly; } + &.in-panel { justify-content: space-around; } +} + +.PlusPromoModal__option-container { + display: flex; + align-items: center; + flex-direction: column; +} + +.PlusPromoModal__option-description-box { + border-style: solid; + border-width: 3px; + background-color: #FFFFFF; + + &.basic { + border-color: #1dafed; + } + + &.plus { + position: relative; // so that the recommended banner is positioned relative to this element + border-image-source: linear-gradient(39deg, #f2daa2, #eab968); + border-image-slice: 1; + } + + &.in-hub { + margin-bottom: 45px; + width: 240px; + height: 298px; + } + + &.in-panel { + margin-bottom: 30px; + width: 221px; + height: 294px; + } +} + +.PlusPromoModal__recommended-banner { + position: absolute; + + &.in-hub { + left: -30px; + top: -30px; + } + + &.in-panel { + left: -10px; + top: -32px; + } +} + +.PlusPromoModal__recommended-banner-text { + position: relative; + left: 18px; + top: -47px; + transform: rotate(-15deg); + color: white; + font-size: 16px; + font-weight: bold; +} + +.PlusPromoModal__option-header { + margin-top: 20px; + font-size: 19px; + font-weight: bold; + letter-spacing: -0.5px; + text-align: center; + + &.basic { color: #1dafed; } + &.plus { color: #ebbf73; } +} + +.PlusPromoModal__price-text { + line-height: 1.3; + text-align: center; + + &.basic { color: #1dafed; } + &.plus { color: #ebbf73; } +} + +.PlusPromoModal__currency-sign { + font-size: 30px; + font-weight: bold; +} + +.PlusPromoModal__amount { + vertical-align: middle; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 58px; +} + +.PlusPromoModal__per-month { + vertical-align: sub; + font-size: 18px; + font-weight: bold; +} + +.PlusPromoModal__option-description-item { + padding-bottom: 15px; + font-size: 16px; + text-align: center; + color: #333333; + + &.italic { + font-style: italic; + } +} + +.PlusPromoModal__check-icon { + padding-right: 5px; + margin-top: -18px; +} + +.PlusPromoModal__plus-option-description-item-container { + display: flex; + justify-content: center; +} + +.PlusPromoModal__button { + margin: 0; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: none; + + &.basic { + width: 135px; + height: 40px; + border: solid 2px #15b4f2; + background-color: white; + color: #2cbcf4; + } + &.basic:hover { + background-color: #2cbcf4; + color: white; + } + + &.plus { + width: 163px; + height: 38px; + border: none; + background-image: linear-gradient(to bottom, #2fdbfa, #15b4f2); + color: white; + font-weight: 600; + letter-spacing: 0.5px; + } + &.plus:hover { + background-image: linear-gradient(to bottom, #1fcbea, #05a4e2); + } +} diff --git a/app/shared-components/PlusPromoModal/index.js b/app/shared-components/PlusPromoModal/index.js new file mode 100644 index 000000000..da26a8b7e --- /dev/null +++ b/app/shared-components/PlusPromoModal/index.js @@ -0,0 +1,16 @@ +/** + * Point of entry index.js file for Plus Promo Modal Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import PlusPromoModal from './PlusPromoModal'; + +export default PlusPromoModal; diff --git a/app/shared-components/index.js b/app/shared-components/index.js index e2dcdc47e..e3abeee09 100644 --- a/app/shared-components/index.js +++ b/app/shared-components/index.js @@ -17,6 +17,7 @@ import ExitButton from './ExitButton'; import Modal from './Modal'; +import PlusPromoModal from './PlusPromoModal'; import SteppedNavigation from './SteppedNavigation'; import ToastMessage from './ToastMessage'; import ToggleCheckbox from './ToggleCheckbox'; @@ -27,6 +28,7 @@ import InsightsPromoModal from './InsightsPromoModal'; export { ExitButton, Modal, + PlusPromoModal, SteppedNavigation, ToastMessage, ToggleCheckbox, diff --git a/src/background.js b/src/background.js index de12238b3..e84c26439 100644 --- a/src/background.js +++ b/src/background.js @@ -541,6 +541,10 @@ function handleRewards(name, message, callback) { */ function handleGhosteryHub(name, message, callback) { switch (name) { + case 'SET_PLUS_PROMO_MODAL_SEEN': + // TODO move this to a Promos class? + conf.plus_promo_modal_last_seen = Date.now(); + break; case 'SEND_PING': { const { type } = message; metrics.ping(type); diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 47ae3af6f..67c5d3ee2 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -124,6 +124,7 @@ class ConfData { _initProperty('notify_library_updates', false); _initProperty('notify_upgrade_updates', true); _initProperty('paid_subscription', false); + _initProperty('plus_promo_modal_last_seen', null); _initProperty('rewards_accepted', false); _initProperty('rewards_opted_in', false); _initProperty('settings_last_imported', 0); diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 006738f2e..55815a3c6 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -351,7 +351,7 @@ class PanelData { const { id: tab_id } = this._activeTab; const { current_theme, enable_ad_block, enable_anti_tracking, enable_smart_block, - enable_offers, is_expanded, is_expert, language, reload_banner_status, + enable_offers, is_expanded, is_expert, language, plus_promo_modal_last_seen, reload_banner_status, trackers_banner_status, } = conf; @@ -365,6 +365,7 @@ class PanelData { is_expert, is_android: globals.BROWSER_INFO.os === 'android', language, + plusPromoModalSeen: !!plus_promo_modal_last_seen, reload_banner_status, tab_id, trackers_banner_status, From 41a08b16bb2d985b7a8cf080c6bbe29f31f2a22d Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Mon, 7 Oct 2019 16:32:14 -0400 Subject: [PATCH 19/39] Create ModalPromos background class responsible for modal promo related business logic and state management --- app/panel/components/Panel.jsx | 4 +-- src/background.js | 4 +-- src/classes/ConfData.js | 2 +- src/classes/ModalPromos.js | 49 ++++++++++++++++++++++++++++++++++ src/classes/PanelData.js | 3 ++- 5 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 src/classes/ModalPromos.js diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 047cfff32..8f017fe4b 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -247,12 +247,12 @@ class Panel extends React.Component { const notificationText = this.props.notificationShown && this.renderNotification(); - const { plusPromoModalSeen } = this.props; + const { isTimeForAnotherPlusPromo } = this.props; return (
    diff --git a/src/background.js b/src/background.js index e84c26439..9c6c3192a 100644 --- a/src/background.js +++ b/src/background.js @@ -42,6 +42,7 @@ import metrics from './classes/Metrics'; import rewards from './classes/Rewards'; import account from './classes/Account'; import GhosteryModule from './classes/Module'; +import modalPromos from './classes/ModalPromos'; // utilities import { allowAllwaysC2P } from './utils/click2play'; @@ -542,8 +543,7 @@ function handleRewards(name, message, callback) { function handleGhosteryHub(name, message, callback) { switch (name) { case 'SET_PLUS_PROMO_MODAL_SEEN': - // TODO move this to a Promos class? - conf.plus_promo_modal_last_seen = Date.now(); + modalPromos.recordPlusPromoSighting(); break; case 'SEND_PING': { const { type } = message; diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 67c5d3ee2..1b4de2764 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -124,7 +124,7 @@ class ConfData { _initProperty('notify_library_updates', false); _initProperty('notify_upgrade_updates', true); _initProperty('paid_subscription', false); - _initProperty('plus_promo_modal_last_seen', null); + _initProperty('plus_promo_modal_last_seen', 0); _initProperty('rewards_accepted', false); _initProperty('rewards_opted_in', false); _initProperty('settings_last_imported', 0); diff --git a/src/classes/ModalPromos.js b/src/classes/ModalPromos.js new file mode 100644 index 000000000..7f12df555 --- /dev/null +++ b/src/classes/ModalPromos.js @@ -0,0 +1,49 @@ +/** + * ModalPromos Class + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import conf from './Conf'; + +const DAYS_BETWEEN_PROMOS = { + plus: 30 +}; +const MSECS_IN_DAY = 1000 * 60 * 60 * 24; // msecs-in-sec * secs-in-min * mins-in-hour * hours-in-day + +/** + * Class for handling the business logic for the display of modal promos (Plus, Insights, etc...) + * @memberOf BackgroundClasses + */ +class ModalPromos { + recordPlusPromoSighting() { + this._recordPromoSighting('plus'); + } + + isTimeForAnotherPlusPromo() { + return this._isTimeForAnotherPlusPromo('plus'); + } + + _isTimeForAnotherPlusPromo(promoType) { + const lastSeenTime = conf[`${promoType}_promo_modal_last_seen`]; + + return ( + (Date.now() - lastSeenTime) > + (MSECS_IN_DAY * DAYS_BETWEEN_PROMOS[promoType]) + ); + } + + _recordPromoSighting(promoType) { + conf[`${promoType}_promo_modal_last_seen`] = Date.now(); + } +} + +// return the class as a singleton +export default new ModalPromos(); diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 55815a3c6..9d3053d90 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -27,6 +27,7 @@ import tabInfo from './TabInfo'; import rewards from './Rewards'; import account from './Account'; import dispatcher from './Dispatcher'; +import modalPromos from './ModalPromos'; import { getCliqzGhosteryBugs, sendCliqzModuleCounts } from '../utils/cliqzModulesData'; import { getActiveTab, flushChromeMemoryCache, processUrl } from '../utils/utils'; import { objectEntries, log } from '../utils/common'; @@ -365,7 +366,7 @@ class PanelData { is_expert, is_android: globals.BROWSER_INFO.os === 'android', language, - plusPromoModalSeen: !!plus_promo_modal_last_seen, + isTimeForAnotherPlusPromo: modalPromos.isTimeForAnotherPlusPromo(), reload_banner_status, tab_id, trackers_banner_status, From 75fa2da89c2259fbb924d86b364764d0cb9c0c16 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 8 Oct 2019 12:01:40 -0400 Subject: [PATCH 20/39] Add local state to Panel to make sure component rerenders after user dismisses promo panel. Send record of promo interaction to background --- app/panel/components/Panel.jsx | 12 ++++++++++-- src/background.js | 8 ++++++-- src/classes/PanelData.js | 2 +- src/classes/{ModalPromos.js => PromoModals.js} | 8 ++++---- 4 files changed, 21 insertions(+), 9 deletions(-) rename src/classes/{ModalPromos.js => PromoModals.js} (85%) diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 8f017fe4b..f49c9b1e7 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -35,6 +35,10 @@ class Panel extends React.Component { this.closeNotification = this.closeNotification.bind(this); this.clickReloadBanner = this.clickReloadBanner.bind(this); this.filterTrackers = this.filterTrackers.bind(this); + + this.state = { + plusPromoModalShown: false, + }; } /** @@ -232,7 +236,10 @@ class Panel extends React.Component { } _plusPromoClickHandlerPlaceholder = () => { - console.error('IVZ Panel#_plusPromoClickHandlerPlaceholder'); + sendMessage('promoModals.sawPlusPromo', {}); + this.setState({ + plusPromoModalShown: true + }); } /** @@ -247,12 +254,13 @@ class Panel extends React.Component { const notificationText = this.props.notificationShown && this.renderNotification(); + const { plusPromoModalShown } = this.state; const { isTimeForAnotherPlusPromo } = this.props; return (
    diff --git a/src/background.js b/src/background.js index 9c6c3192a..c1f9b4683 100644 --- a/src/background.js +++ b/src/background.js @@ -42,7 +42,7 @@ import metrics from './classes/Metrics'; import rewards from './classes/Rewards'; import account from './classes/Account'; import GhosteryModule from './classes/Module'; -import modalPromos from './classes/ModalPromos'; +import promoModals from './classes/PromoModals'; // utilities import { allowAllwaysC2P } from './utils/click2play'; @@ -543,7 +543,7 @@ function handleRewards(name, message, callback) { function handleGhosteryHub(name, message, callback) { switch (name) { case 'SET_PLUS_PROMO_MODAL_SEEN': - modalPromos.recordPlusPromoSighting(); + promoModals.recordPlusPromoSighting(); break; case 'SEND_PING': { const { type } = message; @@ -1078,6 +1078,10 @@ function onMessageHandler(request, sender, callback) { }); return true; } + if (name === 'promoModals.sawPlusPromo') { + promoModals.recordPlusPromoSighting(); + return true; + } } /** diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 9d3053d90..65fb55026 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -27,7 +27,7 @@ import tabInfo from './TabInfo'; import rewards from './Rewards'; import account from './Account'; import dispatcher from './Dispatcher'; -import modalPromos from './ModalPromos'; +import modalPromos from './PromoModals'; import { getCliqzGhosteryBugs, sendCliqzModuleCounts } from '../utils/cliqzModulesData'; import { getActiveTab, flushChromeMemoryCache, processUrl } from '../utils/utils'; import { objectEntries, log } from '../utils/common'; diff --git a/src/classes/ModalPromos.js b/src/classes/PromoModals.js similarity index 85% rename from src/classes/ModalPromos.js rename to src/classes/PromoModals.js index 7f12df555..b8650c9fb 100644 --- a/src/classes/ModalPromos.js +++ b/src/classes/PromoModals.js @@ -1,5 +1,5 @@ /** - * ModalPromos Class + * PromoModals Class * * Ghostery Browser Extension * https://www.ghostery.com/ @@ -19,10 +19,10 @@ const DAYS_BETWEEN_PROMOS = { const MSECS_IN_DAY = 1000 * 60 * 60 * 24; // msecs-in-sec * secs-in-min * mins-in-hour * hours-in-day /** - * Class for handling the business logic for the display of modal promos (Plus, Insights, etc...) + * Class for handling the business logic for the display of promo modals (Plus, Insights, etc...) * @memberOf BackgroundClasses */ -class ModalPromos { +class PromoModals { recordPlusPromoSighting() { this._recordPromoSighting('plus'); } @@ -46,4 +46,4 @@ class ModalPromos { } // return the class as a singleton -export default new ModalPromos(); +export default new PromoModals(); From 43291469ab3570101a2555bc6935804d3154ed47 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 8 Oct 2019 12:34:26 -0400 Subject: [PATCH 21/39] Make PromoModals code more robust --- app/panel/components/Panel.jsx | 6 ++++-- src/classes/ConfData.js | 2 +- src/classes/PanelData.js | 3 ++- src/classes/PromoModals.js | 28 ++++++++++++++++++---------- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index f49c9b1e7..ea308e4e8 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -255,12 +255,14 @@ class Panel extends React.Component { const notificationText = this.props.notificationShown && this.renderNotification(); const { plusPromoModalShown } = this.state; - const { isTimeForAnotherPlusPromo } = this.props; + const { isTimeForAPlusPromo } = this.props; + + const show = !plusPromoModalShown && isTimeForAPlusPromo; return (
    diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 1b4de2764..67c5d3ee2 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -124,7 +124,7 @@ class ConfData { _initProperty('notify_library_updates', false); _initProperty('notify_upgrade_updates', true); _initProperty('paid_subscription', false); - _initProperty('plus_promo_modal_last_seen', 0); + _initProperty('plus_promo_modal_last_seen', null); _initProperty('rewards_accepted', false); _initProperty('rewards_opted_in', false); _initProperty('settings_last_imported', 0); diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 65fb55026..8b95d8f8c 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -366,7 +366,8 @@ class PanelData { is_expert, is_android: globals.BROWSER_INFO.os === 'android', language, - isTimeForAnotherPlusPromo: modalPromos.isTimeForAnotherPlusPromo(), + isTimeForAPlusPromo: modalPromos.isTimeForAPlusPromo(), + haveSeenAPlusPromo: modalPromos.haveSeenAPlusPromo(), reload_banner_status, tab_id, trackers_banner_status, diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index b8650c9fb..f0ae5760b 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -18,30 +18,38 @@ const DAYS_BETWEEN_PROMOS = { }; const MSECS_IN_DAY = 1000 * 60 * 60 * 24; // msecs-in-sec * secs-in-min * mins-in-hour * hours-in-day +const PLUS = 'plus'; +const PROMO_MODAL_LAST_SEEN = 'promo_modal_last_seen'; + /** * Class for handling the business logic for the display of promo modals (Plus, Insights, etc...) * @memberOf BackgroundClasses */ class PromoModals { - recordPlusPromoSighting() { - this._recordPromoSighting('plus'); - } + haveSeenAPlusPromo() { return this._haveSeenAPromo(PLUS); } + + isTimeForAPlusPromo() { return this._isTimeForAPromo(PLUS); } - isTimeForAnotherPlusPromo() { - return this._isTimeForAnotherPlusPromo('plus'); + recordPlusPromoSighting() { this._recordPromoSighting(PLUS); } + + _haveSeenAPromo(type) { + const lastSeenTime = conf[`${type}_${PROMO_MODAL_LAST_SEEN}`]; + return (lastSeenTime !== null); } - _isTimeForAnotherPlusPromo(promoType) { - const lastSeenTime = conf[`${promoType}_promo_modal_last_seen`]; + _isTimeForAPromo(type) { + const lastSeenTime = conf[`${type}_${PROMO_MODAL_LAST_SEEN}`]; + + if (lastSeenTime === null) { return true; } return ( (Date.now() - lastSeenTime) > - (MSECS_IN_DAY * DAYS_BETWEEN_PROMOS[promoType]) + (MSECS_IN_DAY * DAYS_BETWEEN_PROMOS[type]) ); } - _recordPromoSighting(promoType) { - conf[`${promoType}_promo_modal_last_seen`] = Date.now(); + _recordPromoSighting(type) { + conf[`${type}_${PROMO_MODAL_LAST_SEEN}`] = Date.now(); } } From 66c1490505af56e2c7b36892d8bbe1fe60c711e3 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 8 Oct 2019 12:43:22 -0400 Subject: [PATCH 22/39] Factor PlusPromoModal rendering out to helper in Panel. Extend conditional rendering logic to account for upgrade version of modal --- app/panel/components/Panel.jsx | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index ea308e4e8..28f6bf74a 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -242,6 +242,24 @@ class Panel extends React.Component { }); } + _renderPlusPromoModal = () => { + const { plusPromoModalShown } = this.state; + const { haveSeenAPlusPromo, isTimeForAPlusPromo } = this.props; + + if (plusPromoModalShown || !isTimeForAPlusPromo) return null; + + const version = haveSeenAPlusPromo ? 'upgrade' : 'initial'; + + return ( + + ); + } + /** * React's required render function. Returns JSX * @return {JSX} JSX for rendering the Panel @@ -254,18 +272,9 @@ class Panel extends React.Component { const notificationText = this.props.notificationShown && this.renderNotification(); - const { plusPromoModalShown } = this.state; - const { isTimeForAPlusPromo } = this.props; - - const show = !plusPromoModalShown && isTimeForAPlusPromo; - return (
    - + {this._renderPlusPromoModal()}
    From bb5d74d0e7f5b14c56cbacdf94d17cd0fec00840 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Tue, 8 Oct 2019 15:48:58 -0400 Subject: [PATCH 23/39] Mark PromoModals methods as static. Refine PlusPromoModal implementation and add debug code --- app/hub/Views/HomeView/HomeViewContainer.jsx | 1 + app/panel/components/Panel.jsx | 4 +- .../PlusPromoModal/PlusPromoModal.jsx | 57 +++++++++++++++++-- src/classes/PanelData.js | 6 +- src/classes/PromoModals.js | 29 +++++----- 5 files changed, 71 insertions(+), 26 deletions(-) diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index 42bf75371..a13d98f63 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -92,6 +92,7 @@ class HomeViewContainer extends Component { show={!isPlus && !plus_promo_modal_shown} location="hub" clickHandler={this._dismissModal} + version={PlusPromoModal.INITIAL} />
    diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 28f6bf74a..f66f11b3d 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -244,11 +244,11 @@ class Panel extends React.Component { _renderPlusPromoModal = () => { const { plusPromoModalShown } = this.state; - const { haveSeenAPlusPromo, isTimeForAPlusPromo } = this.props; + const { haveSeenInitialPlusPromo, isTimeForAPlusPromo } = this.props; if (plusPromoModalShown || !isTimeForAPlusPromo) return null; - const version = haveSeenAPlusPromo ? 'upgrade' : 'initial'; + const version = haveSeenInitialPlusPromo ? PlusPromoModal.UPGRADE : PlusPromoModal.INITIAL; return ( { +// TODO refactor to reduce duplication alongside implementing _renderUpgradeVersion for GH-1813 +function _renderInitialVersion(props) { const { show, location, clickHandler } = props; const isInHub = location === 'hub'; @@ -126,13 +122,62 @@ const PlusPromoModal = (props) => {
    ); +} + +// TODO flesh out this stub for https://cliqztix.atlassian.net/browse/GH-1813 +function _renderUpgradeVersion(props) { + const { clickHandler, location, show } = props; + + const isInHub = location === 'hub'; + + const locationClassName = { + 'in-hub': isInHub, + 'in-panel': location === 'panel' + }; + const contentClassNames = ClassNames( + 'PlusPromoModal__content', + 'flex-container', + 'flex-dir-column', + 'align-middle', + locationClassName + ); + + return ( + +
    +
    [Upgrade version of the Plus Promo modal]
    +
    + Dismiss +
    +
    +
    + ); +} + +/** + * A Functional React component for a Plus Promo Modal + * @return {JSX} JSX for rendering a Plus Promo Modal + * @memberof SharedComponents + */ +const PlusPromoModal = (props) => { + const { version } = props; + + if (version === PlusPromoModal.INITIAL) { return _renderInitialVersion(props); } + + if (version === PlusPromoModal.UPGRADE) { return _renderUpgradeVersion(props); } + + return null; }; +PlusPromoModal.UPGRADE = 1; +PlusPromoModal.INITIAL = 2; + // PropTypes ensure we pass required props of the correct type PlusPromoModal.propTypes = { show: PropTypes.bool.isRequired, location: PropTypes.string.isRequired, clickHandler: PropTypes.func.isRequired, + version: PropTypes.oneOf([PlusPromoModal.UPGRADE, PlusPromoModal.INITIAL]).isRequired, }; export default PlusPromoModal; diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 8b95d8f8c..427301a18 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -27,7 +27,7 @@ import tabInfo from './TabInfo'; import rewards from './Rewards'; import account from './Account'; import dispatcher from './Dispatcher'; -import modalPromos from './PromoModals'; +import promoModals from './PromoModals'; import { getCliqzGhosteryBugs, sendCliqzModuleCounts } from '../utils/cliqzModulesData'; import { getActiveTab, flushChromeMemoryCache, processUrl } from '../utils/utils'; import { objectEntries, log } from '../utils/common'; @@ -366,8 +366,8 @@ class PanelData { is_expert, is_android: globals.BROWSER_INFO.os === 'android', language, - isTimeForAPlusPromo: modalPromos.isTimeForAPlusPromo(), - haveSeenAPlusPromo: modalPromos.haveSeenAPlusPromo(), + isTimeForAPlusPromo: promoModals.isTimeForAPlusPromo(), + haveSeenInitialPlusPromo: promoModals.haveSeenInitialPlusPromo(), reload_banner_status, tab_id, trackers_banner_status, diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index f0ae5760b..fd583954f 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -12,12 +12,12 @@ */ import conf from './Conf'; +import globals from './Globals'; const DAYS_BETWEEN_PROMOS = { - plus: 30 + plus: globals.DEBUG ? 0.00025 : 30, }; -const MSECS_IN_DAY = 1000 * 60 * 60 * 24; // msecs-in-sec * secs-in-min * mins-in-hour * hours-in-day - +const MSECS_IN_DAY = 86400000; // 1000 msecs-in-sec * 60 secs-in-min * 60 mins-in-hour * 24 hours-in-day const PLUS = 'plus'; const PROMO_MODAL_LAST_SEEN = 'promo_modal_last_seen'; @@ -26,18 +26,16 @@ const PROMO_MODAL_LAST_SEEN = 'promo_modal_last_seen'; * @memberOf BackgroundClasses */ class PromoModals { - haveSeenAPlusPromo() { return this._haveSeenAPromo(PLUS); } - - isTimeForAPlusPromo() { return this._isTimeForAPromo(PLUS); } - - recordPlusPromoSighting() { this._recordPromoSighting(PLUS); } - - _haveSeenAPromo(type) { - const lastSeenTime = conf[`${type}_${PROMO_MODAL_LAST_SEEN}`]; + static haveSeenInitialPlusPromo() { + const lastSeenTime = conf[`${PLUS}_${PROMO_MODAL_LAST_SEEN}`]; return (lastSeenTime !== null); } - _isTimeForAPromo(type) { + static isTimeForAPlusPromo() { return this._isTimeForAPromo(PLUS); } + + static recordPlusPromoSighting() { this._recordPromoSighting(PLUS); } + + static _isTimeForAPromo(type) { const lastSeenTime = conf[`${type}_${PROMO_MODAL_LAST_SEEN}`]; if (lastSeenTime === null) { return true; } @@ -48,10 +46,11 @@ class PromoModals { ); } - _recordPromoSighting(type) { + static _recordPromoSighting(type) { conf[`${type}_${PROMO_MODAL_LAST_SEEN}`] = Date.now(); } } -// return the class as a singleton -export default new PromoModals(); +// the class is simply a namespace for some static methods, +// as we do not need to maintain any state +export default PromoModals; From dd808b5ac9af0349e44c5149e7be26336df9e419 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Wed, 9 Oct 2019 10:12:12 -0400 Subject: [PATCH 24/39] Add logic to hide plus promo modal from Insights subscribers. Cleanup and comments. --- app/hub/Views/HomeView/HomeViewContainer.jsx | 9 ++++++--- app/panel/components/Panel.jsx | 12 +++++++++--- src/classes/PanelData.js | 2 +- src/classes/PromoModals.js | 3 ++- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index a13d98f63..7bf676826 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -59,10 +59,13 @@ class HomeViewContainer extends Component { /** * @private - * Dismisses the Plus promo modal when user clicks either the Select Plus or Select Basic button + * Function to handle clicks on Select Plus and Select Basic in the Plus Promo Modal */ - _dismissModal = () => { + _handlePlusPromoModalClicks = () => { + // GH-1777 + // we want to show the Plus Promo modal once per Hub visit - not every time the user returns to the Home view this.props.actions.markPlusPromoModalShown(); + sendMessage('SET_PLUS_PROMO_MODAL_SEEN', {}); } @@ -91,7 +94,7 @@ class HomeViewContainer extends Component { diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index f66f11b3d..02355d0f1 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -235,7 +235,8 @@ class Panel extends React.Component { return false; } - _plusPromoClickHandlerPlaceholder = () => { + _handlePlusPromoModalClicks = () => { + // TODO send appropriate metrics ping(s) for GH-1775 sendMessage('promoModals.sawPlusPromo', {}); this.setState({ plusPromoModalShown: true @@ -244,7 +245,10 @@ class Panel extends React.Component { _renderPlusPromoModal = () => { const { plusPromoModalShown } = this.state; - const { haveSeenInitialPlusPromo, isTimeForAPlusPromo } = this.props; + const { account, haveSeenInitialPlusPromo, isTimeForAPlusPromo } = this.props; + + if (account && account.user && account.user.subscriptionsPlus) return null; // don't show the promo to Plus subscribers! + if (account && account.user && account.user.scopes && account.user.scopes.includes('subscriptions:insights')) return null; // don't show the promo to Insights subscribers, either if (plusPromoModalShown || !isTimeForAPlusPromo) return null; @@ -254,7 +258,7 @@ class Panel extends React.Component { ); @@ -270,6 +274,8 @@ class Panel extends React.Component { return null; } + console.error('IVZ this.props in Panel#render:', this.props); + const notificationText = this.props.notificationShown && this.renderNotification(); return ( diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 427301a18..117c9e7f9 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -352,7 +352,7 @@ class PanelData { const { id: tab_id } = this._activeTab; const { current_theme, enable_ad_block, enable_anti_tracking, enable_smart_block, - enable_offers, is_expanded, is_expert, language, plus_promo_modal_last_seen, reload_banner_status, + enable_offers, is_expanded, is_expert, language, reload_banner_status, trackers_banner_status, } = conf; diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index fd583954f..ddcd04ed2 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -22,7 +22,7 @@ const PLUS = 'plus'; const PROMO_MODAL_LAST_SEEN = 'promo_modal_last_seen'; /** - * Class for handling the business logic for the display of promo modals (Plus, Insights, etc...) + * Static 'namespace' class for handling the business logic for the display of promo modals (Plus, Insights, etc...) * @memberOf BackgroundClasses */ class PromoModals { @@ -35,6 +35,7 @@ class PromoModals { static recordPlusPromoSighting() { this._recordPromoSighting(PLUS); } + // TODO integrate the Insights promo modal into the "has it been long enough since last modal?" logic here static _isTimeForAPromo(type) { const lastSeenTime = conf[`${type}_${PROMO_MODAL_LAST_SEEN}`]; From a143c186880afdfcb9163e0075e2b1564a8dc6a4 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Wed, 9 Oct 2019 11:42:28 -0400 Subject: [PATCH 25/39] Clean up. Finish string localization in PlusPromoModal --- _locales/en/messages.json | 15 ++++++++++--- app/panel/components/Stats.jsx | 4 ++-- .../PlusPromoModal/PlusPromoModal.jsx | 22 +++++++++---------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 25137c89f..620e8f61a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1878,9 +1878,6 @@ "panel_stats_menu_daily": { "message": "Daily" }, - "panel_stats_header_title": { - "message": "Historical Tracker Stats" - }, "panel_stats_header_title_monthly": { "message": "Historical Tracker Stats (Monthly Avg.)" }, @@ -2033,6 +2030,9 @@ "select_basic": { "message": "Select Basic" }, + "select_plus": { + "message": "Select Plus" + }, "recommended": { "message": "Recommended" }, @@ -2131,5 +2131,14 @@ }, "no_such_email_password_combo": { "message": "Looks like we can't find this email and password combo. Give it another shot." + }, + "all_basic_features_plus_COLON": { + "message": "All basic features, plus:" + }, + "historical_tracker_stats": { + "message": "Historical Tracker Stats" + }, + "new_color_themes": { + "message": "New Color Themes" } } diff --git a/app/panel/components/Stats.jsx b/app/panel/components/Stats.jsx index 117230c8f..b4028171b 100644 --- a/app/panel/components/Stats.jsx +++ b/app/panel/components/Stats.jsx @@ -106,13 +106,13 @@ class Stats extends React.Component { getSummaryTitle = (type) => { switch (type) { case 'cumulative': - return t('panel_stats_header_title'); + return t('historical_tracker_stats'); case 'monthly': return t('panel_stats_header_title_monthly'); case 'daily': return t('panel_stats_header_title_daily'); default: - return t('panel_stats_header_title'); + return t('historical_tracker_stats'); } } diff --git a/app/shared-components/PlusPromoModal/PlusPromoModal.jsx b/app/shared-components/PlusPromoModal/PlusPromoModal.jsx index fd0965a44..5eac29713 100644 --- a/app/shared-components/PlusPromoModal/PlusPromoModal.jsx +++ b/app/shared-components/PlusPromoModal/PlusPromoModal.jsx @@ -17,7 +17,6 @@ import PropTypes from 'prop-types'; import ClassNames from 'classnames'; import Modal from '../Modal/Modal'; -// TODO refactor to reduce duplication alongside implementing _renderUpgradeVersion for GH-1813 function _renderInitialVersion(props) { const { show, location, clickHandler } = props; @@ -52,6 +51,7 @@ function _renderInitialVersion(props) { locationClassName ); + // TODO refactor for clarity & concision alongside implementing _renderUpgradeVersion for GH-1813 return (
    @@ -91,31 +91,31 @@ function _renderInitialVersion(props) {
    {t('recommended')}
    -
    Ghostery Plus
    +
    {t('ghostery_plus')}
    {t('locale_appropriate_currency_icon')} {t('plus_monthly_subscription_price_number')} - per month + {t('per month')}
    -
    All basic features, plus:
    +
    {t('all_basic_features_plus_COLON')}
    -
    Historical Tracker Stats
    +
    {t('historical_tracker_stats')}
    -
    Priority Support
    +
    {t('priority_support')}
    -
    New Color Themes
    +
    {t('new_color_themes')}
    - Select Plus + {t('select_plus')}
    @@ -169,15 +169,15 @@ const PlusPromoModal = (props) => { return null; }; -PlusPromoModal.UPGRADE = 1; -PlusPromoModal.INITIAL = 2; +PlusPromoModal.INITIAL = 1; +PlusPromoModal.UPGRADE = 2; // PropTypes ensure we pass required props of the correct type PlusPromoModal.propTypes = { show: PropTypes.bool.isRequired, location: PropTypes.string.isRequired, clickHandler: PropTypes.func.isRequired, - version: PropTypes.oneOf([PlusPromoModal.UPGRADE, PlusPromoModal.INITIAL]).isRequired, + version: PropTypes.oneOf([PlusPromoModal.INITIAL, PlusPromoModal.UPGRADE]).isRequired, }; export default PlusPromoModal; From 3795b25fa6973c7fd013bf519938bdedd17dc6af Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 16 Oct 2019 12:04:43 -0400 Subject: [PATCH 26/39] Plug in insights modal into promo modal class --- app/panel/components/Panel.jsx | 59 +++++++++++++++++++--------------- src/background.js | 4 +++ src/classes/Metrics.js | 36 +++------------------ src/classes/PanelData.js | 1 + src/classes/PromoModals.js | 33 ++++++++++++++++++- 5 files changed, 74 insertions(+), 59 deletions(-) diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 02355d0f1..c6a10129d 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -28,17 +28,14 @@ class Panel extends React.Component { constructor(props) { super(props); this.state = { - hasEngagedFrequently: true + insightsPromoModalShown: false, + plusPromoModalShown: false }; // event bindings this.closeNotification = this.closeNotification.bind(this); this.clickReloadBanner = this.clickReloadBanner.bind(this); this.filterTrackers = this.filterTrackers.bind(this); - - this.state = { - plusPromoModalShown: false, - }; } /** @@ -61,10 +58,10 @@ class Panel extends React.Component { }); chrome.runtime.onMessage.addListener((request) => { - if (request.name === 'hasEngagedFrequently') { + if (request.name === 'showInsightsModal') { this.setState({ ...this.state, - hasEngagedFrequently: true + insightsPromoModalShown: true }); } }); @@ -81,24 +78,12 @@ class Panel extends React.Component { * Function to toggle the Modal */ toggleModal = () => { - const { hasEngagedFrequently } = this.state; + const { insightsPromoModalShown } = this.state; this.setState({ - hasEngagedFrequently: !hasEngagedFrequently + insightsPromoModalShown: !insightsPromoModalShown }); } - generateModal = () => { - const { loggedIn, user } = this.props; - const { hasEngagedFrequently } = this.state; - if (!loggedIn && hasEngagedFrequently) { - return true; - } - - const isInsightsSubscriber = (user && user.scopes != null) ? user.scopes.includes('subscriptions:insights') : false; - return hasEngagedFrequently && loggedIn && isInsightsSubscriber; - } - - /** * Reload the current tab * @param {Object} event @@ -235,9 +220,11 @@ class Panel extends React.Component { return false; } - _handlePlusPromoModalClicks = () => { + _handlePlusPromoModalClicks = (version) => { // TODO send appropriate metrics ping(s) for GH-1775 - sendMessage('promoModals.sawPlusPromo', {}); + if (version === PlusPromoModal.UPGRADE) { + sendMessage('promoModals.sawPlusPromo', {}); + } this.setState({ plusPromoModalShown: true }); @@ -251,19 +238,39 @@ class Panel extends React.Component { if (account && account.user && account.user.scopes && account.user.scopes.includes('subscriptions:insights')) return null; // don't show the promo to Insights subscribers, either if (plusPromoModalShown || !isTimeForAPlusPromo) return null; - const version = haveSeenInitialPlusPromo ? PlusPromoModal.UPGRADE : PlusPromoModal.INITIAL; return ( ); } + _renderInsightsPromoModal = () => { + const { account } = this.props; + const { insightsPromoModalShown } = this.state; // might have to refactor to redux + + if (!insightsPromoModalShown) return null; + if (account && account.user && account.user.scopes && account.user.scopes.includes('subscriptions:insights')) return null; // don't show the promo to Insights subscribers, either + + // send message here that you did in fact see the insights modal + // figure out structure of modal class + + return ( + + ); + + // send message here that you did in fact see the insights modal + // figure out structure of modal class + } + /** * React's required render function. Returns JSX * @return {JSX} JSX for rendering the Panel @@ -281,6 +288,7 @@ class Panel extends React.Component { return (
    {this._renderPlusPromoModal()} + {this._renderInsightsPromoModal()}
    @@ -294,7 +302,6 @@ class Panel extends React.Component {
    - { this.props.children } diff --git a/src/background.js b/src/background.js index c1f9b4683..0ee925611 100644 --- a/src/background.js +++ b/src/background.js @@ -1082,6 +1082,10 @@ function onMessageHandler(request, sender, callback) { promoModals.recordPlusPromoSighting(); return true; } + if (name === 'promoModals.sawInsightsPromo') { + promoModals.recordInsightsPromoSighting(); + return true; + } } /** diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 772667449..92ace18dd 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -17,6 +17,7 @@ import { log, prefsSet, prefsGet } from '../utils/common'; import { getActiveTab, processUrlQuery } from '../utils/utils'; import rewards from './Rewards'; import { sendMessage } from '../../app/panel/utils/msg'; +import PromoModals from './PromoModals'; // import getUserSubscriptionData from './Account'; // CONSTANTS @@ -778,41 +779,12 @@ class Metrics { const today = Math.floor(Number(new Date().getTime()) / 86400000); // Today's time engaged_daily_velocity_with_repeats.push(today); conf.metrics.engaged_daily_velocity_with_repeats = engaged_daily_velocity_with_repeats; - console.log('Accessing engaged_daily_velocity_with_repeats after pushing today: ', engaged_daily_velocity_with_repeats); - if (this._hasEngagedFrequently()) { - sendMessage('hasEngagedFrequently', '', 'metrics'); + if (PromoModals.isTimeForInsightsPromo()) { + sendMessage('promoModals.sawInsightsPromo', '', 'metrics'); + sendMessage('showInsightsModal', '', 'metrics'); } } - /** - * Toggle the insights promotion if a user has opened the panel 3 times per day for at least 3 days in the past 7 days - * @private - */ - _hasEngagedFrequently = () => { - const today = new Date().getTime(); - const THIRTY_DAYS = 1000 * 60 * 60 * 24 * 30; - const insights_promo_modal_last_seen = Number(conf.insights_promo_modal_last_seen) || null; // TODO: Add logic for plus_promotion_last_seen - const hasSeenPromotionInPastMonth = today - insights_promo_modal_last_seen <= THIRTY_DAYS; - if (!hasSeenPromotionInPastMonth) { - const { engaged_daily_velocity_with_repeats } = conf.metrics; - const pastSevenDays = Array.from(new Set(engaged_daily_velocity_with_repeats)); - let timesPerWeek = 0; - - for (let i = 0; i < pastSevenDays.length; i++) { - const engagementsEachDay = engaged_daily_velocity_with_repeats.filter(day => day === pastSevenDays[i]).length; - if (engagementsEachDay >= 3) { - timesPerWeek++; - } - } - - if (timesPerWeek >= 3) { - conf.insights_promo_modal_last_seen = today; - return true; - } - } - return false; - } - /** * Repeat sending active request every month * if computer is continuously on. diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 117c9e7f9..5b8025e75 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -366,6 +366,7 @@ class PanelData { is_expert, is_android: globals.BROWSER_INFO.os === 'android', language, + isTimeForInsightsPromo: promoModals.isTimeForInsightsPromo(), isTimeForAPlusPromo: promoModals.isTimeForAPlusPromo(), haveSeenInitialPlusPromo: promoModals.haveSeenInitialPlusPromo(), reload_banner_status, diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index ddcd04ed2..e1266db5c 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -19,6 +19,7 @@ const DAYS_BETWEEN_PROMOS = { }; const MSECS_IN_DAY = 86400000; // 1000 msecs-in-sec * 60 secs-in-min * 60 mins-in-hour * 24 hours-in-day const PLUS = 'plus'; +const INSIGHTS = 'insights'; const PROMO_MODAL_LAST_SEEN = 'promo_modal_last_seen'; /** @@ -33,14 +34,24 @@ class PromoModals { static isTimeForAPlusPromo() { return this._isTimeForAPromo(PLUS); } + static isTimeForInsightsPromo() { return this._isTimeForAPromo(INSIGHTS); } + static recordPlusPromoSighting() { this._recordPromoSighting(PLUS); } + static recordInsightsModalSighting() { this._recordPromoSighting(INSIGHTS); } + // TODO integrate the Insights promo modal into the "has it been long enough since last modal?" logic here static _isTimeForAPromo(type) { - const lastSeenTime = conf[`${type}_${PROMO_MODAL_LAST_SEEN}`]; + const lastSeenPlusPromo = conf[`${PLUS}_${PROMO_MODAL_LAST_SEEN}`]; + const lastSeenInsightsPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; + const lastSeenTime = Math.max(lastSeenPlusPromo, lastSeenInsightsPromo); if (lastSeenTime === null) { return true; } + if (type === INSIGHTS && !this._hasEngagedFrequently()) { + return false; + } + return ( (Date.now() - lastSeenTime) > (MSECS_IN_DAY * DAYS_BETWEEN_PROMOS[type]) @@ -50,6 +61,26 @@ class PromoModals { static _recordPromoSighting(type) { conf[`${type}_${PROMO_MODAL_LAST_SEEN}`] = Date.now(); } + + static _hasEngagedFrequently() { + const today = new Date().getTime(); + const { engaged_daily_velocity_with_repeats } = conf.metrics; + const pastSevenDays = Array.from(new Set(engaged_daily_velocity_with_repeats)); + let timesPerWeek = 0; + + for (let i = 0; i < pastSevenDays.length; i++) { + const engagementsEachDay = engaged_daily_velocity_with_repeats.filter(day => day === pastSevenDays[i]).length; + if (engagementsEachDay >= 3) { + timesPerWeek++; + } + } + + if (timesPerWeek >= 3) { + conf.insights_promo_modal_last_seen = today; + return true; + } + return false; + } } // the class is simply a namespace for some static methods, From cd467c851f2416e9c6fd52a49254a577840f2dc6 Mon Sep 17 00:00:00 2001 From: Ilya Zarembsky Date: Wed, 16 Oct 2019 15:30:25 -0400 Subject: [PATCH 27/39] GH-1777 and GH-1776 Plus Promo modals (#458) * Show plus promo modal on first view of Home view of each Hub session * Stub out intro hub plus promo modal layout and implement its buttons * Remove some unnecessary plus promo modal related CSS * Continue adding styling for plus promo modal * Continue styling plus promo modal * Refactor plus promo modal CSS to improve alignment of elements * Continue CSS work for plus promo modal * Implement recommended gold banner in plus promo modal * Update and tweak plus promo modal design * Fix path bug in i18n-checker tool. Consolidate redundant entries in messages. Add check icon to plus promo modal. Modify plus promo modal css. * Additional consolidation of redundant and unused entries in messages * Factor plus promo modal rendering out to shared component. Implement rendering in panel. Implement display context CSS adjustments. * Move Plus Promo modal rendering to a PlusPromoModal shared component. Begin to implement conditional panel display logic * Create ModalPromos background class responsible for modal promo related business logic and state management * Add local state to Panel to make sure component rerenders after user dismisses promo panel. Send record of promo interaction to background * Make PromoModals code more robust * Factor PlusPromoModal rendering out to helper in Panel. Extend conditional rendering logic to account for upgrade version of modal * Mark PromoModals methods as static. Refine PlusPromoModal implementation and add debug code * Add logic to hide plus promo modal from Insights subscribers. Cleanup and comments. * Clean up. Finish string localization in PlusPromoModal * Pull non-shared upgrade version of plus promo modal out of shared PlusPromoModal component and into Panel to simplify * Remove debug console statement --- app/hub/Views/HomeView/HomeViewContainer.jsx | 1 - app/panel/components/Panel.jsx | 61 ++++++++++--------- .../PlusPromoModal/PlusPromoModal.jsx | 44 +------------ 3 files changed, 33 insertions(+), 73 deletions(-) diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index 7bf676826..fd11d3081 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -95,7 +95,6 @@ class HomeViewContainer extends Component { show={!isPlus && !plus_promo_modal_shown} location="hub" clickHandler={this._handlePlusPromoModalClicks} - version={PlusPromoModal.INITIAL} />
    diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index c6a10129d..237402305 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -12,14 +12,13 @@ */ import React from 'react'; -import { NavLink } from 'react-router-dom'; +import ClassNames from 'classnames'; import Header from '../containers/HeaderContainer'; import { PlusPromoModal } from '../../shared-components'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; import { InsightsPromoModal } from '../../shared-components'; -import { login } from '../../Account/AccountActions'; /** * @class Implement base view with functionality common to all views. * @memberof PanelClasses @@ -36,6 +35,10 @@ class Panel extends React.Component { this.closeNotification = this.closeNotification.bind(this); this.clickReloadBanner = this.clickReloadBanner.bind(this); this.filterTrackers = this.filterTrackers.bind(this); + + this.state = { + plusPromoModalShown: false, + }; } /** @@ -220,16 +223,35 @@ class Panel extends React.Component { return false; } - _handlePlusPromoModalClicks = (version) => { + _handlePlusPromoModalClicks = () => { // TODO send appropriate metrics ping(s) for GH-1775 - if (version === PlusPromoModal.UPGRADE) { - sendMessage('promoModals.sawPlusPromo', {}); - } + sendMessage('promoModals.sawPlusPromo', {}); this.setState({ plusPromoModalShown: true }); } + _renderPlusPromoUpgradeModal() { + const contentClassNames = ClassNames( + 'PlusPromoModal__content', + 'flex-container', + 'flex-dir-column', + 'align-middle', + 'panel' + ); + + return ( + +
    +
    [Upgrade version of the Plus Promo modal]
    +
    + Dismiss +
    +
    +
    + ); + } + _renderPlusPromoModal = () => { const { plusPromoModalShown } = this.state; const { account, haveSeenInitialPlusPromo, isTimeForAPlusPromo } = this.props; @@ -238,37 +260,18 @@ class Panel extends React.Component { if (account && account.user && account.user.scopes && account.user.scopes.includes('subscriptions:insights')) return null; // don't show the promo to Insights subscribers, either if (plusPromoModalShown || !isTimeForAPlusPromo) return null; + const version = haveSeenInitialPlusPromo ? PlusPromoModal.UPGRADE : PlusPromoModal.INITIAL; + if (haveSeenInitialPlusPromo) { return this._renderPlusPromoUpgradeModal(); } + return ( - ); - } - - _renderInsightsPromoModal = () => { - const { account } = this.props; - const { insightsPromoModalShown } = this.state; // might have to refactor to redux - - if (!insightsPromoModalShown) return null; - if (account && account.user && account.user.scopes && account.user.scopes.includes('subscriptions:insights')) return null; // don't show the promo to Insights subscribers, either - - // send message here that you did in fact see the insights modal - // figure out structure of modal class - - return ( - ); - - // send message here that you did in fact see the insights modal - // figure out structure of modal class } /** diff --git a/app/shared-components/PlusPromoModal/PlusPromoModal.jsx b/app/shared-components/PlusPromoModal/PlusPromoModal.jsx index 5eac29713..4b55a6bba 100644 --- a/app/shared-components/PlusPromoModal/PlusPromoModal.jsx +++ b/app/shared-components/PlusPromoModal/PlusPromoModal.jsx @@ -124,60 +124,18 @@ function _renderInitialVersion(props) { ); } -// TODO flesh out this stub for https://cliqztix.atlassian.net/browse/GH-1813 -function _renderUpgradeVersion(props) { - const { clickHandler, location, show } = props; - - const isInHub = location === 'hub'; - - const locationClassName = { - 'in-hub': isInHub, - 'in-panel': location === 'panel' - }; - const contentClassNames = ClassNames( - 'PlusPromoModal__content', - 'flex-container', - 'flex-dir-column', - 'align-middle', - locationClassName - ); - - return ( - -
    -
    [Upgrade version of the Plus Promo modal]
    -
    - Dismiss -
    -
    -
    - ); -} - /** * A Functional React component for a Plus Promo Modal * @return {JSX} JSX for rendering a Plus Promo Modal * @memberof SharedComponents */ -const PlusPromoModal = (props) => { - const { version } = props; - - if (version === PlusPromoModal.INITIAL) { return _renderInitialVersion(props); } - - if (version === PlusPromoModal.UPGRADE) { return _renderUpgradeVersion(props); } - - return null; -}; - -PlusPromoModal.INITIAL = 1; -PlusPromoModal.UPGRADE = 2; +const PlusPromoModal = props => _renderInitialVersion(props); // PropTypes ensure we pass required props of the correct type PlusPromoModal.propTypes = { show: PropTypes.bool.isRequired, location: PropTypes.string.isRequired, clickHandler: PropTypes.func.isRequired, - version: PropTypes.oneOf([PlusPromoModal.INITIAL, PlusPromoModal.UPGRADE]).isRequired, }; export default PlusPromoModal; From 178ebf0925cc564e0033f8bab32550ae94dd7a2a Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 18 Oct 2019 16:38:41 -0400 Subject: [PATCH 28/39] Consolidate insights promo with plus promo --- app/panel/components/Login.jsx | 2 +- app/panel/components/Panel.jsx | 28 +++++++++++----- app/panel/components/Summary.jsx | 33 ++++++++++++++----- .../InsightsPromoModal/InsightsPromoModal.jsx | 2 +- src/classes/Metrics.js | 9 ----- src/classes/PanelData.js | 3 +- src/classes/PromoModals.js | 12 +++---- 7 files changed, 53 insertions(+), 36 deletions(-) diff --git a/app/panel/components/Login.jsx b/app/panel/components/Login.jsx index 428fa5a7b..0c63b4205 100644 --- a/app/panel/components/Login.jsx +++ b/app/panel/components/Login.jsx @@ -75,7 +75,7 @@ class Login extends React.Component { this.setState({ loading: false }, () => { history.push({ pathname: this.props.is_expert ? '/detail/blocking' : '/', - state: { showInsightsPromoModal: true } + state: { isInsightsModalHidden: false } }); }); }); diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 237402305..a4db6835b 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -28,6 +28,8 @@ class Panel extends React.Component { super(props); this.state = { insightsPromoModalShown: false, + shouldRepopInsightsModal: false, + isInsightsModalHidden: false, plusPromoModalShown: false }; @@ -35,10 +37,6 @@ class Panel extends React.Component { this.closeNotification = this.closeNotification.bind(this); this.clickReloadBanner = this.clickReloadBanner.bind(this); this.filterTrackers = this.filterTrackers.bind(this); - - this.state = { - plusPromoModalShown: false, - }; } /** @@ -81,9 +79,9 @@ class Panel extends React.Component { * Function to toggle the Modal */ toggleModal = () => { - const { insightsPromoModalShown } = this.state; + const { isInsightsModalHidden } = this.state; this.setState({ - insightsPromoModalShown: !insightsPromoModalShown + isInsightsModalHidden: !isInsightsModalHidden }); } @@ -261,8 +259,6 @@ class Panel extends React.Component { if (plusPromoModalShown || !isTimeForAPlusPromo) return null; - const version = haveSeenInitialPlusPromo ? PlusPromoModal.UPGRADE : PlusPromoModal.INITIAL; - if (haveSeenInitialPlusPromo) { return this._renderPlusPromoUpgradeModal(); } return ( @@ -274,6 +270,22 @@ class Panel extends React.Component { ); } + _renderInsightsPromoModal = () => { + const { account, isTimeForInsightsPromo } = this.props; + const { insightsPromoModalShown } = this.state; + if (insightsPromoModalShown || !isTimeForInsightsPromo) return null; + if (account && account.user && account.user.scopes && account.user.scopes.includes('subscriptions:insights')) return null; + + sendMessage('promoModals.sawInsightsPromo', '', 'metrics'); + + return ( + + ); + } + /** * React's required render function. Returns JSX * @return {JSX} JSX for rendering the Panel diff --git a/app/panel/components/Summary.jsx b/app/panel/components/Summary.jsx index d54ad73fb..f3a6076cc 100644 --- a/app/panel/components/Summary.jsx +++ b/app/panel/components/Summary.jsx @@ -48,7 +48,7 @@ class Summary extends React.Component { this.state = { trackerLatencyTotal: 0, disableBlocking: false, - showInsightsPromoModal: false + isInsightsModalHidden: true }; // Event Bindings @@ -82,7 +82,7 @@ class Summary extends React.Component { this._dynamicUIPort.onMessage.addListener(this.handlePortMessage); this._dynamicUIPort.postMessage({ name: 'SummaryComponentDidMount' }); - const showModal = (this.props.location.state && this.props.location.state.showInsightsPromoModal) !== undefined ? this.props.location.state.showInsightsPromoModal : false; + const showModal = (this.props.location.state && !this.props.location.state.isInsightsModalHidden) !== undefined ? !this.props.location.state.isInsightsModalHidden : false; const isInsightsSubscriber = (this.props.user && this.props.user.scopes != null) ? this.props.user.scopes.includes('subscriptions:insights') : false; if (!isInsightsSubscriber && showModal) { @@ -113,9 +113,11 @@ class Summary extends React.Component { * Function to toggle the Modal */ toggleModal = () => { - const { showInsightsPromoModal } = this.state; + const { isInsightsModalHidden } = this.state; + console.log('toggling summary modal'); + console.log('this.summary.props: ', this.props); this.setState({ - showInsightsPromoModal: !showInsightsPromoModal + isInsightsModalHidden: !isInsightsModalHidden }); } @@ -795,6 +797,23 @@ class Summary extends React.Component { ); } + _renderInsightsPromoModal = () => { + const { account, isTimeForInsightsPromo } = this.props; + const { insightsPromoModalShown, isInsightsModalHidden } = this.state; + + if (insightsPromoModalShown || !isTimeForInsightsPromo) return null; + if (account && account.user && account.user.scopes && account.user.scopes.includes('subscriptions:insights')) return null; // don't show the promo to Insights subscribers, either + + sendMessage('promoModals.sawInsightsPromo', '', 'metrics'); + + return ( + + ); + } + /** * React's required render function. Returns JSX * @return {JSX} JSX for rendering the Summary View of the panel @@ -813,13 +832,9 @@ class Summary extends React.Component { 'Summary--condensed': isCondensed, }); - // console.log('history: ', this.props.location.state); - return ( <> - - - + {this._renderInsightsPromoModal()}
    {!isCondensed && disableBlocking && ()} {!isCondensed && !disableBlocking && this._renderDonut()} diff --git a/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx b/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx index 23b1dd476..417699f3e 100644 --- a/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx +++ b/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx @@ -22,7 +22,7 @@ const InsightsPromoModal = ({ show, toggleModal }) => { const clickSignIn = () => { history.push({ pathname: '/login', - state: { showInsightsPromoModal: true } + state: { isInsightsModalHidden: false } }); toggleModal(); }; diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 92ace18dd..c07735343 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -766,11 +766,6 @@ class Metrics { } /** - * Record Engaged event multiple times in a day - * TODO: Save engaged_daily_velocity_with_repeats to chrome extension storage. - * Current Result: engaged_daily_velocity_with_repeats is saved inside the chrome extension storage, but always is an array with 1 element (today). - * This suggests that conf.metrics.engaged_daily_velocity_with_repeats is an empty array in the beginning, but console.logs are showing correct logic. - * What I have tried: JSON.stringifying the object, using ES6 spread instead of the push since setting an array directly seems to work, checking to see if conf.metrics.engaged_daily_velocity_with_repeats is undefined * @private */ _recordEngagedWithRepeats() { @@ -779,10 +774,6 @@ class Metrics { const today = Math.floor(Number(new Date().getTime()) / 86400000); // Today's time engaged_daily_velocity_with_repeats.push(today); conf.metrics.engaged_daily_velocity_with_repeats = engaged_daily_velocity_with_repeats; - if (PromoModals.isTimeForInsightsPromo()) { - sendMessage('promoModals.sawInsightsPromo', '', 'metrics'); - sendMessage('showInsightsModal', '', 'metrics'); - } } /** diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 5b8025e75..889b3db88 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -367,7 +367,8 @@ class PanelData { is_android: globals.BROWSER_INFO.os === 'android', language, isTimeForInsightsPromo: promoModals.isTimeForInsightsPromo(), - isTimeForAPlusPromo: promoModals.isTimeForAPlusPromo(), + // isTimeForAPlusPromo: promoModals.isTimeForAPlusPromo(), + isTimeForAPlusPromo: false, haveSeenInitialPlusPromo: promoModals.haveSeenInitialPlusPromo(), reload_banner_status, tab_id, diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index e1266db5c..bb696e0d8 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -16,6 +16,7 @@ import globals from './Globals'; const DAYS_BETWEEN_PROMOS = { plus: globals.DEBUG ? 0.00025 : 30, + insights: globals.DEBUG ? 0.00025 : 30 }; const MSECS_IN_DAY = 86400000; // 1000 msecs-in-sec * 60 secs-in-min * 60 mins-in-hour * 24 hours-in-day const PLUS = 'plus'; @@ -38,22 +39,21 @@ class PromoModals { static recordPlusPromoSighting() { this._recordPromoSighting(PLUS); } - static recordInsightsModalSighting() { this._recordPromoSighting(INSIGHTS); } + static recordInsightsPromoSighting() { this._recordPromoSighting(INSIGHTS); } - // TODO integrate the Insights promo modal into the "has it been long enough since last modal?" logic here static _isTimeForAPromo(type) { const lastSeenPlusPromo = conf[`${PLUS}_${PROMO_MODAL_LAST_SEEN}`]; const lastSeenInsightsPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; - const lastSeenTime = Math.max(lastSeenPlusPromo, lastSeenInsightsPromo); + const lastSeenPromo = Math.max(lastSeenPlusPromo, lastSeenInsightsPromo); - if (lastSeenTime === null) { return true; } + if (lastSeenPromo === null) { return true; } if (type === INSIGHTS && !this._hasEngagedFrequently()) { return false; } return ( - (Date.now() - lastSeenTime) > + (Date.now() - lastSeenPromo) > (MSECS_IN_DAY * DAYS_BETWEEN_PROMOS[type]) ); } @@ -63,7 +63,6 @@ class PromoModals { } static _hasEngagedFrequently() { - const today = new Date().getTime(); const { engaged_daily_velocity_with_repeats } = conf.metrics; const pastSevenDays = Array.from(new Set(engaged_daily_velocity_with_repeats)); let timesPerWeek = 0; @@ -76,7 +75,6 @@ class PromoModals { } if (timesPerWeek >= 3) { - conf.insights_promo_modal_last_seen = today; return true; } return false; From 4b7b84a6a65574531147d81cfd6bf4cadaad68c9 Mon Sep 17 00:00:00 2001 From: Benjamin Strumeyer Date: Fri, 18 Oct 2019 11:43:16 -0400 Subject: [PATCH 29/39] GH-1814/promo toggle (#453) * Add language and toggle for notify_promotions * Change label name --- _locales/en/messages.json | 3 +++ app/panel/components/Settings/Notifications.jsx | 6 ++++++ src/classes/ConfData.js | 1 + src/classes/Globals.js | 1 + src/classes/PanelData.js | 3 ++- 5 files changed, 13 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 620e8f61a..344314d05 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -901,6 +901,9 @@ "settings_new_features": { "message": "Releases new features" }, + "settings_new_promotions": { + "message": "Has product promotions" + }, "settings_new_trackers": { "message": "Adds new trackers to its tracker library" }, diff --git a/app/panel/components/Settings/Notifications.jsx b/app/panel/components/Settings/Notifications.jsx index 9569486d7..6b658001b 100644 --- a/app/panel/components/Settings/Notifications.jsx +++ b/app/panel/components/Settings/Notifications.jsx @@ -41,6 +41,12 @@ const Notifications = (props) => {
    +
    +
    + + +
    +
    diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 67c5d3ee2..8db34cacd 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -122,6 +122,7 @@ class ConfData { _initProperty('is_expert', false); _initProperty('last_cmp_date', 0); _initProperty('notify_library_updates', false); + _initProperty('notify_promotions', true); _initProperty('notify_upgrade_updates', true); _initProperty('paid_subscription', false); _initProperty('plus_promo_modal_last_seen', null); diff --git a/src/classes/Globals.js b/src/classes/Globals.js index ed481d1bd..900874ff8 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -114,6 +114,7 @@ class Globals { 'is_expanded', 'is_expert', 'notify_library_updates', + 'notify_promotions', 'notify_upgrade_updates', 'reload_banner_status', 'selected_app_ids', diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 889b3db88..666e280a9 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -512,7 +512,7 @@ class PanelData { alert_bubble_pos, alert_bubble_timeout, block_by_default, enable_autoupdate, enable_click2play, enable_click2play_social, enable_human_web, enable_offers, enable_metrics, hide_alert_trusted, ignore_first_party, notify_library_updates, - notify_upgrade_updates, selected_app_ids, show_alert, show_badge, + notify_promotions, notify_upgrade_updates, selected_app_ids, show_alert, show_badge, show_cmp, show_tracker_urls, toggle_individual_trackers } = userSettingsSource; @@ -529,6 +529,7 @@ class PanelData { hide_alert_trusted, ignore_first_party, notify_library_updates, + notify_promotions, notify_upgrade_updates, selected_app_ids, show_alert, From 7fb774408a0835925f96a097412c9ef91e2ff25b Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 18 Oct 2019 17:10:31 -0400 Subject: [PATCH 30/39] User can opt out of promotions --- app/panel/components/Panel.jsx | 5 +---- app/panel/components/Summary.jsx | 5 +---- src/classes/Metrics.js | 4 ---- src/classes/PanelData.js | 3 +-- src/classes/PromoModals.js | 1 + 5 files changed, 4 insertions(+), 14 deletions(-) diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index a4db6835b..b70663c2e 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -14,11 +14,10 @@ import React from 'react'; import ClassNames from 'classnames'; import Header from '../containers/HeaderContainer'; -import { PlusPromoModal } from '../../shared-components'; +import { PlusPromoModal, InsightsPromoModal, Modal } from '../../shared-components'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; -import { InsightsPromoModal } from '../../shared-components'; /** * @class Implement base view with functionality common to all views. * @memberof PanelClasses @@ -296,8 +295,6 @@ class Panel extends React.Component { return null; } - console.error('IVZ this.props in Panel#render:', this.props); - const notificationText = this.props.notificationShown && this.renderNotification(); return ( diff --git a/app/panel/components/Summary.jsx b/app/panel/components/Summary.jsx index f3a6076cc..fe50efb03 100644 --- a/app/panel/components/Summary.jsx +++ b/app/panel/components/Summary.jsx @@ -17,8 +17,7 @@ import ClassNames from 'classnames'; import Tooltip from './Tooltip'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; -import { Modal, InsightsPromoModal } from '../../shared-components'; -import history from '../utils/history'; +import { InsightsPromoModal } from '../../shared-components'; import globals from '../../../src/classes/Globals'; import { CliqzFeature, @@ -114,8 +113,6 @@ class Summary extends React.Component { */ toggleModal = () => { const { isInsightsModalHidden } = this.state; - console.log('toggling summary modal'); - console.log('this.summary.props: ', this.props); this.setState({ isInsightsModalHidden: !isInsightsModalHidden }); diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index c07735343..7307374d6 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -16,9 +16,6 @@ import conf from './Conf'; import { log, prefsSet, prefsGet } from '../utils/common'; import { getActiveTab, processUrlQuery } from '../utils/utils'; import rewards from './Rewards'; -import { sendMessage } from '../../app/panel/utils/msg'; -import PromoModals from './PromoModals'; -// import getUserSubscriptionData from './Account'; // CONSTANTS const FREQUENCIES = { // in milliseconds @@ -770,7 +767,6 @@ class Metrics { */ _recordEngagedWithRepeats() { const engaged_daily_velocity_with_repeats = conf.metrics.engaged_daily_velocity_with_repeats || []; - console.log('Accessing engaged_daily_velocity_with_repeats: ', engaged_daily_velocity_with_repeats); const today = Math.floor(Number(new Date().getTime()) / 86400000); // Today's time engaged_daily_velocity_with_repeats.push(today); conf.metrics.engaged_daily_velocity_with_repeats = engaged_daily_velocity_with_repeats; diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 666e280a9..4e005e1a5 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -367,8 +367,7 @@ class PanelData { is_android: globals.BROWSER_INFO.os === 'android', language, isTimeForInsightsPromo: promoModals.isTimeForInsightsPromo(), - // isTimeForAPlusPromo: promoModals.isTimeForAPlusPromo(), - isTimeForAPlusPromo: false, + isTimeForAPlusPromo: promoModals.isTimeForAPlusPromo(), haveSeenInitialPlusPromo: promoModals.haveSeenInitialPlusPromo(), reload_banner_status, tab_id, diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index bb696e0d8..48501b5dd 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -46,6 +46,7 @@ class PromoModals { const lastSeenInsightsPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; const lastSeenPromo = Math.max(lastSeenPlusPromo, lastSeenInsightsPromo); + if (conf.notify_promotions === false) { return false; } if (lastSeenPromo === null) { return true; } if (type === INSIGHTS && !this._hasEngagedFrequently()) { From 36977740c181c542b28cd65ed714ed6e29685919 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 18 Oct 2019 18:10:11 -0400 Subject: [PATCH 31/39] Remove empty SCSS class --- .../InsightsPromoModal/InsightsPromoModal.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss b/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss index 9eb6bf0f3..f2a79309e 100644 --- a/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss +++ b/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss @@ -112,7 +112,3 @@ font-size: 15px; color: $tundora; } - -.InsightsModal__call-to-action-container { - -} From 2abb7cd662c17bba728f44e64adf0bf657070662 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 18 Oct 2019 18:17:59 -0400 Subject: [PATCH 32/39] Add newlines --- app/panel/components/Panel.jsx | 1 + app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx | 1 + 2 files changed, 2 insertions(+) diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index b70663c2e..40db913ac 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -272,6 +272,7 @@ class Panel extends React.Component { _renderInsightsPromoModal = () => { const { account, isTimeForInsightsPromo } = this.props; const { insightsPromoModalShown } = this.state; + if (insightsPromoModalShown || !isTimeForInsightsPromo) return null; if (account && account.user && account.user.scopes && account.user.scopes.includes('subscriptions:insights')) return null; diff --git a/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx b/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx index 417699f3e..f95ebbe4e 100644 --- a/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx +++ b/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx @@ -26,6 +26,7 @@ const InsightsPromoModal = ({ show, toggleModal }) => { }); toggleModal(); }; + return (
    From b0aa51b4d32c897ab122acc5735eae187ab4e0e1 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 21 Oct 2019 10:42:03 -0400 Subject: [PATCH 33/39] Add hover effects --- app/panel/components/Panel.jsx | 2 -- .../InsightsPromoModal/InsightsPromoModal.scss | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index fd4d8843e..d3d308022 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -262,8 +262,6 @@ class Panel extends React.Component { if (plusPromoModalShown || !isTimeForAPlusPromo) return null; - const version = haveSeenInitialPlusPromo ? PlusPromoModal.UPGRADE : PlusPromoModal.INITIAL; - if (haveSeenInitialPlusPromo) { return this._renderPlusPromoUpgradeModal(); } return ( diff --git a/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss b/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss index f2a79309e..4488bf761 100644 --- a/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss +++ b/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss @@ -94,6 +94,10 @@ letter-spacing: .5px; color: $white; line-height: 36px; + &:hover { + background-image: linear-gradient(101deg, #000004, #02648C); + color: $white; + } } .InsightsModal__link { @@ -102,6 +106,7 @@ color: $tundora; &:hover { cursor: pointer; + color: #090909; } } From 8231933b76fc62048757ca1a7578711443ba928b Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 23 Oct 2019 17:08:28 -0400 Subject: [PATCH 34/39] Refactor insights promo to use redux. Remove summary component code. Remove redundant scss rules. --- app/images/panel/checked-circle-icon.svg | 14 +-- app/images/panel/insights-ribbon.svg | 59 +----------- app/panel/actions/PanelActions.js | 13 ++- .../BuildingBlocks}/ModalExitButton.jsx | 0 app/panel/components/BuildingBlocks/index.js | 4 +- app/panel/components/InsightsPromoModal.jsx | 87 +++++++++++++++++ app/panel/components/Login.jsx | 4 +- app/panel/components/Panel.jsx | 36 +------ app/panel/components/Summary.jsx | 95 ++++++------------- app/panel/constants/constants.js | 1 + .../containers/InsightsPromoModalContainer.js | 35 +++++++ app/panel/containers/PanelContainer.js | 2 +- app/panel/reducers/panel.js | 10 +- app/scss/panel.scss | 6 +- .../partials/_insights_promo_modal.scss} | 15 --- .../partials/_modal_exit_button.scss} | 0 .../InsightsPromoModal/InsightsPromoModal.jsx | 92 ------------------ .../InsightsPromoModal/index.js | 16 ---- .../ModalExitButton/index.js | 16 ---- app/shared-components/index.js | 6 +- src/classes/ConfData.js | 2 +- src/classes/Metrics.js | 4 +- src/classes/PanelData.js | 3 +- src/classes/PromoModals.js | 14 ++- 24 files changed, 204 insertions(+), 330 deletions(-) rename app/{shared-components/ModalExitButton => panel/components/BuildingBlocks}/ModalExitButton.jsx (100%) create mode 100644 app/panel/components/InsightsPromoModal.jsx create mode 100644 app/panel/containers/InsightsPromoModalContainer.js rename app/{shared-components/InsightsPromoModal/InsightsPromoModal.scss => scss/partials/_insights_promo_modal.scss} (84%) rename app/{shared-components/ModalExitButton/ModalExitButton.scss => scss/partials/_modal_exit_button.scss} (100%) delete mode 100644 app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx delete mode 100644 app/shared-components/InsightsPromoModal/index.js delete mode 100644 app/shared-components/ModalExitButton/index.js diff --git a/app/images/panel/checked-circle-icon.svg b/app/images/panel/checked-circle-icon.svg index 559e89380..493ad4055 100644 --- a/app/images/panel/checked-circle-icon.svg +++ b/app/images/panel/checked-circle-icon.svg @@ -1,13 +1 @@ - - - - 99F3B28B-E969-460E-887A-CDE6B30A5D8C - Created with sketchtool. - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/images/panel/insights-ribbon.svg b/app/images/panel/insights-ribbon.svg index 449008570..568714725 100644 --- a/app/images/panel/insights-ribbon.svg +++ b/app/images/panel/insights-ribbon.svg @@ -1,58 +1 @@ - - - - C5C3BC26-5012-417D-8D3B-1AEC48935928 - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/panel/actions/PanelActions.js b/app/panel/actions/PanelActions.js index 601f169e9..55618cf28 100644 --- a/app/panel/actions/PanelActions.js +++ b/app/panel/actions/PanelActions.js @@ -17,7 +17,8 @@ import { CLOSE_NOTIFICATION, TOGGLE_EXPERT, SET_THEME, - CLEAR_THEME + CLEAR_THEME, + TOGGLE_INSIGHTS_MODAL } from '../constants/constants'; import { sendMessageInPromise } from '../utils/msg'; @@ -98,3 +99,13 @@ export const getTheme = name => dispatch => ( } }) ); + +/** + * Triggered when the user signs in through the Insights modal into an account that does not have an insights subscription, prompting to re-display the modal, requiring a re-render + * @return {Object} + */ +export function toggleInsightsModal() { + return { + type: TOGGLE_INSIGHTS_MODAL, + }; +} diff --git a/app/shared-components/ModalExitButton/ModalExitButton.jsx b/app/panel/components/BuildingBlocks/ModalExitButton.jsx similarity index 100% rename from app/shared-components/ModalExitButton/ModalExitButton.jsx rename to app/panel/components/BuildingBlocks/ModalExitButton.jsx diff --git a/app/panel/components/BuildingBlocks/index.js b/app/panel/components/BuildingBlocks/index.js index e689a77de..3cb86a8ca 100644 --- a/app/panel/components/BuildingBlocks/index.js +++ b/app/panel/components/BuildingBlocks/index.js @@ -23,6 +23,7 @@ import PauseButton from './PauseButton'; import ToggleSlider from './ToggleSlider'; import RewardDetail from './RewardDetail'; import RewardListItem from './RewardListItem'; +import ModalExitButton from './ModalExitButton'; export { ClickOutside, @@ -33,5 +34,6 @@ export { PauseButton, ToggleSlider, RewardDetail, - RewardListItem + RewardListItem, + ModalExitButton }; diff --git a/app/panel/components/InsightsPromoModal.jsx b/app/panel/components/InsightsPromoModal.jsx new file mode 100644 index 000000000..f881a8bac --- /dev/null +++ b/app/panel/components/InsightsPromoModal.jsx @@ -0,0 +1,87 @@ +/** + * Insights Promo Modal Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import React from 'react'; +import Modal from '../../shared-components/Modal'; +import history from '../utils/history'; +import ModalExitButton from './BuildingBlocks/ModalExitButton'; + +// A Functional React component for a Modal +class InsightsPromoModal extends React.Component { + clickSignIn = () => { + history.push({ + pathname: '/login', + }); + this.props.actions.toggleInsightsModal(); + }; + + render() { + return ( + +
    + +
    +
    + {t('panel_insights_promotion_header')} +
    +
    + {t('panel_insights_promotion_description')} +
    +
    +
    +
    + +
    + { t('panel_insights_audit_tags') } +
    +
    +
    + +
    + { t('panel_insights_promotion_trace_poor_performance') } +
    +
    +
    +
    +
    + +
    + { t('panel_insights_promotion_watch_pings') } +
    +
    +
    + +
    + { t('panel_insights_promotion_explore_trends') } +
    +
    +
    +
    +
    + +
    + {t('subscribe_pitch_sign_in')} + {t('subscribe_pitch_no_thanks')} +
    +
    +
    + + ); + } +} + +export default InsightsPromoModal; diff --git a/app/panel/components/Login.jsx b/app/panel/components/Login.jsx index 0c63b4205..8243b1214 100644 --- a/app/panel/components/Login.jsx +++ b/app/panel/components/Login.jsx @@ -73,9 +73,9 @@ class Login extends React.Component { }) .finally(() => { this.setState({ loading: false }, () => { + this.props.actions.toggleInsightsModal(); history.push({ - pathname: this.props.is_expert ? '/detail/blocking' : '/', - state: { isInsightsModalHidden: false } + pathname: this.props.is_expert ? '/detail/blocking' : '/' }); }); }); diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index d3d308022..c835e73d1 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -14,7 +14,8 @@ import React from 'react'; import ClassNames from 'classnames'; import Header from '../containers/HeaderContainer'; -import { PlusPromoModal, InsightsPromoModal, Modal } from '../../shared-components'; +import { PlusPromoModal, Modal } from '../../shared-components'; +import InsightsPromoModal from '../containers/InsightsPromoModalContainer'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; @@ -27,8 +28,6 @@ class Panel extends React.Component { super(props); this.state = { insightsPromoModalShown: false, - shouldRepopInsightsModal: false, - isInsightsModalHidden: false, plusPromoModalShown: false }; @@ -36,10 +35,6 @@ class Panel extends React.Component { this.closeNotification = this.closeNotification.bind(this); this.clickReloadBanner = this.clickReloadBanner.bind(this); this.filterTrackers = this.filterTrackers.bind(this); - - this.state = { - plusPromoModalShown: false, - }; } /** @@ -60,15 +55,6 @@ class Panel extends React.Component { this.props.actions.updatePanelData(body); } }); - - chrome.runtime.onMessage.addListener((request) => { - if (request.name === 'showInsightsModal') { - this.setState({ - ...this.state, - insightsPromoModalShown: true - }); - } - }); } /** @@ -78,16 +64,6 @@ class Panel extends React.Component { this._dynamicUIPort.disconnect(); } - /** - * Function to toggle the Modal - */ - toggleModal = () => { - const { isInsightsModalHidden } = this.state; - this.setState({ - isInsightsModalHidden: !isInsightsModalHidden - }); - } - /** * Reload the current tab * @param {Object} event @@ -274,19 +250,17 @@ class Panel extends React.Component { } _renderInsightsPromoModal = () => { - const { account, isTimeForInsightsPromo } = this.props; + const { account, isTimeForInsightsPromo, isInsightsModalHidden } = this.props; const { insightsPromoModalShown } = this.state; + if (isInsightsModalHidden) return null; if (insightsPromoModalShown || !isTimeForInsightsPromo) return null; if (account && account.user && account.user.scopes && account.user.scopes.includes('subscriptions:insights')) return null; sendMessage('promoModals.sawInsightsPromo', '', 'metrics'); return ( - + ); } diff --git a/app/panel/components/Summary.jsx b/app/panel/components/Summary.jsx index fe50efb03..81fec0993 100644 --- a/app/panel/components/Summary.jsx +++ b/app/panel/components/Summary.jsx @@ -17,7 +17,6 @@ import ClassNames from 'classnames'; import Tooltip from './Tooltip'; import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; -import { InsightsPromoModal } from '../../shared-components'; import globals from '../../../src/classes/Globals'; import { CliqzFeature, @@ -47,7 +46,6 @@ class Summary extends React.Component { this.state = { trackerLatencyTotal: 0, disableBlocking: false, - isInsightsModalHidden: true }; // Event Bindings @@ -80,13 +78,6 @@ class Summary extends React.Component { this._dynamicUIPort = this.context; this._dynamicUIPort.onMessage.addListener(this.handlePortMessage); this._dynamicUIPort.postMessage({ name: 'SummaryComponentDidMount' }); - - const showModal = (this.props.location.state && !this.props.location.state.isInsightsModalHidden) !== undefined ? !this.props.location.state.isInsightsModalHidden : false; - const isInsightsSubscriber = (this.props.user && this.props.user.scopes != null) ? this.props.user.scopes.includes('subscriptions:insights') : false; - - if (!isInsightsSubscriber && showModal) { - this.toggleModal(); - } } /** @@ -108,16 +99,6 @@ class Summary extends React.Component { this._dynamicUIPort.onMessage.removeListener(this.handlePortMessage); } - /** - * Function to toggle the Modal - */ - toggleModal = () => { - const { isInsightsModalHidden } = this.state; - this.setState({ - isInsightsModalHidden: !isInsightsModalHidden - }); - } - /** * Handles clicking on Cliqz Features: AntiTracking, AdBlocking, SmartBlocking * @param {Object} options options including: @@ -794,23 +775,6 @@ class Summary extends React.Component { ); } - _renderInsightsPromoModal = () => { - const { account, isTimeForInsightsPromo } = this.props; - const { insightsPromoModalShown, isInsightsModalHidden } = this.state; - - if (insightsPromoModalShown || !isTimeForInsightsPromo) return null; - if (account && account.user && account.user.scopes && account.user.scopes.includes('subscriptions:insights')) return null; // don't show the promo to Insights subscribers, either - - sendMessage('promoModals.sawInsightsPromo', '', 'metrics'); - - return ( - - ); - } - /** * React's required render function. Returns JSX * @return {JSX} JSX for rendering the Summary View of the panel @@ -830,41 +794,38 @@ class Summary extends React.Component { }); return ( - <> - {this._renderInsightsPromoModal()} -
    - {!isCondensed && disableBlocking && ()} - {!isCondensed && !disableBlocking && this._renderDonut()} - {!isCondensed && !disableBlocking && this._renderPageHostReadout()} - - {isCondensed && !disableBlocking && this._renderTotalTrackersFound()} - -
    - {!disableBlocking && this._renderTotalTrackersBlocked()} - {!disableBlocking && this._renderTotalRequestsModified()} - {!disableBlocking && this._renderPageLoadTime()} -
    +
    + {!isCondensed && disableBlocking && ()} + {!isCondensed && !disableBlocking && this._renderDonut()} + {!isCondensed && !disableBlocking && this._renderPageHostReadout()} - {isCondensed && disableBlocking && ( -
    - )} + {isCondensed && !disableBlocking && this._renderTotalTrackersFound()} -
    - {this._renderGhosteryFeature('trust')} - {this._renderGhosteryFeature('restrict', 'Summary__ghosteryFeatureContainer--middle')} - {this._renderPauseButton()} -
    -
    - {this._renderCliqzAntiTracking()} - {this._renderCliqzAdBlock()} - {this._renderCliqzSmartBlock()} -
    - {this._renderStatsNavicon()} - {enable_offers && this._renderRewardsNavicon()} +
    + {!disableBlocking && this._renderTotalTrackersBlocked()} + {!disableBlocking && this._renderTotalRequestsModified()} + {!disableBlocking && this._renderPageLoadTime()} +
    + + {isCondensed && disableBlocking && ( +
    + )} - {!isCondensed && this._renderPlusUpgradeBannerOrSubscriberIcon()} +
    + {this._renderGhosteryFeature('trust')} + {this._renderGhosteryFeature('restrict', 'Summary__ghosteryFeatureContainer--middle')} + {this._renderPauseButton()}
    - +
    + {this._renderCliqzAntiTracking()} + {this._renderCliqzAdBlock()} + {this._renderCliqzSmartBlock()} +
    + {this._renderStatsNavicon()} + {enable_offers && this._renderRewardsNavicon()} + + {!isCondensed && this._renderPlusUpgradeBannerOrSubscriberIcon()} +
    ); } } diff --git a/app/panel/constants/constants.js b/app/panel/constants/constants.js index 7d5a98342..4c4b2c1ef 100644 --- a/app/panel/constants/constants.js +++ b/app/panel/constants/constants.js @@ -18,6 +18,7 @@ export const SHOW_NOTIFICATION = 'SHOW_NOTIFICATION'; export const TOGGLE_CLIQZ_FEATURE = 'TOGGLE_CLIQZ_FEATURE'; export const CLOSE_NOTIFICATION = 'CLOSE_NOTIFICATION'; export const TOGGLE_EXPERT = 'TOGGLE_EXPERT'; +export const TOGGLE_INSIGHTS_MODAL = 'TOGGLE_INSIGHTS_MODAL'; // summary export const UPDATE_SUMMARY_DATA = 'UPDATE_SUMMARY_DATA'; diff --git a/app/panel/containers/InsightsPromoModalContainer.js b/app/panel/containers/InsightsPromoModalContainer.js new file mode 100644 index 000000000..2cfa8de58 --- /dev/null +++ b/app/panel/containers/InsightsPromoModalContainer.js @@ -0,0 +1,35 @@ +/** + * InsightsPromoModal Container + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import InsightsPromoModal from '../components/InsightsPromoModal'; +import * as actions from '../actions/PanelActions'; // get shared actions from Panel + +/* + * Bind Login view component action creators using Redux's bindActionCreators + * @memberOf PanelContainers + * @param {function} dispatch redux store method which dispatches actions + * @param {Object} ownProps Login view component own props + * @return {function} to be used as an argument in redux connect call + */ +const mapDispatchToProps = dispatch => ({ actions: bindActionCreators(Object.assign(actions), dispatch) }); +/** + * Connects Login view component to the Redux store. + * @memberOf PanelContainers + * @param {function} mapStateToProps maps redux store state properties to Login view component own properties + * @param {function} mapDispatchToProps binds Login view component action creators + * @return {Object} A higher-order React component class that passes state and action + * creators into Login view component. Used by React framework. + */ +export default connect(null, mapDispatchToProps)(InsightsPromoModal); diff --git a/app/panel/containers/PanelContainer.js b/app/panel/containers/PanelContainer.js index 9b2f73d8e..2a4b9e007 100644 --- a/app/panel/containers/PanelContainer.js +++ b/app/panel/containers/PanelContainer.js @@ -30,7 +30,7 @@ import { updateBlockingData } from '../actions/BlockingActions'; const mapStateToProps = state => Object.assign({}, state.panel, state.drawer, state.account, { paused_blocking: state.summary.paused_blocking, sitePolicy: state.summary.sitePolicy, - trackerCounts: state.summary.trackerCounts + trackerCounts: state.summary.trackerCounts, }); /** * Bind Panel view component action creators using Redux's bindActionCreators diff --git a/app/panel/reducers/panel.js b/app/panel/reducers/panel.js index b249b6e70..37b6f6e52 100644 --- a/app/panel/reducers/panel.js +++ b/app/panel/reducers/panel.js @@ -26,7 +26,8 @@ import { SET_OFFER_READ, TOGGLE_EXPANDED, SET_THEME, - CLEAR_THEME + CLEAR_THEME, + TOGGLE_INSIGHTS_MODAL } from '../constants/constants'; import { LOGIN_SUCCESS, @@ -60,6 +61,7 @@ const initialState = { email: '', emailValidated: false, current_theme: 'default', + isInsightsModalHidden: false, }; /** * Default export for panel view reducer. Handles actions @@ -260,6 +262,12 @@ export default (state = initialState, action) => { } return state; } + case TOGGLE_INSIGHTS_MODAL: { + return { + ...state, + isInsightsModalHidden: !state.isInsightsModalHidden + }; + } default: return state; } }; diff --git a/app/scss/panel.scss b/app/scss/panel.scss index f79b4a723..3f6596ed4 100644 --- a/app/scss/panel.scss +++ b/app/scss/panel.scss @@ -51,6 +51,7 @@ html body { // Partial View SASS files @import './partials/_svgs'; +@import './partials/_shared_components_svgs'; @import './partials/_header'; @import './partials/_callout'; @import './partials/_summary'; @@ -73,10 +74,9 @@ html body { @import './partials/_subscribe'; @import './partials/_stats'; @import './partials/_stats_graph'; +@import './partials/_modal_exit_button'; +@import './partials/insights_promo_modal.scss'; // Imports from ../shared-components directory -@import './partials/shared_components_svgs'; -@import '../shared-components/ModalExitButton/ModalExitButton.scss'; @import '../shared-components/Modal/Modal.scss'; -@import '../shared-components/InsightsPromoModal/InsightsPromoModal.scss'; @import '../shared-components/PlusPromoModal/PlusPromoModal.scss'; diff --git a/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss b/app/scss/partials/_insights_promo_modal.scss similarity index 84% rename from app/shared-components/InsightsPromoModal/InsightsPromoModal.scss rename to app/scss/partials/_insights_promo_modal.scss index 4488bf761..670e9b275 100644 --- a/app/shared-components/InsightsPromoModal/InsightsPromoModal.scss +++ b/app/scss/partials/_insights_promo_modal.scss @@ -13,23 +13,15 @@ height: 94px; width: 177px; margin-bottom: 16px; - background-size: 177px 94px; background-image: url('/app/images/panel/insights-ribbon.svg'); - background-position: center center; - background-repeat: no-repeat; } .InsightsModal__header { font-family: 'Roboto'; - width: 348px; height: 27.1px; font-size: 20px; font-weight: 900; - font-style: normal; - font-stretch: normal; line-height: 1.35; - letter-spacing: normal; - text-align: center; color: $tundora; margin-bottom: 8.9px; } @@ -39,12 +31,7 @@ height: 54.2px; font-size: 18px; font-weight: 500; - font-style: normal; - font-stretch: normal; - line-height: 1.5; - letter-spacing: normal; text-align: center; - color: $tundora; margin-bottom: 12.8px; font-family: 'Roboto'; } @@ -70,9 +57,7 @@ height: 18px; width: 18px; margin-right: 8px; - object-fit: contain; background-image: url('/app/images/panel/checked-circle-icon.svg'); - padding-right: 10px; } .InsightsModal__call-to-action-container { diff --git a/app/shared-components/ModalExitButton/ModalExitButton.scss b/app/scss/partials/_modal_exit_button.scss similarity index 100% rename from app/shared-components/ModalExitButton/ModalExitButton.scss rename to app/scss/partials/_modal_exit_button.scss diff --git a/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx b/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx deleted file mode 100644 index f95ebbe4e..000000000 --- a/app/shared-components/InsightsPromoModal/InsightsPromoModal.jsx +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Insights Promo Modal Component - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2019 Ghostery, Inc. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import Modal from '../Modal/Modal'; -import history from '../../panel/utils/history'; -import ModalExitButton from '../ModalExitButton/ModalExitButton'; - -// A Functional React component for a Modal -const InsightsPromoModal = ({ show, toggleModal }) => { - const clickSignIn = () => { - history.push({ - pathname: '/login', - state: { isInsightsModalHidden: false } - }); - toggleModal(); - }; - - return ( - -
    - -
    -
    - {t('panel_insights_promotion_header')} -
    -
    - {t('panel_insights_promotion_description')} -
    -
    -
    -
    - -
    - { t('panel_insights_audit_tags') } -
    -
    -
    - -
    - { t('panel_insights_promotion_trace_poor_performance') } -
    -
    -
    -
    -
    - -
    - { t('panel_insights_promotion_watch_pings') } -
    -
    -
    - -
    - { t('panel_insights_promotion_explore_trends') } -
    -
    -
    -
    -
    - -
    - {t('subscribe_pitch_sign_in')} - {t('subscribe_pitch_no_thanks')} -
    -
    -
    - - ); -}; - -// PropTypes ensure we pass required props of the correct type -Modal.propTypes = { - toggleModal: PropTypes.func.isRequired -}; - -export default InsightsPromoModal; diff --git a/app/shared-components/InsightsPromoModal/index.js b/app/shared-components/InsightsPromoModal/index.js deleted file mode 100644 index af3281b9b..000000000 --- a/app/shared-components/InsightsPromoModal/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Point of entry index.js file for Insights Promo Modal Component - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2019 Ghostery, Inc. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -import InsightsPromoModal from './InsightsPromoModal'; - -export default InsightsPromoModal; diff --git a/app/shared-components/ModalExitButton/index.js b/app/shared-components/ModalExitButton/index.js deleted file mode 100644 index 6745028a1..000000000 --- a/app/shared-components/ModalExitButton/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Point of entry index.js file for Insights Promo Modal Component - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2019 Ghostery, Inc. All rights reserved. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0 - */ - -import ModalExitButton from './ModalExitButton'; - -export default ModalExitButton; diff --git a/app/shared-components/index.js b/app/shared-components/index.js index e3abeee09..a7fe2f139 100644 --- a/app/shared-components/index.js +++ b/app/shared-components/index.js @@ -22,8 +22,6 @@ import SteppedNavigation from './SteppedNavigation'; import ToastMessage from './ToastMessage'; import ToggleCheckbox from './ToggleCheckbox'; import ToggleSwitch from './ToggleSwitch'; -import ModalExitButton from './ModalExitButton'; -import InsightsPromoModal from './InsightsPromoModal'; export { ExitButton, @@ -32,7 +30,5 @@ export { SteppedNavigation, ToastMessage, ToggleCheckbox, - ToggleSwitch, - ModalExitButton, - InsightsPromoModal + ToggleSwitch }; diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 8db34cacd..fffaec688 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -115,7 +115,7 @@ class ConfData { _initProperty('hide_alert_trusted', false); _initProperty('ignore_first_party', true); _initProperty('import_callout_dismissed', true); - _initProperty('insights_promo_modal_last_seen', 0); + _initProperty('insights_promo_modal_last_seen', null); _initProperty('install_random_number', 0); _initProperty('install_date', 0); _initProperty('is_expanded', false); diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 7307374d6..01b7b6cd6 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -766,7 +766,9 @@ class Metrics { * @private */ _recordEngagedWithRepeats() { - const engaged_daily_velocity_with_repeats = conf.metrics.engaged_daily_velocity_with_repeats || []; + // const engaged_daily_velocity_with_repeats = conf.metrics.engaged_daily_velocity_with_repeats || []; + const engaged_daily_velocity_with_repeats = conf.metrics.engaged_daily_velocity_with_repeats || [123, 123, 123, 124, 124, 124, 18192, 18192]; + console.log('engaged_daily_velocity_with_repeats', engaged_daily_velocity_with_repeats); const today = Math.floor(Number(new Date().getTime()) / 86400000); // Today's time engaged_daily_velocity_with_repeats.push(today); conf.metrics.engaged_daily_velocity_with_repeats = engaged_daily_velocity_with_repeats; diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 4e005e1a5..666e280a9 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -367,7 +367,8 @@ class PanelData { is_android: globals.BROWSER_INFO.os === 'android', language, isTimeForInsightsPromo: promoModals.isTimeForInsightsPromo(), - isTimeForAPlusPromo: promoModals.isTimeForAPlusPromo(), + // isTimeForAPlusPromo: promoModals.isTimeForAPlusPromo(), + isTimeForAPlusPromo: false, haveSeenInitialPlusPromo: promoModals.haveSeenInitialPlusPromo(), reload_banner_status, tab_id, diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index 48501b5dd..4024a780a 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -42,12 +42,15 @@ class PromoModals { static recordInsightsPromoSighting() { this._recordPromoSighting(INSIGHTS); } static _isTimeForAPromo(type) { - const lastSeenPlusPromo = conf[`${PLUS}_${PROMO_MODAL_LAST_SEEN}`]; - const lastSeenInsightsPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; - const lastSeenPromo = Math.max(lastSeenPlusPromo, lastSeenInsightsPromo); - if (conf.notify_promotions === false) { return false; } - if (lastSeenPromo === null) { return true; } + + // const lastSeenPlusPromo = conf[`${PLUS}_${PROMO_MODAL_LAST_SEEN}`]; + // const lastSeenInsightsPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; + // const lastSeenPromo = lastSeenPlusPromo > lastSeenInsightsPromo ? lastSeenPlusPromo : lastSeenInsightsPromo; + const lastSeenPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; + console.log('lastSeenPromo: ', lastSeenPromo); + + // if (lastSeenPromo === null) { return true; } if (type === INSIGHTS && !this._hasEngagedFrequently()) { return false; @@ -76,6 +79,7 @@ class PromoModals { } if (timesPerWeek >= 3) { + console.log('timesPerWeek', timesPerWeek); return true; } return false; From 3478857c0ff1c7948ca416b40c2e44b73991ee53 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 23 Oct 2019 19:17:16 -0400 Subject: [PATCH 35/39] Refactor hasEngagedFrequently to update in recordEngaged --- src/classes/Metrics.js | 22 +++++++++------------- src/classes/PanelData.js | 3 +-- src/classes/PromoModals.js | 33 +++++++++++++-------------------- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 01b7b6cd6..fbd4e4c31 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -209,7 +209,6 @@ class Metrics { this._recordActive(); break; case 'engaged': - this._recordEngagedWithRepeats(); this._recordEngaged(); break; @@ -750,6 +749,7 @@ class Metrics { _recordEngaged() { const engaged_daily_velocity = conf.metrics.engaged_daily_velocity || []; const today = Math.floor(Number(new Date().getTime()) / 86400000); // Today's time + engaged_daily_velocity.sort(); if (!engaged_daily_velocity.includes(today)) { engaged_daily_velocity.push(today); @@ -758,22 +758,18 @@ class Metrics { } } + const daily_engaged_count = conf.metrics.daily_engaged_count || new Array(engaged_daily_velocity.length).fill(0); + daily_engaged_count[engaged_daily_velocity.indexOf(today)]++; + if (!daily_engaged_count.includes(today)) { + if (daily_engaged_count.length > 7) { + daily_engaged_count.shift(); + } + } + conf.metrics.daily_engaged_count = daily_engaged_count; conf.metrics.engaged_daily_velocity = engaged_daily_velocity; this._sendReq('engaged', ['daily', 'weekly', 'monthly']); } - /** - * @private - */ - _recordEngagedWithRepeats() { - // const engaged_daily_velocity_with_repeats = conf.metrics.engaged_daily_velocity_with_repeats || []; - const engaged_daily_velocity_with_repeats = conf.metrics.engaged_daily_velocity_with_repeats || [123, 123, 123, 124, 124, 124, 18192, 18192]; - console.log('engaged_daily_velocity_with_repeats', engaged_daily_velocity_with_repeats); - const today = Math.floor(Number(new Date().getTime()) / 86400000); // Today's time - engaged_daily_velocity_with_repeats.push(today); - conf.metrics.engaged_daily_velocity_with_repeats = engaged_daily_velocity_with_repeats; - } - /** * Repeat sending active request every month * if computer is continuously on. diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 666e280a9..4e005e1a5 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -367,8 +367,7 @@ class PanelData { is_android: globals.BROWSER_INFO.os === 'android', language, isTimeForInsightsPromo: promoModals.isTimeForInsightsPromo(), - // isTimeForAPlusPromo: promoModals.isTimeForAPlusPromo(), - isTimeForAPlusPromo: false, + isTimeForAPlusPromo: promoModals.isTimeForAPlusPromo(), haveSeenInitialPlusPromo: promoModals.haveSeenInitialPlusPromo(), reload_banner_status, tab_id, diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index 4024a780a..e1db6f368 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -44,13 +44,11 @@ class PromoModals { static _isTimeForAPromo(type) { if (conf.notify_promotions === false) { return false; } - // const lastSeenPlusPromo = conf[`${PLUS}_${PROMO_MODAL_LAST_SEEN}`]; - // const lastSeenInsightsPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; - // const lastSeenPromo = lastSeenPlusPromo > lastSeenInsightsPromo ? lastSeenPlusPromo : lastSeenInsightsPromo; - const lastSeenPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; - console.log('lastSeenPromo: ', lastSeenPromo); + const lastSeenPlusPromo = conf[`${PLUS}_${PROMO_MODAL_LAST_SEEN}`]; + const lastSeenInsightsPromo = conf[`${INSIGHTS}_${PROMO_MODAL_LAST_SEEN}`]; + const lastSeenPromo = lastSeenPlusPromo > lastSeenInsightsPromo ? lastSeenPlusPromo : lastSeenInsightsPromo; - // if (lastSeenPromo === null) { return true; } + if (lastSeenPromo === null) { return true; } if (type === INSIGHTS && !this._hasEngagedFrequently()) { return false; @@ -67,21 +65,16 @@ class PromoModals { } static _hasEngagedFrequently() { - const { engaged_daily_velocity_with_repeats } = conf.metrics; - const pastSevenDays = Array.from(new Set(engaged_daily_velocity_with_repeats)); - let timesPerWeek = 0; - - for (let i = 0; i < pastSevenDays.length; i++) { - const engagementsEachDay = engaged_daily_velocity_with_repeats.filter(day => day === pastSevenDays[i]).length; - if (engagementsEachDay >= 3) { - timesPerWeek++; - } - } + const { daily_engaged_count } = conf.metrics; + const DAILY_TARGET = 3; + const WEEKLY_TARGET = 3; + + let very_engaged_days = 0; + daily_engaged_count.forEach((count) => { + very_engaged_days = count >= DAILY_TARGET ? ++very_engaged_days : very_engaged_days; + }); + if (very_engaged_days >= WEEKLY_TARGET) return true; - if (timesPerWeek >= 3) { - console.log('timesPerWeek', timesPerWeek); - return true; - } return false; } } From b490d7cc66e241bf9738a274c97b122d9c9d1756 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 24 Oct 2019 09:48:10 -0400 Subject: [PATCH 36/39] Refactor recordEngaged --- src/classes/Metrics.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index fbd4e4c31..40a02131f 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -748,23 +748,20 @@ class Metrics { */ _recordEngaged() { const engaged_daily_velocity = conf.metrics.engaged_daily_velocity || []; + const today = Math.floor(Number(new Date().getTime()) / 86400000); // Today's time + const daily_engaged_count = conf.metrics.daily_engaged_count || new Array(engaged_daily_velocity.length).fill(0); + daily_engaged_count[engaged_daily_velocity.indexOf(today)]++; engaged_daily_velocity.sort(); if (!engaged_daily_velocity.includes(today)) { engaged_daily_velocity.push(today); if (engaged_daily_velocity.length > 7) { + daily_engaged_count.shift(); engaged_daily_velocity.shift(); } } - const daily_engaged_count = conf.metrics.daily_engaged_count || new Array(engaged_daily_velocity.length).fill(0); - daily_engaged_count[engaged_daily_velocity.indexOf(today)]++; - if (!daily_engaged_count.includes(today)) { - if (daily_engaged_count.length > 7) { - daily_engaged_count.shift(); - } - } conf.metrics.daily_engaged_count = daily_engaged_count; conf.metrics.engaged_daily_velocity = engaged_daily_velocity; this._sendReq('engaged', ['daily', 'weekly', 'monthly']); From 0edc57cca505dd50a6d94831db2582d6c571ad3b Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 24 Oct 2019 11:11:27 -0400 Subject: [PATCH 37/39] Fix recordEngaged bug --- src/classes/Metrics.js | 10 ++++++---- src/classes/PromoModals.js | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 40a02131f..71864c1d7 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -748,21 +748,23 @@ class Metrics { */ _recordEngaged() { const engaged_daily_velocity = conf.metrics.engaged_daily_velocity || []; + const engaged_daily_count = conf.metrics.engaged_daily_count || new Array(engaged_daily_velocity.length).fill(0); const today = Math.floor(Number(new Date().getTime()) / 86400000); // Today's time - const daily_engaged_count = conf.metrics.daily_engaged_count || new Array(engaged_daily_velocity.length).fill(0); - daily_engaged_count[engaged_daily_velocity.indexOf(today)]++; engaged_daily_velocity.sort(); if (!engaged_daily_velocity.includes(today)) { engaged_daily_velocity.push(today); + engaged_daily_count.push(1); if (engaged_daily_velocity.length > 7) { - daily_engaged_count.shift(); + engaged_daily_count.shift(); engaged_daily_velocity.shift(); } + } else { + engaged_daily_count[engaged_daily_velocity.indexOf(today)]++; } - conf.metrics.daily_engaged_count = daily_engaged_count; + conf.metrics.engaged_daily_count = engaged_daily_count; conf.metrics.engaged_daily_velocity = engaged_daily_velocity; this._sendReq('engaged', ['daily', 'weekly', 'monthly']); } diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index e1db6f368..c068b2c84 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -65,12 +65,12 @@ class PromoModals { } static _hasEngagedFrequently() { - const { daily_engaged_count } = conf.metrics; + const { engaged_daily_count } = conf.metrics || []; const DAILY_TARGET = 3; const WEEKLY_TARGET = 3; let very_engaged_days = 0; - daily_engaged_count.forEach((count) => { + engaged_daily_count.forEach((count) => { very_engaged_days = count >= DAILY_TARGET ? ++very_engaged_days : very_engaged_days; }); if (very_engaged_days >= WEEKLY_TARGET) return true; From 14936c6cb62247d6753dd6e64b0652c67ae0a97e Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Thu, 24 Oct 2019 13:53:23 -0400 Subject: [PATCH 38/39] Fix font paths --- app/scss/partials/_fonts.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/scss/partials/_fonts.scss b/app/scss/partials/_fonts.scss index 65b9a34c2..c048be1ee 100644 --- a/app/scss/partials/_fonts.scss +++ b/app/scss/partials/_fonts.scss @@ -187,40 +187,40 @@ font-family: 'Roboto'; font-style: normal; font-weight: 300; - src: local('Roboto Light'), local('Roboto-Light'), url('../../../app/fonts/roboto-latin-bold-300.woff2') format('woff2'), + src: local('Roboto Light'), local('Roboto-Light'), url('../fonts/roboto-latin-bold-300.woff2') format('woff2'), } /* roboto-regular - latin */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; - src: local('Roboto'), local('Roboto-Regular'), url('../../../app/fonts/roboto-all-charsets.woff2') format('woff2'), + src: local('Roboto'), local('Roboto-Regular'), url('../fonts/roboto-all-charsets.woff2') format('woff2'), } /* roboto-500 - latin */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 500; - src: local('Roboto Medium'), local('Roboto-Medium'), url('../../../app/fonts/roboto-latin-bold-500.woff2') format('woff2'), + src: local('Roboto Medium'), local('Roboto-Medium'), url('../fonts/roboto-latin-bold-500.woff2') format('woff2'), } /* roboto-700 - latin */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 700; - src: local('Roboto Bold'), local('Roboto-Bold'), url('../../../app/fonts/roboto-latin-bold-700.woff2') format('woff2'), + src: local('Roboto Bold'), local('Roboto-Bold'), url('../fonts/roboto-latin-bold-700.woff2') format('woff2'), } /* roboto-900 - latin */ @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 900; - src: local('Roboto Black'), local('Roboto-Black'), url('../../../app/fonts/roboto-latin-bold-900.woff2') format('woff2'), + src: local('Roboto Black'), local('Roboto-Black'), url('../fonts/roboto-latin-bold-900.woff2') format('woff2'), } /* roboto-condensed-700 - latin */ @font-face { font-family: 'Roboto Condensed'; font-style: normal; font-weight: 700; - src: local('Roboto Condensed Bold'), local('RobotoCondensed-Bold'), url('../../../app/fonts/roboto-condensed-latin-bold-700.woff2') format('woff2'), /* Super Modern Browsers */ + src: local('Roboto Condensed Bold'), local('RobotoCondensed-Bold'), url('../fonts/roboto-condensed-latin-bold-700.woff2') format('woff2'), /* Super Modern Browsers */ } From 1d905aa849aaef519bf904b758074ffaa4619235 Mon Sep 17 00:00:00 2001 From: wlycdgr Date: Thu, 24 Oct 2019 13:57:12 -0400 Subject: [PATCH 39/39] Remove unused font asset --- app/fonts/roboto-all-charsets.woff | Bin 85876 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/fonts/roboto-all-charsets.woff diff --git a/app/fonts/roboto-all-charsets.woff b/app/fonts/roboto-all-charsets.woff deleted file mode 100644 index 96c1986f01459bc3b7ca8e18fc06785e5e35dc45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85876 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 YL6htZCcLmfC1Z!aXXnWeQyf*20Ac;`*Z=?k