fix: add donation ga and tests (#38003)

* fix: add donation ga

* feat: add ga-saga tests
This commit is contained in:
Ahmad Abdolsaheb
2020-02-04 08:43:56 +03:00
committed by GitHub
parent 992ee98cc1
commit 24eb2c4310
19 changed files with 360 additions and 168 deletions

158
client/package-lock.json generated
View File

@ -4840,8 +4840,7 @@
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"optional": true
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"aproba": {
"version": "1.2.0",
@ -4862,14 +4861,12 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"optional": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -4884,20 +4881,17 @@
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"optional": true
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"optional": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"core-util-is": {
"version": "1.0.2",
@ -5014,8 +5008,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"optional": true
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
"version": "1.3.5",
@ -5027,7 +5020,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -5042,7 +5034,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -5050,14 +5041,12 @@
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"optional": true
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"minipass": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -5076,7 +5065,6 @@
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -5157,8 +5145,7 @@
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"optional": true
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"object-assign": {
"version": "4.1.1",
@ -5170,7 +5157,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"optional": true,
"requires": {
"wrappy": "1"
}
@ -5256,8 +5242,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"optional": true
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
@ -5293,7 +5278,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -5313,7 +5297,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -5357,14 +5340,12 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"optional": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"optional": true
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
}
}
},
@ -8757,8 +8738,7 @@
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"optional": true
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"aproba": {
"version": "1.2.0",
@ -8779,14 +8759,12 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"optional": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -8801,20 +8779,17 @@
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"optional": true
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"optional": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"core-util-is": {
"version": "1.0.2",
@ -8931,8 +8906,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"optional": true
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
"version": "1.3.5",
@ -8944,7 +8918,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -8959,7 +8932,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -8967,14 +8939,12 @@
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"optional": true
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"minipass": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -8993,7 +8963,6 @@
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -9080,8 +9049,7 @@
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"optional": true
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"object-assign": {
"version": "4.1.1",
@ -9093,7 +9061,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"optional": true,
"requires": {
"wrappy": "1"
}
@ -9179,8 +9146,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"optional": true
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
@ -9216,7 +9182,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -9236,7 +9201,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -9280,17 +9244,21 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"optional": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"optional": true
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
}
}
},
"fsm-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fsm-iterator/-/fsm-iterator-1.1.0.tgz",
"integrity": "sha1-M33kXeGesgV4jPAuOpVewgZ2Dew=",
"dev": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -12392,8 +12360,7 @@
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"optional": true
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"loose-envify": {
"version": "1.4.0",
@ -12445,7 +12412,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"optional": true,
"requires": {
"ansi-regex": "^4.1.0"
}
@ -15379,6 +15345,12 @@
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"lodash.ismatch": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
"integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=",
"dev": true
},
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
@ -15467,8 +15439,7 @@
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"optional": true
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"string-width": {
"version": "3.1.0",
@ -15485,7 +15456,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"optional": true,
"requires": {
"ansi-regex": "^4.1.0"
}
@ -19428,6 +19398,20 @@
"@redux-saga/core": "^1.1.1"
}
},
"redux-saga-test-plan": {
"version": "4.0.0-rc.3",
"resolved": "https://registry.npmjs.org/redux-saga-test-plan/-/redux-saga-test-plan-4.0.0-rc.3.tgz",
"integrity": "sha512-18Ha1AWmP4Xt64cSyJv2j7sH8lwQzdk/LByiyhoNyuqyhzkQzlKlflHr3LqiSCzn3e2AgFxagynto8XGRqsHcg==",
"dev": true,
"requires": {
"core-js": "^2.4.1",
"fsm-iterator": "^1.1.0",
"lodash.isequal": "^4.5.0",
"lodash.ismatch": "^4.4.0",
"object-assign": "^4.1.0",
"util-inspect": "^0.1.8"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
@ -20073,7 +20057,6 @@
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.15.0.tgz",
"integrity": "sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg==",
"optional": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@ -22433,6 +22416,47 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"util-inspect": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/util-inspect/-/util-inspect-0.1.8.tgz",
"integrity": "sha1-KznbzS2SHy2EMJI8r/QPS1zqXbE=",
"dev": true,
"requires": {
"array-map": "0.0.0",
"array-reduce": "0.0.0",
"foreach": "2.0.4",
"indexof": "0.0.1",
"isarray": "0.0.1",
"json3": "3.3.0",
"object-keys": "0.5.0"
},
"dependencies": {
"foreach": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.4.tgz",
"integrity": "sha1-zF0NiuHUbMmlVcJoL5EJd4WZNd8=",
"dev": true
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"json3": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.0.tgz",
"integrity": "sha1-Dp5/bF0nC3WJKa9Nb+/chL1m4lk=",
"dev": true
},
"object-keys": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.5.0.tgz",
"integrity": "sha1-CeIR8+ADGK/E9ZLjbnzcENmtcpM=",
"dev": true
}
}
},
"util.promisify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz",

View File

@ -104,6 +104,7 @@
"jest": "^24.9.0",
"jest-dom": "^3.5.0",
"react-test-renderer": "^16.10.2",
"redux-saga-test-plan": "^4.0.0-rc.3",
"webpack-cli": "^3.3.9"
},
"repository": {

View File

@ -15,7 +15,8 @@ import {
showCert,
userFetchStateSelector,
usernameSelector,
isDonatingSelector
isDonatingSelector,
executeGA
} from '../redux';
import validCertNames from '../../utils/validCertNames';
import { createFlashMessage } from '../components/Flash/redux';
@ -37,6 +38,7 @@ const propTypes = {
certDashedName: PropTypes.string,
certName: PropTypes.string,
createFlashMessage: PropTypes.func.isRequired,
executeGA: PropTypes.func,
fetchState: PropTypes.shape({
pending: PropTypes.bool,
complete: PropTypes.bool,
@ -74,19 +76,20 @@ const mapStateToProps = (state, { certName }) => {
};
const mapDispatchToProps = dispatch =>
bindActionCreators({ createFlashMessage, showCert }, dispatch);
bindActionCreators({ createFlashMessage, showCert, executeGA }, dispatch);
class ShowCertification extends Component {
constructor(...args) {
super(...args);
this.state = {
closeBtn: false,
donationClosed: false
isDonationSubmitted: false,
isDonationDisplayed: false,
isDonationClosed: false
};
this.hideDonationSection = this.hideDonationSection.bind(this);
this.showDonationCloseBtn = this.showDonationCloseBtn.bind(this);
this.handleProcessing = this.handleProcessing.bind(this);
}
componentDidMount() {
@ -97,12 +100,53 @@ class ShowCertification extends Component {
return null;
}
hideDonationSection() {
this.setState({ donationClosed: true });
shouldComponentUpdate(nextProps) {
const {
userFetchState: { complete: userComplete },
signedInUserName,
isDonating,
cert: { username = '' },
executeGA
} = nextProps;
const { isDonationDisplayed } = this.state;
if (
!isDonationDisplayed &&
userComplete &&
signedInUserName === username &&
!isDonating
) {
this.setState({
isDonationDisplayed: true
});
executeGA({
type: 'event',
data: {
category: 'Donation',
action: 'Displayed Certificate Donation',
nonInteraction: true
}
});
}
return true;
}
showDonationCloseBtn() {
this.setState({ closeBtn: true });
hideDonationSection() {
this.setState({ isDonationDisplayed: false, isDonationClosed: true });
}
handleProcessing(duration, amount) {
this.props.executeGA({
type: 'event',
data: {
category: 'donation',
action: 'certificate stripe form submission',
label: duration,
value: amount
}
});
this.setState({ isDonationSubmitted: true });
}
render() {
@ -111,13 +155,14 @@ class ShowCertification extends Component {
fetchState,
validCertName,
createFlashMessage,
certName,
signedInUserName,
isDonating,
userFetchState
certName
} = this.props;
const { donationClosed, closeBtn } = this.state;
const {
isDonationSubmitted,
isDonationDisplayed,
isDonationClosed
} = this.state;
if (!validCertName) {
createFlashMessage(standardErrorMessage);
@ -125,7 +170,6 @@ class ShowCertification extends Component {
}
const { pending, complete, errored } = fetchState;
const { complete: userComplete } = userFetchState;
if (pending) {
return <Loader fullScreen={true} />;
@ -149,8 +193,6 @@ class ShowCertification extends Component {
completionTime
} = cert;
let conditionalDonationSection = '';
const donationCloseBtn = (
<div>
<Button
@ -164,43 +206,36 @@ class ShowCertification extends Component {
</div>
);
if (
userComplete &&
signedInUserName === username &&
!isDonating &&
!donationClosed
) {
conditionalDonationSection = (
<Grid className='donation-section'>
{!closeBtn && (
<Row>
<Col sm={10} smOffset={1} xs={12}>
<p>
Only you can see this message. Congratulations on earning this
certification. Its no easy task. Running freeCodeCamp isnt
easy either. Nor is it cheap. Help us help you and many other
people around the world. Make a tax-deductible supporting
donation to our nonprofit today.
</p>
</Col>
</Row>
)}
<MinimalDonateForm
showCloseBtn={this.showDonationCloseBtn}
defaultTheme='light'
/>
let donationSection = (
<Grid className='donation-section'>
{!isDonationSubmitted && (
<Row>
<Col sm={4} smOffset={4} xs={6} xsOffset={3}>
{closeBtn ? donationCloseBtn : ''}
<Col sm={10} smOffset={1} xs={12}>
<p>
Only you can see this message. Congratulations on earning this
certification. Its no easy task. Running freeCodeCamp isnt
easy either. Nor is it cheap. Help us help you and many other
people around the world. Make a tax-deductible supporting
donation to our nonprofit today.
</p>
</Col>
</Row>
</Grid>
);
}
)}
<MinimalDonateForm
handleProcessing={this.handleProcessing}
defaultTheme='light'
/>
<Row>
<Col sm={4} smOffset={4} xs={6} xsOffset={3}>
{isDonationSubmitted && donationCloseBtn}
</Col>
</Row>
</Grid>
);
return (
<div className='certificate-outer-wrapper'>
{conditionalDonationSection}
{isDonationDisplayed && !isDonationClosed ? donationSection : ''}
<Grid className='certificate-wrapper certification-namespace'>
<Row>
<header>

View File

@ -34,6 +34,7 @@ const numToCommas = num =>
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
const propTypes = {
handleProcessing: PropTypes.func,
isDonating: PropTypes.bool,
isSignedIn: PropTypes.bool,
navigate: PropTypes.func.isRequired,
@ -191,7 +192,7 @@ class DonateForm extends Component {
}
renderDonationOptions() {
const { stripe } = this.props;
const { stripe, handleProcessing } = this.props;
const { donationAmount, donationDuration, paymentType } = this.state;
return (
<div>
@ -203,6 +204,7 @@ class DonateForm extends Component {
donationAmount={donationAmount}
donationDuration={donationDuration}
getDonationButtonLabel={this.getDonationButtonLabel}
handleProcessing={handleProcessing}
hideAmountOptionsCB={this.hideAmountOptionsCB}
/>
</Elements>

View File

@ -24,6 +24,7 @@ const propTypes = {
donationDuration: PropTypes.string.isRequired,
email: PropTypes.string,
getDonationButtonLabel: PropTypes.func.isRequired,
handleProcessing: PropTypes.func,
isSignedIn: PropTypes.bool,
showCloseBtn: PropTypes.func,
stripe: PropTypes.shape({
@ -146,8 +147,11 @@ class DonateFormChildViewForHOC extends Component {
// change the donation modal button label to close
// or display the close button for the cert donation section
if (this.props.showCloseBtn) {
this.props.showCloseBtn();
if (this.props.handleProcessing) {
this.props.handleProcessing(
this.state.donationDuration,
Math.round(this.state.donationAmount / 100)
);
}
return postChargeStripe({

View File

@ -11,11 +11,11 @@ import Heart from '../../assets/icons/Heart';
import Cup from '../../assets/icons/Cup';
import MinimalDonateForm from './MinimalDonateForm';
import ga from '../../analytics';
import {
closeDonationModal,
isDonationModalOpenSelector,
isBlockDonationModalSelector
isBlockDonationModalSelector,
executeGA
} from '../../redux';
import { challengeMetaSelector } from '../../templates/Challenges/redux';
@ -36,7 +36,8 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
closeDonationModal
closeDonationModal,
executeGA
},
dispatch
);
@ -45,18 +46,44 @@ const propTypes = {
activeDonors: PropTypes.number,
block: PropTypes.string,
closeDonationModal: PropTypes.func.isRequired,
executeGA: PropTypes.func,
isBlockDonation: PropTypes.bool,
show: PropTypes.bool
};
function DonateModal({ show, block, isBlockDonation, closeDonationModal }) {
function DonateModal({
show,
block,
isBlockDonation,
closeDonationModal,
executeGA
}) {
const [closeLabel, setCloseLabel] = React.useState(false);
const showCloseBtn = () => {
const handleProcessing = (duration, amount) => {
executeGA({
type: 'event',
data: {
category: 'donation',
action: 'Modal strip form submission',
label: duration,
value: amount
}
});
setCloseLabel(true);
};
if (show) {
ga.modalview('/donation-modal');
executeGA({ type: 'modal', data: '/donation-modal' });
executeGA({
type: 'event',
data: {
category: 'Donation',
action: `Displayed ${
isBlockDonation ? 'block' : 'progress'
} donation modal`,
nonInteraction: true
}
});
}
const donationText = (
@ -101,7 +128,7 @@ function DonateModal({ show, block, isBlockDonation, closeDonationModal }) {
<Modal.Body>
{isBlockDonation ? blockDonationText : progressDonationText}
<Spacer />
<MinimalDonateForm showCloseBtn={showCloseBtn} />
<MinimalDonateForm handleProcessing={handleProcessing} />
<Spacer />
<Row>
<Col sm={4} smOffset={4} xs={8} xsOffset={2}>

View File

@ -19,8 +19,8 @@ import './Donation.css';
const propTypes = {
defaultTheme: PropTypes.string,
handleProcessing: PropTypes.func,
isDonating: PropTypes.bool,
showCloseBtn: PropTypes.func,
stripe: PropTypes.shape({
createToken: PropTypes.func.isRequired
})
@ -79,7 +79,7 @@ class MinimalDonateForm extends Component {
render() {
const { donationAmount, donationDuration, stripe } = this.state;
const { showCloseBtn, defaultTheme } = this.props;
const { handleProcessing, defaultTheme } = this.props;
return (
<Row>
@ -93,7 +93,7 @@ class MinimalDonateForm extends Component {
getDonationButtonLabel={() =>
`Confirm your donation of $5 per month`
}
showCloseBtn={showCloseBtn}
handleProcessing={handleProcessing}
/>
</Elements>
</StripeProvider>

View File

@ -5,9 +5,8 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Link } from 'gatsby';
import ga from '../../../analytics';
import { makeExpandedBlockSelector, toggleBlock } from '../redux';
import { completedChallengesSelector } from '../../../redux';
import { completedChallengesSelector, executeGA } from '../../../redux';
import Caret from '../../../assets/icons/Caret';
import { blockNameify } from '../../../../utils/blockNameify';
import GreenPass from '../../../assets/icons/GreenPass';
@ -29,12 +28,13 @@ const mapStateToProps = (state, ownProps) => {
};
const mapDispatchToProps = dispatch =>
bindActionCreators({ toggleBlock }, dispatch);
bindActionCreators({ toggleBlock, executeGA }, dispatch);
const propTypes = {
blockDashedName: PropTypes.string,
challenges: PropTypes.array,
completedChallenges: PropTypes.arrayOf(PropTypes.string),
executeGA: PropTypes.func,
intro: PropTypes.shape({
fields: PropTypes.shape({ slug: PropTypes.string.isRequired }),
frontmatter: PropTypes.shape({
@ -58,19 +58,25 @@ export class Block extends Component {
}
handleBlockClick() {
const { blockDashedName, toggleBlock } = this.props;
ga.event({
category: 'Map Block Click',
action: blockDashedName
const { blockDashedName, toggleBlock, executeGA } = this.props;
executeGA({
type: 'event',
data: {
category: 'Map Block Click',
action: blockDashedName
}
});
return toggleBlock(blockDashedName);
}
handleChallengeClick(slug) {
return () => {
return ga.event({
category: 'Map Challenge Click',
action: slug
return this.props.executeGA({
type: 'event',
data: {
category: 'Map Challenge Click',
action: slug
}
});
};
}

View File

@ -44,6 +44,7 @@ test('<Block expanded snapshot', () => {
test('<Block /> should handle toggle clicks correctly', async () => {
const toggleSpy = jest.fn();
const toggleMapSpy = jest.fn();
const executeGA = jest.fn();
const props = {
blockDashedName: 'block-a',
@ -51,6 +52,7 @@ test('<Block /> should handle toggle clicks correctly', async () => {
completedChallenges: mockCompleted,
intro: mockIntroNodes[0],
isExpanded: false,
executeGA: executeGA,
toggleBlock: toggleSpy,
toggleMapModal: toggleMapSpy
};

View File

@ -2,8 +2,7 @@ import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import ga from '../../analytics';
import { fetchUser, isSignedInSelector } from '../../redux';
import { fetchUser, isSignedInSelector, executeGA } from '../../redux';
import { createSelector } from 'reselect';
const mapStateToProps = createSelector(
@ -13,7 +12,7 @@ const mapStateToProps = createSelector(
})
);
const mapDispatchToProps = { fetchUser };
const mapDispatchToProps = { fetchUser, executeGA };
class CertificationLayout extends Component {
componentDidMount() {
@ -21,7 +20,7 @@ class CertificationLayout extends Component {
if (!isSignedIn) {
fetchUser();
}
ga.pageview(pathname);
this.props.executeGA({ type: 'page', data: pathname });
}
render() {
return <Fragment>{this.props.children}</Fragment>;
@ -31,6 +30,7 @@ class CertificationLayout extends Component {
CertificationLayout.displayName = 'CertificationLayout';
CertificationLayout.propTypes = {
children: PropTypes.any,
executeGA: PropTypes.func,
fetchUser: PropTypes.func.isRequired,
isSignedIn: PropTypes.bool,
pathname: PropTypes.string.isRequired

View File

@ -6,13 +6,13 @@ import { createSelector } from 'reselect';
import Helmet from 'react-helmet';
import fontawesome from '@fortawesome/fontawesome';
import ga from '../../analytics';
import {
fetchUser,
isSignedInSelector,
onlineStatusChange,
isOnlineSelector,
userSelector
userSelector,
executeGA
} from '../../redux';
import { flashMessageSelector, removeFlashMessage } from '../Flash/redux';
@ -68,6 +68,7 @@ const metaKeywords = [
const propTypes = {
children: PropTypes.node.isRequired,
executeGA: PropTypes.func,
fetchUser: PropTypes.func.isRequired,
flashMessage: PropTypes.shape({
id: PropTypes.string,
@ -101,27 +102,27 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch =>
bindActionCreators(
{ fetchUser, removeFlashMessage, onlineStatusChange },
{ fetchUser, removeFlashMessage, onlineStatusChange, executeGA },
dispatch
);
class DefaultLayout extends Component {
componentDidMount() {
const { isSignedIn, fetchUser, pathname } = this.props;
const { isSignedIn, fetchUser, pathname, executeGA } = this.props;
if (!isSignedIn) {
fetchUser();
}
ga.pageview(pathname);
executeGA({ type: 'page', data: pathname });
window.addEventListener('online', this.updateOnlineStatus);
window.addEventListener('offline', this.updateOnlineStatus);
}
componentDidUpdate(prevProps) {
const { pathname } = this.props;
const { pathname, executeGA } = this.props;
const { pathname: prevPathname } = prevProps;
if (pathname !== prevPathname) {
ga.pageview(pathname);
executeGA({ type: 'page', data: pathname });
}
}

View File

@ -1,6 +1,7 @@
import React, { Component, Fragment } from 'react';
import Helmet from 'react-helmet';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
@ -9,10 +10,11 @@ import { stripePublicKey } from '../../config/env.json';
import { Spacer, Loader } from '../components/helpers';
import DonateForm from '../components/Donation/DonateForm';
import DonateText from '../components/Donation/DonateText';
import { signInLoadingSelector, userSelector } from '../redux';
import { signInLoadingSelector, userSelector, executeGA } from '../redux';
import { stripeScriptLoader } from '../utils/scriptLoaders';
const propTypes = {
executeGA: PropTypes.func,
isDonating: PropTypes.bool,
showLoading: PropTypes.bool.isRequired
};
@ -26,6 +28,14 @@ const mapStateToProps = createSelector(
})
);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
executeGA
},
dispatch
);
export class DonatePage extends Component {
constructor(...props) {
super(...props);
@ -33,11 +43,19 @@ export class DonatePage extends Component {
stripe: null,
enableSettings: false
};
this.handleProcessing = this.handleProcessing.bind(this);
this.handleStripeLoad = this.handleStripeLoad.bind(this);
}
componentDidMount() {
this.props.executeGA({
type: 'event',
data: {
category: 'Donation',
action: `Displayed donate page`,
nonInteraction: true
}
});
if (window.Stripe) {
this.handleStripeLoad();
} else if (document.querySelector('#stripe-js')) {
@ -56,6 +74,18 @@ export class DonatePage extends Component {
}
}
handleProcessing(duration, amount) {
this.props.executeGA({
type: 'event',
data: {
category: 'donation',
action: 'donate page stripe form submission',
label: duration,
value: amount
}
});
}
handleStripeLoad() {
// Create Stripe instance once Stripe.js loads
console.info('stripe has loaded');
@ -98,6 +128,7 @@ export class DonatePage extends Component {
<Col md={6}>
<DonateForm
enableDonationSettingsPage={this.enableDonationSettingsPage}
handleProcessing={this.handleProcessing}
stripe={stripe}
/>
</Col>
@ -117,4 +148,7 @@ export class DonatePage extends Component {
DonatePage.displayName = 'DonatePage';
DonatePage.propTypes = propTypes;
export default connect(mapStateToProps)(DonatePage);
export default connect(
mapStateToProps,
mapDispatchToProps
)(DonatePage);

View File

@ -0,0 +1,12 @@
import { takeEvery, call, all } from 'redux-saga/effects';
import ga from '../analytics';
const GaTypes = { event: ga.event, page: ga.pageview, modal: ga.modalview };
function* callGaType({ payload: { type, data } }) {
yield call(GaTypes[type], data);
}
export function* createGaSaga(types) {
yield all([takeEvery(types.executeGA, callGaType)]);
}

View File

@ -0,0 +1,28 @@
import { types } from '.';
import { createGaSaga } from './ga-saga';
import ga from '../analytics';
import { expectSaga } from 'redux-saga-test-plan';
describe('ga-saga', () => {
it('calls GA after executeGA action', () => {
const GaTypes = { event: ga.event, page: ga.pageview, modal: ga.modalview };
const mockEventPayload = {
type: 'event',
data: {
category: 'donation',
action: 'year end gift paypal button click'
}
};
return (
expectSaga(createGaSaga, types)
// Assert that the `call` with expected pramater will eventually happen.
.call(GaTypes.event, mockEventPayload.data)
// Dispatch any actions that the saga will `take`.
.dispatch({ type: types.executeGA, payload: mockEventPayload })
// Start the test.
.run()
);
});
});

View File

@ -11,6 +11,7 @@ import { createReportUserSaga } from './report-user-saga';
import { createShowCertSaga } from './show-cert-saga';
import { createNightModeSaga } from './night-mode-saga';
import { createDonationSaga } from './donation-saga';
import { createGaSaga } from './ga-saga';
import hardGoToEpic from './hard-go-to-epic';
import failedUpdatesEpic from './failed-updates-epic';
@ -65,6 +66,7 @@ export const types = createTypes(
'onlineStatusChange',
'resetUserData',
'tryToShowDonationModal',
'executeGA',
'submitComplete',
'updateComplete',
'updateCurrentChallengeId',
@ -84,6 +86,7 @@ export const sagas = [
...createAcceptTermsSaga(types),
...createAppMountSaga(types),
...createDonationSaga(types),
...createGaSaga(types),
...createFetchUserSaga(types),
...createShowCertSaga(types),
...createReportUserSaga(types),
@ -95,6 +98,9 @@ export const appMount = createAction(types.appMount);
export const tryToShowDonationModal = createAction(
types.tryToShowDonationModal
);
export const executeGA = createAction(types.executeGA);
export const allowBlockDonationRequests = createAction(
types.allowBlockDonationRequests
);

View File

@ -6,10 +6,8 @@ import { createSelector } from 'reselect';
import { Button, Modal } from '@freecodecamp/react-bootstrap';
import { useStaticQuery, graphql } from 'gatsby';
import ga from '../../../analytics';
import Login from '../../../components/Header/components/Login';
import CompletionModalBody from './CompletionModalBody';
import { dasherize } from '../../../../../utils/slugs';
import './completion-modal.css';
@ -25,7 +23,7 @@ import {
lastBlockChalSubmitted
} from '../redux';
import { isSignedInSelector } from '../../../redux';
import { isSignedInSelector, executeGA } from '../../../redux';
const mapStateToProps = createSelector(
challengeFilesSelector,
@ -60,7 +58,8 @@ const mapDispatchToProps = function(dispatch) {
},
lastBlockChalSubmitted: () => {
dispatch(lastBlockChalSubmitted());
}
},
executeGA
};
return () => dispatchers;
};
@ -70,6 +69,7 @@ const propTypes = {
close: PropTypes.func.isRequired,
completedChallengesIds: PropTypes.array,
currentBlockIds: PropTypes.array,
executeGA: PropTypes.func,
files: PropTypes.object.isRequired,
id: PropTypes.string,
isOpen: PropTypes.bool,
@ -190,7 +190,7 @@ export class CompletionModalInner extends Component {
const { completedPercent } = this.state;
if (isOpen) {
ga.modalview('/completion-modal');
executeGA({ type: 'modal', data: '/completion-modal' });
}
const dashedName = dasherize(title);
return (

View File

@ -4,21 +4,22 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Button, Modal } from '@freecodecamp/react-bootstrap';
import ga from '../../../analytics';
import { createQuestion, closeModal, isHelpModalOpenSelector } from '../redux';
import { executeGA } from '../../../redux';
import './help-modal.css';
const mapStateToProps = state => ({ isOpen: isHelpModalOpenSelector(state) });
const mapDispatchToProps = dispatch =>
bindActionCreators(
{ createQuestion, closeHelpModal: () => closeModal('help') },
{ createQuestion, executeGA, closeHelpModal: () => closeModal('help') },
dispatch
);
const propTypes = {
closeHelpModal: PropTypes.func.isRequired,
createQuestion: PropTypes.func.isRequired,
executeGA: PropTypes.func,
isOpen: PropTypes.bool
};
@ -27,9 +28,9 @@ const RSA =
export class HelpModal extends Component {
render() {
const { isOpen, closeHelpModal, createQuestion } = this.props;
const { isOpen, closeHelpModal, createQuestion, executeGA } = this.props;
if (isOpen) {
ga.modalview('/help-modal');
executeGA({ type: 'modal', data: '/help-modal' });
}
return (
<Modal dialogClassName='help-modal' onHide={closeHelpModal} show={isOpen}>

View File

@ -5,13 +5,14 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Button, Modal } from '@freecodecamp/react-bootstrap';
import ga from '../../../analytics';
import { isResetModalOpenSelector, closeModal, resetChallenge } from '../redux';
import { executeGA } from '../../../redux';
import './reset-modal.css';
const propTypes = {
close: PropTypes.func.isRequired,
executeGA: PropTypes.func,
isOpen: PropTypes.bool.isRequired,
reset: PropTypes.func.isRequired
};
@ -25,7 +26,11 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch =>
bindActionCreators(
{ close: () => closeModal('reset'), reset: () => resetChallenge() },
{
close: () => closeModal('reset'),
executeGA,
reset: () => resetChallenge()
},
dispatch
);
@ -35,7 +40,7 @@ function withActions(...fns) {
function ResetModal({ reset, close, isOpen }) {
if (isOpen) {
ga.modalview('/reset-modal');
executeGA({ type: 'modal', data: '/reset-modal' });
}
return (
<Modal

View File

@ -4,26 +4,30 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Modal } from '@freecodecamp/react-bootstrap';
import ga from '../../../analytics';
import { closeModal, isVideoModalOpenSelector } from '../redux';
import { executeGA } from '../../../redux';
import './video-modal.css';
const mapStateToProps = state => ({ isOpen: isVideoModalOpenSelector(state) });
const mapDispatchToProps = dispatch =>
bindActionCreators({ closeVideoModal: () => closeModal('video') }, dispatch);
bindActionCreators(
{ closeVideoModal: () => closeModal('video'), executeGA },
dispatch
);
const propTypes = {
closeVideoModal: PropTypes.func.isRequired,
executeGA: PropTypes.func,
isOpen: PropTypes.bool,
videoUrl: PropTypes.string
};
export class VideoModal extends Component {
render() {
const { isOpen, closeVideoModal, videoUrl } = this.props;
const { isOpen, closeVideoModal, videoUrl, executeGA } = this.props;
if (isOpen) {
ga.modalview('/video-modal');
executeGA({ type: 'modal', data: '/completion-modal' });
}
return (
<Modal