From 7d642208522d36ba6f9a10a533972d1448817be8 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 10 Apr 2024 17:35:48 +0000
Subject: [PATCH 01/82] chore(deps): update dependency babel-plugin-formatjs to
v10.5.14
---
package-lock.json | 8 ++++----
package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 37648b1c..091761a3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -51,7 +51,7 @@
"@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "13.0.28",
- "babel-plugin-formatjs": "10.5.13",
+ "babel-plugin-formatjs": "10.5.14",
"eslint-plugin-import": "2.29.1",
"glob": "7.2.3",
"history": "5.3.0",
@@ -7022,9 +7022,9 @@
}
},
"node_modules/babel-plugin-formatjs": {
- "version": "10.5.13",
- "resolved": "https://registry.npmjs.org/babel-plugin-formatjs/-/babel-plugin-formatjs-10.5.13.tgz",
- "integrity": "sha512-0dtMhoa6q0P5lUHBphLd8/y+CRlh5IG3Rq+Wk64kOEDwUVZF8xq1qMSjC3iw1wH4tmP2qTzBL+Bz1xEjyBd/ew==",
+ "version": "10.5.14",
+ "resolved": "https://registry.npmjs.org/babel-plugin-formatjs/-/babel-plugin-formatjs-10.5.14.tgz",
+ "integrity": "sha512-F++4Txa2bxrjbvspLxIXBDwcbKOftUvGgBDlz0hGe/fp7eXSPBJcu49xWj0/5VyMgSad4gKmBzWE5bfC27x19Q==",
"dependencies": {
"@babel/core": "^7.10.4",
"@babel/helper-plugin-utils": "^7.10.4",
diff --git a/package.json b/package.json
index 3aa662c1..02a75d25 100644
--- a/package.json
+++ b/package.json
@@ -74,7 +74,7 @@
"@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "13.0.28",
- "babel-plugin-formatjs": "10.5.13",
+ "babel-plugin-formatjs": "10.5.14",
"eslint-plugin-import": "2.29.1",
"glob": "7.2.3",
"history": "5.3.0",
From faf4ff84889a43ea21f5e03a4b30d47950c3c25a Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 10 Apr 2024 19:13:00 +0000
Subject: [PATCH 02/82] fix(deps): update dependency @edx/frontend-platform to
v7.1.3
---
package-lock.json | 24 ++++++++++++------------
package.json | 2 +-
2 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 091761a3..33a45337 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
- "@edx/frontend-platform": "7.1.2",
+ "@edx/frontend-platform": "7.1.3",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-brands-svg-icons": "6.5.1",
@@ -2283,9 +2283,9 @@
}
},
"node_modules/@edx/frontend-platform": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-7.1.2.tgz",
- "integrity": "sha512-iKd8n6QoFjaATEfLOYe+gsJLHFoGWpZbX2Z4D6hM+OthTISE98K8+/7Gx7PAcT9vlJEA33OVIThncZYwYaaluw==",
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-7.1.3.tgz",
+ "integrity": "sha512-klw0pvFnAo8k8iVPxcR9WCPLaYqV2AFdnz1COF6hJ6gOIFuj2Mb5cuEy9di89INpZFO/0FEHq1IBVojm1rbJcg==",
"dependencies": {
"@cospired/i18n-iso-languages": "4.2.0",
"@formatjs/intl-pluralrules": "4.3.3",
@@ -2304,7 +2304,7 @@
"lodash.merge": "4.6.2",
"lodash.snakecase": "4.1.1",
"pubsub-js": "1.9.4",
- "react-intl": "6.6.2",
+ "react-intl": "6.6.3",
"universal-cookie": "4.0.4"
},
"bin": {
@@ -2573,9 +2573,9 @@
}
},
"node_modules/@formatjs/intl": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.0.tgz",
- "integrity": "sha512-X3xT9guVkKDS86EKV80lS0KxoazUglkJTGZO66sKY7otgl0VeStPA8B3u8UkKT47PexVV98fUzjpkchYmbe9nw==",
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.1.tgz",
+ "integrity": "sha512-dsLG15U7xDi8yzKf4hcAWSsCaez3XrjTO2oaRHPyHtXLm1aEzYbDw6bClo/HMHu+iwS5GbDqT3DV+hYP2ylScg==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.18.2",
"@formatjs/fast-memoize": "2.2.0",
@@ -18118,13 +18118,13 @@
}
},
"node_modules/react-intl": {
- "version": "6.6.2",
- "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.6.2.tgz",
- "integrity": "sha512-IpW2IkLtGENSFlX3vfH11rjuCIsW0VyjT0Q1pPKMZPtT2z1FxLt4weFT5Ezti2TScT1xiyb3aQBFth9EB7jzAg==",
+ "version": "6.6.3",
+ "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.6.3.tgz",
+ "integrity": "sha512-vLKI0f+Q3pXD5szmCUPikTY7CJDPtxCBMG5YABQZ3IGEKzNB47zlvXyasUFfT25zpgQXeOfhRCdx4q6ubuR6bA==",
"dependencies": {
"@formatjs/ecma402-abstract": "1.18.2",
"@formatjs/icu-messageformat-parser": "2.7.6",
- "@formatjs/intl": "2.10.0",
+ "@formatjs/intl": "2.10.1",
"@formatjs/intl-displaynames": "6.6.6",
"@formatjs/intl-listformat": "7.5.5",
"@types/hoist-non-react-statics": "^3.3.1",
diff --git a/package.json b/package.json
index 02a75d25..57a05824 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
- "@edx/frontend-platform": "7.1.2",
+ "@edx/frontend-platform": "7.1.3",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-brands-svg-icons": "6.5.1",
From cf2b50005bea8255194fc281303b1f1706153f01 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 10 Apr 2024 21:26:38 +0000
Subject: [PATCH 03/82] fix(deps): update font awesome to v6.5.2
---
package-lock.json | 36 ++++++++++++++++++------------------
package.json | 6 +++---
2 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 33a45337..c6bf1d4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,9 +12,9 @@
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-platform": "7.1.3",
"@edx/openedx-atlas": "^0.6.0",
- "@fortawesome/fontawesome-svg-core": "6.5.1",
- "@fortawesome/free-brands-svg-icons": "6.5.1",
- "@fortawesome/free-solid-svg-icons": "6.5.1",
+ "@fortawesome/fontawesome-svg-core": "6.5.2",
+ "@fortawesome/free-brands-svg-icons": "6.5.2",
+ "@fortawesome/free-solid-svg-icons": "6.5.2",
"@fortawesome/react-fontawesome": "0.2.0",
"@openedx/paragon": "^22.1.1",
"@optimizely/react-sdk": "^2.9.1",
@@ -2694,45 +2694,45 @@
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
- "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz",
+ "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
- "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz",
+ "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==",
"hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "6.5.1"
+ "@fortawesome/fontawesome-common-types": "6.5.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-brands-svg-icons": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.1.tgz",
- "integrity": "sha512-093l7DAkx0aEtBq66Sf19MgoZewv1zeY9/4C7vSKPO4qMwEsW/2VYTUTpBtLwfb9T2R73tXaRDPmE4UqLCYHfg==",
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz",
+ "integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==",
"hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "6.5.1"
+ "@fortawesome/fontawesome-common-types": "6.5.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
- "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz",
+ "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==",
"hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "6.5.1"
+ "@fortawesome/fontawesome-common-types": "6.5.2"
},
"engines": {
"node": ">=6"
diff --git a/package.json b/package.json
index 57a05824..28904b24 100644
--- a/package.json
+++ b/package.json
@@ -35,9 +35,9 @@
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-platform": "7.1.3",
"@edx/openedx-atlas": "^0.6.0",
- "@fortawesome/fontawesome-svg-core": "6.5.1",
- "@fortawesome/free-brands-svg-icons": "6.5.1",
- "@fortawesome/free-solid-svg-icons": "6.5.1",
+ "@fortawesome/fontawesome-svg-core": "6.5.2",
+ "@fortawesome/free-brands-svg-icons": "6.5.2",
+ "@fortawesome/free-solid-svg-icons": "6.5.2",
"@fortawesome/react-fontawesome": "0.2.0",
"@openedx/paragon": "^22.1.1",
"@optimizely/react-sdk": "^2.9.1",
From 65e95a4d1bc5570fd0322d599f497b7d2312bd78 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 11 Apr 2024 01:48:26 +0000
Subject: [PATCH 04/82] chore(deps): update dependency @openedx/frontend-build
to v13.1.4
---
package-lock.json | 450 +++++++++++++++++++++++++++++++++++++++-------
package.json | 2 +-
2 files changed, 387 insertions(+), 65 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index c6bf1d4d..6ef5d2e5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -50,7 +50,7 @@
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "1.1.0",
- "@openedx/frontend-build": "13.0.28",
+ "@openedx/frontend-build": "13.1.4",
"babel-plugin-formatjs": "10.5.14",
"eslint-plugin-import": "2.29.1",
"glob": "7.2.3",
@@ -2167,9 +2167,9 @@
}
},
"node_modules/@csstools/cascade-layer-name-parser": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.7.tgz",
- "integrity": "sha512-9J4aMRJ7A2WRjaRLvsMeWrL69FmEuijtiW1XlK/sG+V0UJiHVYUyvj9mY4WAXfU/hGIiGOgL8e0jJcRyaZTjDQ==",
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.9.tgz",
+ "integrity": "sha512-RRqNjxTZDUhx7pxYOBG/AkCVmPS3zYzfE47GEhIGkFuWFTQGJBgWOUUkKNo5MfxIfjDz5/1L3F3rF1oIsYaIpw==",
"funding": [
{
"type": "github",
@@ -2184,14 +2184,14 @@
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
- "@csstools/css-parser-algorithms": "^2.5.0",
- "@csstools/css-tokenizer": "^2.2.3"
+ "@csstools/css-parser-algorithms": "^2.6.1",
+ "@csstools/css-tokenizer": "^2.2.4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz",
- "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==",
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz",
+ "integrity": "sha512-ubEkAaTfVZa+WwGhs5jbo5Xfqpeaybr/RvWzvFxRs4jfq16wH8l8Ty/QEEpINxll4xhuGfdMbipRyz5QZh9+FA==",
"funding": [
{
"type": "github",
@@ -2206,13 +2206,13 @@
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
- "@csstools/css-tokenizer": "^2.2.3"
+ "@csstools/css-tokenizer": "^2.2.4"
}
},
"node_modules/@csstools/css-tokenizer": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz",
- "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==",
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.4.tgz",
+ "integrity": "sha512-PuWRAewQLbDhGeTvFuq2oClaSCKPIBmHyIobCV39JHRYN0byDcUWJl5baPeNUcqrjtdMNqFooE0FGl31I3JOqw==",
"funding": [
{
"type": "github",
@@ -2228,9 +2228,9 @@
}
},
"node_modules/@csstools/media-query-list-parser": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.7.tgz",
- "integrity": "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ==",
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.9.tgz",
+ "integrity": "sha512-qqGuFfbn4rUmyOB0u8CVISIp5FfJ5GAR3mBrZ9/TKndHakdnm6pY0L/fbLcpPnrzwCyyTEZl1nUcXAYHEWneTA==",
"funding": [
{
"type": "github",
@@ -2245,8 +2245,8 @@
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
- "@csstools/css-parser-algorithms": "^2.5.0",
- "@csstools/css-tokenizer": "^2.2.3"
+ "@csstools/css-parser-algorithms": "^2.6.1",
+ "@csstools/css-tokenizer": "^2.2.4"
}
},
"node_modules/@discoveryjs/json-ext": {
@@ -3932,9 +3932,9 @@
}
},
"node_modules/@openedx/frontend-build": {
- "version": "13.0.28",
- "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-13.0.28.tgz",
- "integrity": "sha512-9CSTpA7EGlC5lHaH03X3E1YpCp2SPsJ5It7905KimacGlliK0yf8FZIyluOCm3jSGQ5u/xjvZyw6xbl0XJCKZA==",
+ "version": "13.1.4",
+ "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-13.1.4.tgz",
+ "integrity": "sha512-YqXU6KgFnmDD/vGLvq/A9NP6R8lHfaEx64ajQC50ebFMlF3J7HWUruct4PuroeTupq9UAfRZUilzQHYQZjV08A==",
"dependencies": {
"@babel/cli": "7.22.5",
"@babel/core": "7.22.5",
@@ -3950,7 +3950,7 @@
"@fullhuman/postcss-purgecss": "5.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
"@svgr/webpack": "8.1.0",
- "autoprefixer": "10.4.16",
+ "autoprefixer": "10.4.19",
"babel-jest": "26.6.3",
"babel-loader": "9.1.3",
"babel-plugin-formatjs": "^10.4.0",
@@ -3964,19 +3964,20 @@
"dotenv-webpack": "8.0.1",
"eslint": "8.44.0",
"eslint-config-airbnb": "19.0.4",
+ "eslint-plugin-formatjs": "^4.12.2",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
- "express": "4.18.2",
+ "express": "^4.18.2",
"file-loader": "6.2.0",
"html-webpack-plugin": "5.6.0",
"identity-obj-proxy": "3.0.0",
"image-minimizer-webpack-plugin": "3.8.3",
"jest": "26.6.3",
"mini-css-extract-plugin": "1.6.2",
- "postcss": "8.4.33",
- "postcss-custom-media": "10.0.2",
+ "postcss": "8.4.38",
+ "postcss-custom-media": "10.0.4",
"postcss-loader": "7.3.4",
"postcss-rtlcss": "5.1.0",
"react-dev-utils": "12.0.1",
@@ -3988,11 +3989,11 @@
"source-map-loader": "4.0.2",
"style-loader": "3.3.4",
"url-loader": "4.1.1",
- "webpack": "5.89.0",
- "webpack-bundle-analyzer": "4.10.1",
- "webpack-cli": "5.1.4",
- "webpack-dev-server": "4.15.1",
- "webpack-merge": "5.10.0"
+ "webpack": "^5.89.0",
+ "webpack-bundle-analyzer": "^4.10.1",
+ "webpack-cli": "^5.1.4",
+ "webpack-dev-server": "^4.15.1",
+ "webpack-merge": "^5.10.0"
},
"bin": {
"fedx-scripts": "bin/fedx-scripts.js"
@@ -4792,9 +4793,9 @@
}
},
"node_modules/@openedx/frontend-build/node_modules/jest-snapshot/node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -6007,6 +6008,11 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
},
+ "node_modules/@types/picomatch": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.3.tgz",
+ "integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg=="
+ },
"node_modules/@types/prettier": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz",
@@ -6074,6 +6080,11 @@
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="
},
+ "node_modules/@types/semver": {
+ "version": "7.5.8",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="
+ },
"node_modules/@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
@@ -6140,6 +6151,229 @@
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="
},
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+ "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
+ "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+ "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "9.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+ "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/semver": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+ "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -6838,9 +7072,9 @@
}
},
"node_modules/autoprefixer": {
- "version": "10.4.16",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
- "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
+ "version": "10.4.19",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
+ "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
"funding": [
{
"type": "opencollective",
@@ -6856,9 +7090,9 @@
}
],
"dependencies": {
- "browserslist": "^4.21.10",
- "caniuse-lite": "^1.0.30001538",
- "fraction.js": "^4.3.6",
+ "browserslist": "^4.23.0",
+ "caniuse-lite": "^1.0.30001599",
+ "fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
@@ -7413,9 +7647,9 @@
"integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
},
"node_modules/browserslist": {
- "version": "4.22.2",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
- "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
+ "version": "4.23.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
+ "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"funding": [
{
"type": "opencollective",
@@ -7431,8 +7665,8 @@
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001565",
- "electron-to-chromium": "^1.4.601",
+ "caniuse-lite": "^1.0.30001587",
+ "electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
@@ -7564,9 +7798,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001579",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz",
- "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==",
+ "version": "1.0.30001608",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001608.tgz",
+ "integrity": "sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==",
"funding": [
{
"type": "opencollective",
@@ -9015,9 +9249,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/electron-to-chromium": {
- "version": "1.4.643",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.643.tgz",
- "integrity": "sha512-QHscvvS7gt155PtoRC0dR2ilhL8E9LHhfTQEq1uD5AL0524rBLAwpAREFH06f87/e45B9XkR6Ki5dbhbCsVEIg=="
+ "version": "1.4.733",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.733.tgz",
+ "integrity": "sha512-gUI9nhI2iBGF0OaYYLKOaOtliFMl+Bt1rY7VmEjwxOxqoYLub/D9xmduPEhbw2imE6gYkJKhIE5it+KE2ulVxQ=="
},
"node_modules/email-prop-type": {
"version": "3.0.1",
@@ -9427,6 +9661,59 @@
"ms": "^2.1.1"
}
},
+ "node_modules/eslint-plugin-formatjs": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-formatjs/-/eslint-plugin-formatjs-4.13.0.tgz",
+ "integrity": "sha512-sxgHQNyVclNRO7aydGwxohwxYR03/oRDW0uUXFWayNMPTlnb9sET3LCovBjvQF7qAHDGFDcLwg4ECSyui4nG8A==",
+ "dependencies": {
+ "@formatjs/icu-messageformat-parser": "2.7.6",
+ "@formatjs/ts-transformer": "3.13.12",
+ "@types/eslint": "7 || 8",
+ "@types/picomatch": "^2.3.0",
+ "@typescript-eslint/utils": "^6.18.1",
+ "emoji-regex": "^10.2.1",
+ "magic-string": "^0.30.0",
+ "picomatch": "^2.3.1",
+ "tslib": "2.6.2",
+ "typescript": "5",
+ "unicode-emoji-utils": "^1.2.0"
+ },
+ "peerDependencies": {
+ "eslint": "7 || 8"
+ }
+ },
+ "node_modules/eslint-plugin-formatjs/node_modules/@formatjs/ts-transformer": {
+ "version": "3.13.12",
+ "resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-3.13.12.tgz",
+ "integrity": "sha512-uf1+DgbsCrzHAg7uIf0QlzpIkHYxRSRig5iJa9FaoUNIDZzNEE2oW/uLLLq7I9Z2FLIPhbmgq8hbW40FoQv+Fg==",
+ "dependencies": {
+ "@formatjs/icu-messageformat-parser": "2.7.6",
+ "@types/json-stable-stringify": "^1.0.32",
+ "@types/node": "14 || 16 || 17",
+ "chalk": "^4.0.0",
+ "json-stable-stringify": "^1.0.1",
+ "tslib": "^2.4.0",
+ "typescript": "5"
+ },
+ "peerDependencies": {
+ "ts-jest": ">=27"
+ },
+ "peerDependenciesMeta": {
+ "ts-jest": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-formatjs/node_modules/@types/node": {
+ "version": "17.0.45",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
+ "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="
+ },
+ "node_modules/eslint-plugin-formatjs/node_modules/emoji-regex": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
+ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
+ },
"node_modules/eslint-plugin-import": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
@@ -15628,6 +15915,17 @@
"lz-string": "bin/bin.js"
}
},
+ "node_modules/magic-string": {
+ "version": "0.30.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz",
+ "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/mailto-link": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mailto-link/-/mailto-link-2.0.0.tgz",
@@ -16917,9 +17215,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.33",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
- "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
+ "version": "8.4.38",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"funding": [
{
"type": "opencollective",
@@ -16937,7 +17235,7 @@
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
+ "source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -16991,9 +17289,9 @@
}
},
"node_modules/postcss-custom-media": {
- "version": "10.0.2",
- "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.2.tgz",
- "integrity": "sha512-zcEFNRmDm2fZvTPdI1pIW3W//UruMcLosmMiCdpQnrCsTRzWlKQPYMa1ud9auL0BmrryKK1+JjIGn19K0UjO/w==",
+ "version": "10.0.4",
+ "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.4.tgz",
+ "integrity": "sha512-Ubs7O3wj2prghaKRa68VHBvuy3KnTQ0zbGwqDYY1mntxJD0QL2AeiAy+AMfl3HBedTCVr2IcFNktwty9YpSskA==",
"funding": [
{
"type": "github",
@@ -17005,10 +17303,10 @@
}
],
"dependencies": {
- "@csstools/cascade-layer-name-parser": "^1.0.5",
- "@csstools/css-parser-algorithms": "^2.3.2",
- "@csstools/css-tokenizer": "^2.2.1",
- "@csstools/media-query-list-parser": "^2.1.5"
+ "@csstools/cascade-layer-name-parser": "^1.0.9",
+ "@csstools/css-parser-algorithms": "^2.6.1",
+ "@csstools/css-tokenizer": "^2.2.4",
+ "@csstools/media-query-list-parser": "^2.1.9"
},
"engines": {
"node": "^14 || ^16 || >=18"
@@ -20042,9 +20340,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
+ "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"engines": {
"node": ">=0.10.0"
}
@@ -20980,6 +21278,17 @@
"node": ">=8"
}
},
+ "node_modules/ts-api-utils": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+ "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@@ -21211,6 +21520,19 @@
"node": ">=4"
}
},
+ "node_modules/unicode-emoji-utils": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/unicode-emoji-utils/-/unicode-emoji-utils-1.2.0.tgz",
+ "integrity": "sha512-djUB91p/6oYpgps4W5K/MAvM+UspoAANHSUW495BrxeLRoned3iNPEDQgrKx9LbLq93VhNz0NWvI61vcfrwYoA==",
+ "dependencies": {
+ "emoji-regex": "10.3.0"
+ }
+ },
+ "node_modules/unicode-emoji-utils/node_modules/emoji-regex": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
+ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
+ },
"node_modules/unicode-match-property-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
diff --git a/package.json b/package.json
index 28904b24..5b938c2c 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "1.1.0",
- "@openedx/frontend-build": "13.0.28",
+ "@openedx/frontend-build": "13.1.4",
"babel-plugin-formatjs": "10.5.14",
"eslint-plugin-import": "2.29.1",
"glob": "7.2.3",
From 564ec70d9e3d1bdd4b904184f0be047717776f83 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 11 Apr 2024 04:08:08 +0000
Subject: [PATCH 05/82] fix(deps): update dependency @openedx/paragon to
v22.2.1
---
package-lock.json | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 6ef5d2e5..824226d7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5055,9 +5055,16 @@
}
},
"node_modules/@openedx/paragon": {
- "version": "22.1.1",
- "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.1.1.tgz",
- "integrity": "sha512-XPRuV9zn7BeCIYfU5kE2XZ4YevjA0wfS/fuydB8Ta/aNY1dw9fQ7CjHOIfkZqDic4Jygusj/uhE/1WYJD8kvyw==",
+ "version": "22.2.1",
+ "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.2.1.tgz",
+ "integrity": "sha512-Dd7PzvHwNnUokqbFkuOpugJZ9dHaUBOcYwqAA2aMoN7tgi4xEZWsfDFyP1+se2UPuR7NvNGammEesLAwGQ0Ylw==",
+ "workspaces": [
+ "example",
+ "component-generator",
+ "www",
+ "icons",
+ "dependent-usage-analyzer"
+ ],
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
From 611af073268759470187c8f3edf863c73ec85548 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 11 Apr 2024 06:43:02 +0000
Subject: [PATCH 06/82] fix(deps): update dependency algoliasearch to v4.23.3
---
package-lock.json | 183 +++++++++++++++++++++++++---------------------
1 file changed, 101 insertions(+), 82 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 824226d7..5dad67b3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -69,74 +69,74 @@
}
},
"node_modules/@algolia/cache-browser-local-storage": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.22.1.tgz",
- "integrity": "sha512-Sw6IAmOCvvP6QNgY9j+Hv09mvkvEIDKjYW8ow0UDDAxSXy664RBNQk3i/0nt7gvceOJ6jGmOTimaZoY1THmU7g==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.23.3.tgz",
+ "integrity": "sha512-vRHXYCpPlTDE7i6UOy2xE03zHF2C8MEFjPN2v7fRbqVpcOvAUQK81x3Kc21xyb5aSIpYCjWCZbYZuz8Glyzyyg==",
"dependencies": {
- "@algolia/cache-common": "4.22.1"
+ "@algolia/cache-common": "4.23.3"
}
},
"node_modules/@algolia/cache-common": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.22.1.tgz",
- "integrity": "sha512-TJMBKqZNKYB9TptRRjSUtevJeQVXRmg6rk9qgFKWvOy8jhCPdyNZV1nB3SKGufzvTVbomAukFR8guu/8NRKBTA=="
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.23.3.tgz",
+ "integrity": "sha512-h9XcNI6lxYStaw32pHpB1TMm0RuxphF+Ik4o7tcQiodEdpKK+wKufY6QXtba7t3k8eseirEMVB83uFFF3Nu54A=="
},
"node_modules/@algolia/cache-in-memory": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.22.1.tgz",
- "integrity": "sha512-ve+6Ac2LhwpufuWavM/aHjLoNz/Z/sYSgNIXsinGofWOysPilQZPUetqLj8vbvi+DHZZaYSEP9H5SRVXnpsNNw==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.23.3.tgz",
+ "integrity": "sha512-yvpbuUXg/+0rbcagxNT7un0eo3czx2Uf0y4eiR4z4SD7SiptwYTpbuS0IHxcLHG3lq22ukx1T6Kjtk/rT+mqNg==",
"dependencies": {
- "@algolia/cache-common": "4.22.1"
+ "@algolia/cache-common": "4.23.3"
}
},
"node_modules/@algolia/client-account": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.22.1.tgz",
- "integrity": "sha512-k8m+oegM2zlns/TwZyi4YgCtyToackkOpE+xCaKCYfBfDtdGOaVZCM5YvGPtK+HGaJMIN/DoTL8asbM3NzHonw==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.23.3.tgz",
+ "integrity": "sha512-hpa6S5d7iQmretHHF40QGq6hz0anWEHGlULcTIT9tbUssWUriN9AUXIFQ8Ei4w9azD0hc1rUok9/DeQQobhQMA==",
"dependencies": {
- "@algolia/client-common": "4.22.1",
- "@algolia/client-search": "4.22.1",
- "@algolia/transporter": "4.22.1"
+ "@algolia/client-common": "4.23.3",
+ "@algolia/client-search": "4.23.3",
+ "@algolia/transporter": "4.23.3"
}
},
"node_modules/@algolia/client-analytics": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.22.1.tgz",
- "integrity": "sha512-1ssi9pyxyQNN4a7Ji9R50nSdISIumMFDwKNuwZipB6TkauJ8J7ha/uO60sPJFqQyqvvI+px7RSNRQT3Zrvzieg==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.23.3.tgz",
+ "integrity": "sha512-LBsEARGS9cj8VkTAVEZphjxTjMVCci+zIIiRhpFun9jGDUlS1XmhCW7CTrnaWeIuCQS/2iPyRqSy1nXPjcBLRA==",
"dependencies": {
- "@algolia/client-common": "4.22.1",
- "@algolia/client-search": "4.22.1",
- "@algolia/requester-common": "4.22.1",
- "@algolia/transporter": "4.22.1"
+ "@algolia/client-common": "4.23.3",
+ "@algolia/client-search": "4.23.3",
+ "@algolia/requester-common": "4.23.3",
+ "@algolia/transporter": "4.23.3"
}
},
"node_modules/@algolia/client-common": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.22.1.tgz",
- "integrity": "sha512-IvaL5v9mZtm4k4QHbBGDmU3wa/mKokmqNBqPj0K7lcR8ZDKzUorhcGp/u8PkPC/e0zoHSTvRh7TRkGX3Lm7iOQ==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.23.3.tgz",
+ "integrity": "sha512-l6EiPxdAlg8CYhroqS5ybfIczsGUIAC47slLPOMDeKSVXYG1n0qGiz4RjAHLw2aD0xzh2EXZ7aRguPfz7UKDKw==",
"dependencies": {
- "@algolia/requester-common": "4.22.1",
- "@algolia/transporter": "4.22.1"
+ "@algolia/requester-common": "4.23.3",
+ "@algolia/transporter": "4.23.3"
}
},
"node_modules/@algolia/client-personalization": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.22.1.tgz",
- "integrity": "sha512-sl+/klQJ93+4yaqZ7ezOttMQ/nczly/3GmgZXJ1xmoewP5jmdP/X/nV5U7EHHH3hCUEHeN7X1nsIhGPVt9E1cQ==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.23.3.tgz",
+ "integrity": "sha512-3E3yF3Ocr1tB/xOZiuC3doHQBQ2zu2MPTYZ0d4lpfWads2WTKG7ZzmGnsHmm63RflvDeLK/UVx7j2b3QuwKQ2g==",
"dependencies": {
- "@algolia/client-common": "4.22.1",
- "@algolia/requester-common": "4.22.1",
- "@algolia/transporter": "4.22.1"
+ "@algolia/client-common": "4.23.3",
+ "@algolia/requester-common": "4.23.3",
+ "@algolia/transporter": "4.23.3"
}
},
"node_modules/@algolia/client-search": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.22.1.tgz",
- "integrity": "sha512-yb05NA4tNaOgx3+rOxAmFztgMTtGBi97X7PC3jyNeGiwkAjOZc2QrdZBYyIdcDLoI09N0gjtpClcackoTN0gPA==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.23.3.tgz",
+ "integrity": "sha512-P4VAKFHqU0wx9O+q29Q8YVuaowaZ5EM77rxfmGnkHUJggh28useXQdopokgwMeYw2XUht49WX5RcTQ40rZIabw==",
"dependencies": {
- "@algolia/client-common": "4.22.1",
- "@algolia/requester-common": "4.22.1",
- "@algolia/transporter": "4.22.1"
+ "@algolia/client-common": "4.23.3",
+ "@algolia/requester-common": "4.23.3",
+ "@algolia/transporter": "4.23.3"
}
},
"node_modules/@algolia/events": {
@@ -145,47 +145,65 @@
"integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ=="
},
"node_modules/@algolia/logger-common": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.22.1.tgz",
- "integrity": "sha512-OnTFymd2odHSO39r4DSWRFETkBufnY2iGUZNrMXpIhF5cmFE8pGoINNPzwg02QLBlGSaLqdKy0bM8S0GyqPLBg=="
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.23.3.tgz",
+ "integrity": "sha512-y9kBtmJwiZ9ZZ+1Ek66P0M68mHQzKRxkW5kAAXYN/rdzgDN0d2COsViEFufxJ0pb45K4FRcfC7+33YB4BLrZ+g=="
},
"node_modules/@algolia/logger-console": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.22.1.tgz",
- "integrity": "sha512-O99rcqpVPKN1RlpgD6H3khUWylU24OXlzkavUAMy6QZd1776QAcauE3oP8CmD43nbaTjBexZj2nGsBH9Tc0FVA==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.23.3.tgz",
+ "integrity": "sha512-8xoiseoWDKuCVnWP8jHthgaeobDLolh00KJAdMe9XPrWPuf1by732jSpgy2BlsLTaT9m32pHI8CRfrOqQzHv3A==",
"dependencies": {
- "@algolia/logger-common": "4.22.1"
+ "@algolia/logger-common": "4.23.3"
+ }
+ },
+ "node_modules/@algolia/recommend": {
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-4.23.3.tgz",
+ "integrity": "sha512-9fK4nXZF0bFkdcLBRDexsnGzVmu4TSYZqxdpgBW2tEyfuSSY54D4qSRkLmNkrrz4YFvdh2GM1gA8vSsnZPR73w==",
+ "dependencies": {
+ "@algolia/cache-browser-local-storage": "4.23.3",
+ "@algolia/cache-common": "4.23.3",
+ "@algolia/cache-in-memory": "4.23.3",
+ "@algolia/client-common": "4.23.3",
+ "@algolia/client-search": "4.23.3",
+ "@algolia/logger-common": "4.23.3",
+ "@algolia/logger-console": "4.23.3",
+ "@algolia/requester-browser-xhr": "4.23.3",
+ "@algolia/requester-common": "4.23.3",
+ "@algolia/requester-node-http": "4.23.3",
+ "@algolia/transporter": "4.23.3"
}
},
"node_modules/@algolia/requester-browser-xhr": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.22.1.tgz",
- "integrity": "sha512-dtQGYIg6MteqT1Uay3J/0NDqD+UciHy3QgRbk7bNddOJu+p3hzjTRYESqEnoX/DpEkaNYdRHUKNylsqMpgwaEw==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.23.3.tgz",
+ "integrity": "sha512-jDWGIQ96BhXbmONAQsasIpTYWslyjkiGu0Quydjlowe+ciqySpiDUrJHERIRfELE5+wFc7hc1Q5hqjGoV7yghw==",
"dependencies": {
- "@algolia/requester-common": "4.22.1"
+ "@algolia/requester-common": "4.23.3"
}
},
"node_modules/@algolia/requester-common": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.22.1.tgz",
- "integrity": "sha512-dgvhSAtg2MJnR+BxrIFqlLtkLlVVhas9HgYKMk2Uxiy5m6/8HZBL40JVAMb2LovoPFs9I/EWIoFVjOrFwzn5Qg=="
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.23.3.tgz",
+ "integrity": "sha512-xloIdr/bedtYEGcXCiF2muajyvRhwop4cMZo+K2qzNht0CMzlRkm8YsDdj5IaBhshqfgmBb3rTg4sL4/PpvLYw=="
},
"node_modules/@algolia/requester-node-http": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.22.1.tgz",
- "integrity": "sha512-JfmZ3MVFQkAU+zug8H3s8rZ6h0ahHZL/SpMaSasTCGYR5EEJsCc8SI5UZ6raPN2tjxa5bxS13BRpGSBUens7EA==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.23.3.tgz",
+ "integrity": "sha512-zgu++8Uj03IWDEJM3fuNl34s746JnZOWn1Uz5taV1dFyJhVM/kTNw9Ik7YJWiUNHJQXcaD8IXD1eCb0nq/aByA==",
"dependencies": {
- "@algolia/requester-common": "4.22.1"
+ "@algolia/requester-common": "4.23.3"
}
},
"node_modules/@algolia/transporter": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.22.1.tgz",
- "integrity": "sha512-kzWgc2c9IdxMa3YqA6TN0NW5VrKYYW/BELIn7vnLyn+U/RFdZ4lxxt9/8yq3DKV5snvoDzzO4ClyejZRdV3lMQ==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.23.3.tgz",
+ "integrity": "sha512-Wjl5gttqnf/gQKJA+dafnD0Y6Yw97yvfY8R9h0dQltX1GXTgNs1zWgvtWW0tHl1EgMdhAyw189uWiZMnL3QebQ==",
"dependencies": {
- "@algolia/cache-common": "4.22.1",
- "@algolia/logger-common": "4.22.1",
- "@algolia/requester-common": "4.22.1"
+ "@algolia/cache-common": "4.23.3",
+ "@algolia/logger-common": "4.23.3",
+ "@algolia/requester-common": "4.23.3"
}
},
"node_modules/@ampproject/remapping": {
@@ -6741,24 +6759,25 @@
}
},
"node_modules/algoliasearch": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.22.1.tgz",
- "integrity": "sha512-jwydKFQJKIx9kIZ8Jm44SdpigFwRGPESaxZBaHSV0XWN2yBJAOT4mT7ppvlrpA4UGzz92pqFnVKr/kaZXrcreg==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.23.3.tgz",
+ "integrity": "sha512-Le/3YgNvjW9zxIQMRhUHuhiUjAlKY/zsdZpfq4dlLqg6mEm0nL6yk+7f2hDOtLpxsgE4jSzDmvHL7nXdBp5feg==",
"dependencies": {
- "@algolia/cache-browser-local-storage": "4.22.1",
- "@algolia/cache-common": "4.22.1",
- "@algolia/cache-in-memory": "4.22.1",
- "@algolia/client-account": "4.22.1",
- "@algolia/client-analytics": "4.22.1",
- "@algolia/client-common": "4.22.1",
- "@algolia/client-personalization": "4.22.1",
- "@algolia/client-search": "4.22.1",
- "@algolia/logger-common": "4.22.1",
- "@algolia/logger-console": "4.22.1",
- "@algolia/requester-browser-xhr": "4.22.1",
- "@algolia/requester-common": "4.22.1",
- "@algolia/requester-node-http": "4.22.1",
- "@algolia/transporter": "4.22.1"
+ "@algolia/cache-browser-local-storage": "4.23.3",
+ "@algolia/cache-common": "4.23.3",
+ "@algolia/cache-in-memory": "4.23.3",
+ "@algolia/client-account": "4.23.3",
+ "@algolia/client-analytics": "4.23.3",
+ "@algolia/client-common": "4.23.3",
+ "@algolia/client-personalization": "4.23.3",
+ "@algolia/client-search": "4.23.3",
+ "@algolia/logger-common": "4.23.3",
+ "@algolia/logger-console": "4.23.3",
+ "@algolia/recommend": "4.23.3",
+ "@algolia/requester-browser-xhr": "4.23.3",
+ "@algolia/requester-common": "4.23.3",
+ "@algolia/requester-node-http": "4.23.3",
+ "@algolia/transporter": "4.23.3"
}
},
"node_modules/algoliasearch-helper": {
From dba93333fded652989e9a02d4b168a84fb3fbc21 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 11 Apr 2024 09:17:34 +0000
Subject: [PATCH 07/82] fix(deps): update dependency algoliasearch-helper to
v3.17.0
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 5dad67b3..19e3f81c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6781,9 +6781,9 @@
}
},
"node_modules/algoliasearch-helper": {
- "version": "3.16.3",
- "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.16.3.tgz",
- "integrity": "sha512-1OuJT6sONAa9PxcOmWo5WCAT3jQSpCR9/m5Azujja7nhUQwAUDvaaAYrcmUySsrvHh74usZHbE3jFfGnWtZj8w==",
+ "version": "3.17.0",
+ "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.17.0.tgz",
+ "integrity": "sha512-R5422OiQjvjlK3VdpNQ/Qk7KsTIGeM5ACm8civGifOVWdRRV/3SgXuKmeNxe94Dz6fwj/IgpVmXbHutU4mHubg==",
"dependencies": {
"@algolia/events": "^4.0.1"
},
From c663f6fa30e6e848dfe551c69207cf1de0039d64 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Wed, 17 Apr 2024 17:00:28 +0500
Subject: [PATCH 08/82] Rebase 2u main with master (#1228)
* chore(deps): update dependency babel-plugin-formatjs to v10.5.14
* fix(deps): update dependency @edx/frontend-platform to v7.1.3
* fix(deps): update font awesome to v6.5.2
* chore(deps): update dependency @openedx/frontend-build to v13.1.4
* fix(deps): update dependency @openedx/paragon to v22.2.1
* fix(deps): update dependency algoliasearch to v4.23.3
* fix(deps): update dependency algoliasearch-helper to v3.17.0
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
From 6f325c20c323f36bc53082f27db6bcad2f530728 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Wed, 3 Apr 2024 11:28:39 +0500
Subject: [PATCH 09/82] feat: implement multi step registration experiment
---
src/common-components/messages.jsx | 6 +
src/config/index.js | 2 +
src/logistration/Logistration.jsx | 55 ++-
src/logistration/Logistration.test.jsx | 7 +
src/register/RegistrationPage.jsx | 341 +++++++++++++-----
src/register/RegistrationPage.test.jsx | 5 +
.../ConfigurableRegistrationForm.jsx | 24 +-
.../components/RegistrationFailure.jsx | 11 +-
.../ConfigurableRegistrationForm.test.jsx | 8 +
.../tests/RegistrationFailure.test.jsx | 5 +
.../components/tests/ThirdPartyAuth.test.jsx | 5 +
src/register/data/actions.js | 20 +-
.../data/optimizelyExperiment/helper.js | 105 ++++++
...ltiStepRegistrationExperimentVariation.jsx | 54 +++
src/register/data/reducers.js | 25 ++
src/register/data/sagas.js | 7 +-
src/register/data/tests/reducers.test.js | 5 +
src/register/data/utils.js | 37 +-
src/register/messages.jsx | 23 ++
src/sass/_registration.scss | 4 -
20 files changed, 610 insertions(+), 139 deletions(-)
create mode 100644 src/register/data/optimizelyExperiment/helper.js
create mode 100644 src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
diff --git a/src/common-components/messages.jsx b/src/common-components/messages.jsx
index 08e88b8f..6f0e69ca 100644
--- a/src/common-components/messages.jsx
+++ b/src/common-components/messages.jsx
@@ -132,6 +132,12 @@ const messages = defineMessages({
defaultMessage: 'Company or school credentials',
description: 'Company or school login link text.',
},
+ // multi step registration experiment messages
+ 'tab.back.btn.text': {
+ id: 'tab.back.btn.text',
+ defaultMessage: 'Back',
+ description: 'Tab back button text',
+ },
});
export default messages;
diff --git a/src/config/index.js b/src/config/index.js
index fa3613aa..5d221faa 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -34,6 +34,8 @@ const configuration = {
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
+ // Multi Step Registration Experiment
+ MULTI_STEP_REGISTRATION_EXPERIMENT_ID: process.env.MULTI_STEP_REGISTRATION_EXPERIMENT_ID || '',
};
export default configuration;
diff --git a/src/logistration/Logistration.jsx b/src/logistration/Logistration.jsx
index 9451aa27..5199e1b3 100644
--- a/src/logistration/Logistration.jsx
+++ b/src/logistration/Logistration.jsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import { connect } from 'react-redux';
+import { connect, useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -7,10 +7,11 @@ import { getAuthService } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Icon,
+ IconButton,
Tab,
Tabs,
} from '@openedx/paragon';
-import { ChevronLeft } from '@openedx/paragon/icons';
+import { ArrowBackIos, ChevronLeft } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { Navigate, useNavigate } from 'react-router-dom';
@@ -27,7 +28,11 @@ import {
import { LoginPage } from '../login';
import { backupLoginForm } from '../login/data/actions';
import { RegistrationPage } from '../register';
-import { backupRegistrationForm } from '../register/data/actions';
+import { backupRegistrationForm, setMultiStepRegistrationExpData } from '../register/data/actions';
+import {
+ FIRST_STEP,
+ getMultiStepRegistrationPreviousStep,
+} from '../register/data/optimizelyExperiment/helper';
const Logistration = (props) => {
const { selectedPage, tpaProviders } = props;
@@ -42,6 +47,10 @@ const Logistration = (props) => {
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
const hideRegistrationLink = getConfig().SHOW_REGISTRATION_LINKS === false;
+ const dispatch = useDispatch();
+ const multiStepRegExpVariation = useSelector(state => state.register.multiStepRegExpVariation);
+ const multiStepRegistrationPageStep = useSelector(state => state.register.multiStepRegistrationPageStep);
+
useEffect(() => {
const authService = getAuthService();
if (authService) {
@@ -91,6 +100,39 @@ const Logistration = (props) => {
);
+ /**
+ * Temporary function created to resolve the complexity in tabs conditioning for multi-step
+ * registration experiment
+ */
+ const getTabs = () => {
+ if (multiStepRegistrationPageStep !== FIRST_STEP) {
+ const prevStep = getMultiStepRegistrationPreviousStep(multiStepRegistrationPageStep);
+ return (
+
+ {
+ dispatch(setMultiStepRegistrationExpData(multiStepRegExpVariation, prevStep));
+ }}
+ variant="primary"
+ size="inline"
+ className="mr-1"
+ />
+ {formatMessage(messages['tab.back.btn.text'])}
+
+ );
+ }
+ return (
+ handleOnSelect(tabKey, selectedPage)}>
+
+
+
+ );
+ };
+
const isValidTpaHint = () => {
const { provider } = getTpaProvider(tpaHint, providers, secondaryProviders);
return !!provider;
@@ -123,12 +165,7 @@ const Logistration = (props) => {
)
- : (!isValidTpaHint() && !hideRegistrationLink && (
- handleOnSelect(tabKey, selectedPage)}>
-
-
-
- ))}
+ : (!isValidTpaHint() && !hideRegistrationLink && getTabs())}
{ key && (
)}
diff --git a/src/logistration/Logistration.test.jsx b/src/logistration/Logistration.test.jsx
index 87bf3e70..ce7af88f 100644
--- a/src/logistration/Logistration.test.jsx
+++ b/src/logistration/Logistration.test.jsx
@@ -15,12 +15,16 @@ import {
} from '../data/constants';
import { backupLoginForm } from '../login/data/actions';
import { backupRegistrationForm } from '../register/data/actions';
+import { FIRST_STEP, NOT_INITIALIZED } from '../register/data/optimizelyExperiment/helper';
+import useMultiStepRegistrationExperimentVariation
+ from '../register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
+jest.mock('../register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
@@ -63,6 +67,8 @@ describe('Logistration', () => {
registrationError: {},
usernameSuggestions: [],
validationApiRateLimited: false,
+ multiStepRegExpVariation: '',
+ multiStepRegistrationPageStep: FIRST_STEP,
},
commonComponents: {
thirdPartyAuthContext: {
@@ -83,6 +89,7 @@ describe('Logistration', () => {
username: 'test-user',
})),
}));
+ useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({
loggingService: { logError: jest.fn() },
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 80ac1f6b..70897a31 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -17,14 +17,29 @@ import RegistrationFailure from './components/RegistrationFailure';
import {
backupRegistrationFormBegin,
clearRegistrationBackendError,
+ fetchRealtimeValidations,
registerNewUser,
setEmailSuggestionInStore,
+ setMultiStepRegistrationExpData,
setUserPipelineDataLoaded,
} from './data/actions';
import {
FORM_SUBMISSION_ERROR,
TPA_AUTHENTICATION_FAILURE,
} from './data/constants';
+import {
+ CONTROL,
+ FIRST_STEP,
+ getMultiStepRegistrationNextStep,
+ getRegisterButtonLabelInExperiment,
+ getRegisterButtonSubmitStateInExperiment,
+ MULTI_STEP_REGISTRATION_EXP_VARIATION,
+ NOT_INITIALIZED,
+ SECOND_STEP,
+ shouldDisplayFieldInExperiment, THIRD_STEP,
+} from './data/optimizelyExperiment/helper';
+import useMultiStepRegistrationExperimentVariation
+ from './data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import getBackendValidations from './data/selectors';
import {
isFormValid, prepareRegistrationPayload,
@@ -73,6 +88,13 @@ const RegistrationPage = (props) => {
const shouldBackupState = useSelector(state => state.register.shouldBackupState);
const userPipelineDataLoaded = useSelector(state => state.register.userPipelineDataLoaded);
const submitState = useSelector(state => state.register.submitState);
+ const backendValidations = useSelector(getBackendValidations);
+ const multiStepRegExpVariation = useSelector(state => state.register.multiStepRegExpVariation);
+ const multiStepRegistrationPageStep = useSelector(state => state.register.multiStepRegistrationPageStep);
+ const isValidatingMultiStepRegistrationPage = useSelector(
+ state => state.register.isValidatingMultiStepRegistrationPage,
+ );
+ const validationsSubmitState = useSelector(state => state.register.validationsSubmitState);
const fieldDescriptions = useSelector(state => state.commonComponents.fieldDescriptions);
const optionalFields = useSelector(state => state.commonComponents.optionalFields);
@@ -85,7 +107,6 @@ const RegistrationPage = (props) => {
const secondaryProviders = useSelector(state => state.commonComponents.thirdPartyAuthContext.secondaryProviders);
const pipelineUserDetails = useSelector(state => state.commonComponents.thirdPartyAuthContext.pipelineUserDetails);
- const backendValidations = useSelector(getBackendValidations);
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
const tpaHint = useMemo(() => getTpaHint(), []);
@@ -102,6 +123,26 @@ const RegistrationPage = (props) => {
? formatMessage(messages['create.account.cta.button'], { label: cta })
: formatMessage(messages['create.account.for.free.button']);
+ /**
+ * Multi-Step Registration Page Experiment
+ */
+ const multiStepRegistrationExpVariation = useMultiStepRegistrationExperimentVariation(
+ multiStepRegExpVariation, registrationEmbedded, tpaHint, currentProvider, thirdPartyAuthApiStatus,
+ );
+
+ useEffect(() => {
+ if (isValidatingMultiStepRegistrationPage && backendValidations
+ && Object.values(backendValidations).every(value => value === '')
+ ) {
+ setErrorCode({ type: '', count: 0 });
+ const nextStep = getMultiStepRegistrationNextStep(multiStepRegistrationPageStep);
+ dispatch(setMultiStepRegistrationExpData(multiStepRegistrationExpVariation, nextStep));
+ }
+ }, [ // eslint-disable-line react-hooks/exhaustive-deps
+ isValidatingMultiStepRegistrationPage,
+ backendValidations,
+ ]);
+
/**
* Set the userPipelineDetails data in formFields for only first time
*/
@@ -148,8 +189,11 @@ const RegistrationPage = (props) => {
formFields: { ...formFields },
errors: { ...errors },
}));
+ dispatch(setMultiStepRegistrationExpData(
+ multiStepRegistrationExpVariation, multiStepRegistrationPageStep, false,
+ ));
}
- }, [shouldBackupState, configurableFormFields, formFields, errors, dispatch, backedUpFormData]);
+ }, [shouldBackupState]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (backendValidations) {
@@ -169,13 +213,21 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (registrationResult.success) {
+ let registeredEventProps = {};
+
+ if (multiStepRegistrationExpVariation !== NOT_INITIALIZED) {
+ registeredEventProps = {
+ variation: multiStepRegistrationExpVariation,
+ };
+ }
+
// This event is used by GTM
- sendTrackEvent('edx.bi.user.account.registered.client', {});
+ sendTrackEvent('edx.bi.user.account.registered.client', registeredEventProps);
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
}
- }, [registrationResult]);
+ }, [registrationResult]); // eslint-disable-line react-hooks/exhaustive-deps
const handleOnChange = (event) => {
const { name } = event.target;
@@ -247,7 +299,56 @@ const RegistrationPage = (props) => {
const handleSubmit = (e) => {
e.preventDefault();
- registerUser();
+
+ if (multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION
+ && multiStepRegistrationPageStep !== THIRD_STEP) {
+ let formFieldsPayload = {};
+
+ if (multiStepRegistrationPageStep === FIRST_STEP) {
+ // We only want to validate email in the first step of registration
+ // Doing manual validations to avoid the case where user clicks CTA without focusing out of field.
+ formFieldsPayload = { email: formFields.email };
+ } else if (multiStepRegistrationPageStep === SECOND_STEP) {
+ // We only want to validate name and password field in the second step of registration
+ // Doing manual validations to avoid the case where user clicks CTA without focusing out of field.
+ formFieldsPayload = { name: formFields.name, password: formFields.password };
+ }
+
+ const { isValid, fieldErrors } = isFormValid(
+ formFieldsPayload, errors, {}, {}, formatMessage,
+ );
+ setErrors(prevErrors => ({
+ ...prevErrors,
+ ...fieldErrors,
+ }));
+ // returning if not valid
+ if (!isValid) {
+ setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
+ } else {
+ dispatch(fetchRealtimeValidations(formFieldsPayload, true));
+ }
+ } else if (multiStepRegistrationExpVariation === CONTROL && multiStepRegistrationPageStep !== SECOND_STEP) {
+ // We only want to validate name, email and password fields in the first step of CONTROL registration
+ // Doing manual validations to avoid the case where user clicks CTA without focusing out of field.
+ const formFieldsPayload = { name: formFields.name, email: formFields.email, password: formFields.password };
+
+ const { isValid, fieldErrors } = isFormValid(
+ formFieldsPayload, errors, {}, {}, formatMessage,
+ );
+
+ setErrors(prevErrors => ({
+ ...prevErrors,
+ ...fieldErrors,
+ }));
+ // returning if not valid
+ if (!isValid) {
+ setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
+ } else {
+ dispatch(fetchRealtimeValidations(formFieldsPayload, true));
+ }
+ } else {
+ registerUser();
+ }
};
useEffect(() => {
@@ -282,104 +383,146 @@ const RegistrationPage = (props) => {
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
}
/>
- {autoSubmitRegForm && !errorCode.type ? (
-
-
-
- ) : (
-
+ )}
>
);
};
diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx
index 2634713f..1f6fd1a3 100644
--- a/src/register/RegistrationPage.test.jsx
+++ b/src/register/RegistrationPage.test.jsx
@@ -17,6 +17,9 @@ import {
setUserPipelineDataLoaded,
} from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants';
+import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
+import useMultiStepRegistrationExperimentVariation
+ from './data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
@@ -30,6 +33,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('./data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -128,6 +132,7 @@ describe('RegistrationPage', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx
index be1f9c27..5e69c7c0 100644
--- a/src/register/components/ConfigurableRegistrationForm.jsx
+++ b/src/register/components/ConfigurableRegistrationForm.jsx
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import { FormFieldRenderer } from '../../field-renderer';
import { FIELDS } from '../data/constants';
+import { FIRST_STEP, shouldDisplayFieldInExperiment } from '../data/optimizelyExperiment/helper';
import messages from '../messages';
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
@@ -31,6 +32,8 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors,
setFormFields,
autoSubmitRegistrationForm,
+ multiStepRegistrationExpVariation,
+ multiStepRegistrationPageStep,
} = props;
const countryList = useMemo(() => getCountryList(getLocale()), []);
@@ -105,7 +108,9 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
};
- if (flags.showConfigurableRegistrationFields) {
+ if (flags.showConfigurableRegistrationFields && shouldDisplayFieldInExperiment(
+ 'other_fields', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
+ )) {
Object.keys(fieldDescriptions).forEach(fieldName => {
const fieldData = fieldDescriptions[fieldName];
switch (fieldData.name) {
@@ -157,7 +162,9 @@ const ConfigurableRegistrationForm = (props) => {
});
}
- if (flags.showConfigurableEdxFields || showCountryField) {
+ if ((flags.showConfigurableEdxFields || showCountryField) && shouldDisplayFieldInExperiment(
+ 'country', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
+ )) {
formFieldDescriptions.push(
{
);
}
- if (flags.showMarketingEmailOptInCheckbox) {
+ if (flags.showMarketingEmailOptInCheckbox && shouldDisplayFieldInExperiment(
+ 'marketing_email_opt_in', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
+ )) {
formFieldDescriptions.push(
{
);
}
- if (flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode) {
+ if ((flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode)
+ && shouldDisplayFieldInExperiment(
+ 'honor_code', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
+ )) {
formFieldDescriptions.push(
@@ -227,11 +239,15 @@ ConfigurableRegistrationForm.propTypes = {
setFieldErrors: PropTypes.func.isRequired,
setFormFields: PropTypes.func.isRequired,
autoSubmitRegistrationForm: PropTypes.bool,
+ multiStepRegistrationExpVariation: PropTypes.string,
+ multiStepRegistrationPageStep: PropTypes.string,
};
ConfigurableRegistrationForm.defaultProps = {
fieldDescriptions: {},
autoSubmitRegistrationForm: false,
+ multiStepRegistrationExpVariation: '',
+ multiStepRegistrationPageStep: FIRST_STEP,
};
export default ConfigurableRegistrationForm;
diff --git a/src/register/components/RegistrationFailure.jsx b/src/register/components/RegistrationFailure.jsx
index c34c2af7..f2a83585 100644
--- a/src/register/components/RegistrationFailure.jsx
+++ b/src/register/components/RegistrationFailure.jsx
@@ -13,12 +13,13 @@ import {
TPA_AUTHENTICATION_FAILURE,
TPA_SESSION_EXPIRED,
} from '../data/constants';
+import { FIRST_STEP } from '../data/optimizelyExperiment/helper';
import messages from '../messages';
const RegistrationFailureMessage = (props) => {
const { formatMessage } = useIntl();
const {
- context, errorCode, failureCount,
+ context, errorCode, failureCount, multiStepRegistrationPageStep,
} = props;
useEffect(() => {
@@ -49,7 +50,11 @@ const RegistrationFailureMessage = (props) => {
errorMessage = formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider });
break;
default:
- errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
+ if (multiStepRegistrationPageStep !== FIRST_STEP) {
+ errorMessage = formatMessage(messages['multistep.registration.form.submission.error']);
+ } else {
+ errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
+ }
break;
}
@@ -65,6 +70,7 @@ RegistrationFailureMessage.defaultProps = {
context: {
errorMessage: null,
},
+ multiStepRegistrationPageStep: FIRST_STEP,
};
RegistrationFailureMessage.propTypes = {
@@ -74,6 +80,7 @@ RegistrationFailureMessage.propTypes = {
}),
errorCode: PropTypes.string.isRequired,
failureCount: PropTypes.number.isRequired,
+ multiStepRegistrationPageStep: PropTypes.string,
};
export default RegistrationFailureMessage;
diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
index 537d11eb..28269072 100644
--- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
+++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
@@ -11,6 +11,9 @@ import configureStore from 'redux-mock-store';
import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants';
+import { FIRST_STEP, NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useMultiStepRegistrationExperimentVariation
+ from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
@@ -22,6 +25,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
@@ -93,6 +97,9 @@ describe('ConfigurableRegistrationForm', () => {
registrationError: {},
registrationFormData,
usernameSuggestions: [],
+ multiStepRegistrationPageStep: FIRST_STEP,
+ multiStepRegExpVariation: '',
+ isValidatingMultiStepRegistrationPage: false,
},
commonComponents: {
thirdPartyAuthApiStatus: null,
@@ -121,6 +128,7 @@ describe('ConfigurableRegistrationForm', () => {
};
window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us'));
+ useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/RegistrationFailure.test.jsx b/src/register/components/tests/RegistrationFailure.test.jsx
index 003cc966..b9667c9c 100644
--- a/src/register/components/tests/RegistrationFailure.test.jsx
+++ b/src/register/components/tests/RegistrationFailure.test.jsx
@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
} from '../../data/constants';
+import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useMultiStepRegistrationExperimentVariation
+ from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import RegistrationFailureMessage from '../RegistrationFailure';
@@ -23,6 +26,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
@@ -121,6 +125,7 @@ describe('RegistrationFailure', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/ThirdPartyAuth.test.jsx b/src/register/components/tests/ThirdPartyAuth.test.jsx
index 917f10f9..45672ad5 100644
--- a/src/register/components/tests/ThirdPartyAuth.test.jsx
+++ b/src/register/components/tests/ThirdPartyAuth.test.jsx
@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants';
+import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useMultiStepRegistrationExperimentVariation
+ from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -22,6 +25,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -120,6 +124,7 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/data/actions.js b/src/register/data/actions.js
index 9fa5aed5..530f3bc8 100644
--- a/src/register/data/actions.js
+++ b/src/register/data/actions.js
@@ -8,6 +8,7 @@ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERRO
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
+export const REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA = 'REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA';
// Backup registration form
export const backupRegistrationForm = () => ({
@@ -20,18 +21,19 @@ export const backupRegistrationFormBegin = (data) => ({
});
// Validate fields from the backend
-export const fetchRealtimeValidations = (formPayload) => ({
+export const fetchRealtimeValidations = (formPayload, isValidatingMultiStepRegistrationPage) => ({
type: REGISTER_FORM_VALIDATIONS.BASE,
- payload: { formPayload },
+ payload: { formPayload, isValidatingMultiStepRegistrationPage },
});
-export const fetchRealtimeValidationsBegin = () => ({
+export const fetchRealtimeValidationsBegin = (isValidatingMultiStepRegistrationPage) => ({
type: REGISTER_FORM_VALIDATIONS.BEGIN,
+ payload: { isValidatingMultiStepRegistrationPage },
});
-export const fetchRealtimeValidationsSuccess = (validations) => ({
+export const fetchRealtimeValidationsSuccess = (validations, isValidatingMultiStepRegistrationPage) => ({
type: REGISTER_FORM_VALIDATIONS.SUCCESS,
- payload: { validations },
+ payload: { validations, isValidatingMultiStepRegistrationPage },
});
export const fetchRealtimeValidationsFailure = () => ({
@@ -83,3 +85,11 @@ export const setUserPipelineDataLoaded = (value) => ({
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
payload: { value },
});
+
+// Multi Step Registration Experiment Actions
+export const setMultiStepRegistrationExpData = (
+ multiStepRegExpVariation, multiStepRegistrationPageStep, isValidatingMultiStepRegistrationPage,
+) => ({
+ type: REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA,
+ payload: { multiStepRegExpVariation, multiStepRegistrationPageStep, isValidatingMultiStepRegistrationPage },
+});
diff --git a/src/register/data/optimizelyExperiment/helper.js b/src/register/data/optimizelyExperiment/helper.js
new file mode 100644
index 00000000..e59ec718
--- /dev/null
+++ b/src/register/data/optimizelyExperiment/helper.js
@@ -0,0 +1,105 @@
+/**
+ * This file contains data for Multi Step Registration Optimizely experiment
+ */
+import { getConfig } from '@edx/frontend-platform';
+
+import messages from '../../messages';
+
+export const NOT_INITIALIZED = 'experiment-not-initialized';
+export const CONTROL = 'control-registration-page';
+export const MULTI_STEP_REGISTRATION_EXP_VARIATION = 'multi-step-registration-page';
+
+export const FIRST_STEP = 'first-step';
+export const SECOND_STEP = 'second-step';
+export const THIRD_STEP = 'third-step';
+
+export const CONTROL_REGISTER_PAGE_FIRST_STEP_FIELDS = ['name', 'email', 'password', 'marketing_email_opt_in', 'ThirdPartyAuth'];
+export const CONTROL_REGISTER_PAGE_SECOND_STEP_FIELDS = ['username', 'country'];
+export const CONTROL_REGISTER_PAGE_COMMON_FIELDS = ['tos_and_honor_code', 'honor_code'];
+
+export const MULTI_STEP_REGISTER_PAGE_FIRST_STEP_FIELDS = ['email', 'marketing_email_opt_in', 'ThirdPartyAuth'];
+export const MULTI_STEP_REGISTER_PAGE_SECOND_STEP_FIELDS = ['name', 'password'];
+export const MULTI_STEP_REGISTER_PAGE_THIRD_STEP_FIELDS = ['username', 'country'];
+export const MULTI_STEP_REGISTER_PAGE_COMMON_FIELDS = ['tos_and_honor_code', 'honor_code'];
+
+const MULTI_STEP_REGISTRATION_EXP_PAGE = 'authn_register_page';
+
+export function getMultiStepRegistrationExperimentVariation() {
+ try {
+ if (window.optimizely
+ && window.optimizely.get('data').experiments[getConfig().MULTI_STEP_REGISTRATION_EXPERIMENT_ID]) {
+ const selectedVariant = window.optimizely.get('state').getVariationMap()[
+ getConfig().MULTI_STEP_REGISTRATION_EXPERIMENT_ID
+ ];
+ return selectedVariant?.name;
+ }
+ } catch (e) { /* empty */ }
+ return '';
+}
+
+export function activateMultiStepRegistrationExperiment() {
+ window.optimizely = window.optimizely || [];
+ window.optimizely.push({
+ type: 'page',
+ pageName: MULTI_STEP_REGISTRATION_EXP_PAGE,
+ });
+}
+
+/**
+ * We want to display username and honor_code fields in second page if user is in multi-step
+ * registration page experiment
+ */
+export const shouldDisplayFieldInExperiment = (fieldName, expVariation, registerPageStep) => (
+ !expVariation || expVariation === NOT_INITIALIZED
+ || (expVariation === CONTROL
+ && (
+ CONTROL_REGISTER_PAGE_COMMON_FIELDS.includes(fieldName)
+ || (registerPageStep === FIRST_STEP && CONTROL_REGISTER_PAGE_FIRST_STEP_FIELDS.includes(fieldName))
+ || (registerPageStep === SECOND_STEP && CONTROL_REGISTER_PAGE_SECOND_STEP_FIELDS.includes(fieldName))
+ ))
+ || (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION
+ && (
+ MULTI_STEP_REGISTER_PAGE_COMMON_FIELDS.includes(fieldName)
+ || (registerPageStep === FIRST_STEP && MULTI_STEP_REGISTER_PAGE_FIRST_STEP_FIELDS.includes(fieldName))
+ || (registerPageStep === SECOND_STEP && MULTI_STEP_REGISTER_PAGE_SECOND_STEP_FIELDS.includes(fieldName))
+ || (registerPageStep === THIRD_STEP && MULTI_STEP_REGISTER_PAGE_THIRD_STEP_FIELDS.includes(fieldName))
+ ))
+);
+
+export const getRegisterButtonLabelInExperiment = (
+ existingButtonLabel, expVariation, registerPageStep, formatMessage,
+) => {
+ if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep === FIRST_STEP) {
+ return formatMessage(messages['multistep.registration.exp.continue.button']);
+ }
+ return existingButtonLabel;
+};
+
+export const getRegisterButtonSubmitStateInExperiment = (
+ registerSubmitState, validationsSubmitState, expVariation, registerPageStep,
+) => {
+ if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep !== THIRD_STEP) {
+ return validationsSubmitState;
+ }
+ return registerSubmitState;
+};
+
+export const getMultiStepRegistrationPreviousStep = (currentStep) => {
+ if (currentStep === THIRD_STEP) {
+ return SECOND_STEP;
+ }
+ if (currentStep === SECOND_STEP) {
+ return FIRST_STEP;
+ }
+ return currentStep;
+};
+
+export const getMultiStepRegistrationNextStep = (currentStep) => {
+ if (currentStep === FIRST_STEP) {
+ return SECOND_STEP;
+ }
+ if (currentStep === SECOND_STEP) {
+ return THIRD_STEP;
+ }
+ return currentStep;
+};
diff --git a/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx b/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
new file mode 100644
index 00000000..692101b7
--- /dev/null
+++ b/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
@@ -0,0 +1,54 @@
+import { useEffect, useState } from 'react';
+
+import {
+ activateMultiStepRegistrationExperiment,
+ getMultiStepRegistrationExperimentVariation,
+ NOT_INITIALIZED,
+} from './helper';
+import { COMPLETE_STATE } from '../../../data/constants';
+
+/**
+ * This hook returns activates multi step registration experiment and returns the experiment
+ * variation for the user.
+ */
+const useMultiStepRegistrationExperimentVariation = (
+ initExpVariation,
+ registrationEmbedded,
+ tpaHint,
+ currentProvider,
+ thirdPartyAuthApiStatus,
+) => {
+ const [variation, setVariation] = useState(initExpVariation);
+
+ useEffect(() => {
+ if (initExpVariation || registrationEmbedded || !!tpaHint || !!currentProvider
+ || thirdPartyAuthApiStatus !== COMPLETE_STATE) {
+ return variation;
+ }
+
+ const getVariation = () => {
+ const expVariation = getMultiStepRegistrationExperimentVariation();
+ if (expVariation) {
+ setVariation(expVariation);
+ } else {
+ // This is to handle the case when user dont get variation for some reason, the register page
+ // shows unlimited spinner.
+ setVariation(NOT_INITIALIZED);
+ }
+ };
+
+ activateMultiStepRegistrationExperiment();
+
+ const timer = setTimeout(getVariation, 300);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [ // eslint-disable-line react-hooks/exhaustive-deps
+ currentProvider, initExpVariation, registrationEmbedded, thirdPartyAuthApiStatus, tpaHint,
+ ]);
+
+ return variation;
+};
+
+export default useMultiStepRegistrationExperimentVariation;
diff --git a/src/register/data/reducers.js b/src/register/data/reducers.js
index 70c3a994..32438877 100644
--- a/src/register/data/reducers.js
+++ b/src/register/data/reducers.js
@@ -5,9 +5,11 @@ import {
REGISTER_NEW_USER,
REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS,
+ REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA,
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from './actions';
+import { FIRST_STEP } from './optimizelyExperiment/helper';
import {
DEFAULT_STATE,
PENDING_STATE,
@@ -35,10 +37,14 @@ export const defaultState = {
},
validations: null,
submitState: DEFAULT_STATE,
+ validationsSubmitState: DEFAULT_STATE,
userPipelineDataLoaded: false,
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
+ multiStepRegExpVariation: '',
+ multiStepRegistrationPageStep: FIRST_STEP,
+ isValidatingMultiStepRegistrationPage: false,
};
const reducer = (state = defaultState, action = {}) => {
@@ -85,12 +91,22 @@ const reducer = (state = defaultState, action = {}) => {
registrationError: { ...registrationErrorTemp },
};
}
+ case REGISTER_FORM_VALIDATIONS.BEGIN: {
+ return {
+ ...state,
+ validationsSubmitState: action.payload?.isValidatingMultiStepRegistrationPage
+ ? PENDING_STATE
+ : state.validationsSubmitState,
+ };
+ }
case REGISTER_FORM_VALIDATIONS.SUCCESS: {
const { usernameSuggestions, ...validationWithoutUsernameSuggestions } = action.payload.validations;
return {
...state,
validations: validationWithoutUsernameSuggestions,
+ isValidatingMultiStepRegistrationPage: !!action.payload?.isValidatingMultiStepRegistrationPage,
usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
+ validationsSubmitState: DEFAULT_STATE,
};
}
case REGISTER_FORM_VALIDATIONS.FAILURE:
@@ -98,6 +114,7 @@ const reducer = (state = defaultState, action = {}) => {
...state,
validationApiRateLimited: true,
validations: null,
+ validationsSubmitState: DEFAULT_STATE,
};
case REGISTER_CLEAR_USERNAME_SUGGESTIONS:
return {
@@ -129,6 +146,14 @@ const reducer = (state = defaultState, action = {}) => {
emailSuggestion: action.payload.emailSuggestion,
},
};
+ case REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA: {
+ return {
+ ...state,
+ multiStepRegExpVariation: action.payload.multiStepRegExpVariation,
+ multiStepRegistrationPageStep: action.payload.multiStepRegistrationPageStep,
+ isValidatingMultiStepRegistrationPage: !!action.payload?.isValidatingMultiStepRegistrationPage,
+ };
+ }
default:
return {
...state,
diff --git a/src/register/data/sagas.js b/src/register/data/sagas.js
index 0325cd4b..3edd395e 100644
--- a/src/register/data/sagas.js
+++ b/src/register/data/sagas.js
@@ -40,10 +40,13 @@ export function* handleNewUserRegistration(action) {
export function* fetchRealtimeValidations(action) {
try {
- yield put(fetchRealtimeValidationsBegin());
+ yield put(fetchRealtimeValidationsBegin(action.payload?.isValidatingMultiStepRegistrationPage));
const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
- yield put(fetchRealtimeValidationsSuccess(camelCaseObject(fieldValidations)));
+ yield put(fetchRealtimeValidationsSuccess(
+ camelCaseObject(fieldValidations),
+ action.payload?.isValidatingMultiStepRegistrationPage,
+ ));
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(fetchRealtimeValidationsFailure());
diff --git a/src/register/data/tests/reducers.test.js b/src/register/data/tests/reducers.test.js
index 3e2270ab..909704d2 100644
--- a/src/register/data/tests/reducers.test.js
+++ b/src/register/data/tests/reducers.test.js
@@ -11,6 +11,7 @@ import {
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from '../actions';
+import { FIRST_STEP } from '../optimizelyExperiment/helper';
import reducer from '../reducers';
describe('Registration Reducer Tests', () => {
@@ -34,10 +35,14 @@ describe('Registration Reducer Tests', () => {
},
validations: null,
submitState: DEFAULT_STATE,
+ validationsSubmitState: DEFAULT_STATE,
userPipelineDataLoaded: false,
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
+ multiStepRegExpVariation: '',
+ multiStepRegistrationPageStep: FIRST_STEP,
+ isValidatingMultiStepRegistrationPage: false,
};
it('should return the initial state', () => {
diff --git a/src/register/data/utils.js b/src/register/data/utils.js
index b0cff129..747f2f2c 100644
--- a/src/register/data/utils.js
+++ b/src/register/data/utils.js
@@ -43,30 +43,39 @@ export const isFormValid = (
Object.keys(payload).forEach(key => {
switch (key) {
case 'name':
- fieldErrors.name = validateName(payload.name, formatMessage);
+ if (!fieldErrors.name) {
+ fieldErrors.name = validateName(payload.name, formatMessage);
+ }
if (fieldErrors.name) { isValid = false; }
break;
case 'email': {
- const {
- fieldError, confirmEmailError, suggestion,
- } = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
- if (fieldError) {
- fieldErrors.email = fieldError;
- isValid = false;
+ if (!fieldErrors.email) {
+ const {
+ fieldError, confirmEmailError, suggestion,
+ } = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
+ if (fieldError) {
+ fieldErrors.email = fieldError;
+ isValid = false;
+ }
+ if (confirmEmailError) {
+ fieldErrors.confirm_email = confirmEmailError;
+ isValid = false;
+ }
+ emailSuggestion = suggestion;
}
- if (confirmEmailError) {
- fieldErrors.confirm_email = confirmEmailError;
- isValid = false;
- }
- emailSuggestion = suggestion;
+ if (fieldErrors.email) { isValid = false; }
break;
}
case 'username':
- fieldErrors.username = validateUsername(payload.username, formatMessage);
+ if (!fieldErrors.username) {
+ fieldErrors.username = validateUsername(payload.username, formatMessage);
+ }
if (fieldErrors.username) { isValid = false; }
break;
case 'password':
- fieldErrors.password = validatePasswordField(payload.password, formatMessage);
+ if (!fieldErrors.password) {
+ fieldErrors.password = validatePasswordField(payload.password, formatMessage);
+ }
if (fieldErrors.password) { isValid = false; }
break;
default:
diff --git a/src/register/messages.jsx b/src/register/messages.jsx
index 39d9e7f5..cea5e5df 100644
--- a/src/register/messages.jsx
+++ b/src/register/messages.jsx
@@ -201,6 +201,29 @@ const messages = defineMessages({
defaultMessage: 'Did you mean',
description: 'Did you mean alert suggestion',
},
+ // MultiStep Registration experiment
+ 'multistep.registration.exp.continue.button': {
+ id: 'multistep.registration.exp.continue.button',
+ defaultMessage: 'Continue',
+ description: 'Label text for multistep registration page second step',
+ },
+ 'multistep.registration.username.second.step.guideline.content': {
+ id: 'multistep.registration.username.second.step.guideline.content',
+ defaultMessage: 'Finish Registration',
+ description: 'Guideline content for username field in multi-step registration experiment step 2',
+ },
+ 'multistep.registration.username.third.step.guideline.content': {
+ id: 'multistep.registration.username.third.step.guideline.content',
+ defaultMessage: 'To finalize your registration, please confirm your country of residence '
+ + 'and create a public username that will identify you in your course communication forums. '
+ + 'The username cannot be changed.',
+ description: 'Guideline content for username field in multi-step registration experiment step 2',
+ },
+ 'multistep.registration.form.submission.error': {
+ id: 'multistep.registration.form.submission.error',
+ defaultMessage: 'Please check your responses for this and the previous step and try again.',
+ description: 'Error message that appears on top of the form when invalid form is submitted',
+ },
});
export default messages;
diff --git a/src/sass/_registration.scss b/src/sass/_registration.scss
index 75889601..4126da9a 100644
--- a/src/sass/_registration.scss
+++ b/src/sass/_registration.scss
@@ -1,7 +1,3 @@
-.register-button {
- min-width: 14.4rem;
-}
-
.pgn__form-autosuggest__wrapper > .pgn__form-group {
margin-bottom: 0 !important;
}
From 90f650ce3eb0778e5328727184cecfc81b6a41d5 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Thu, 18 Apr 2024 11:04:28 +0500
Subject: [PATCH 10/82] feat: add multi step registration eventing (#1226)
* feat: implement multi step registration experiment
* feat: add multi step registration eventing
---
src/common-components/SocialAuthProviders.jsx | 14 ++++-
src/common-components/ThirdPartyAuth.jsx | 4 ++
src/register/RegistrationPage.jsx | 35 +++++++++++-
.../data/optimizelyExperiment/track.js | 56 +++++++++++++++++++
...ltiStepRegistrationExperimentVariation.jsx | 2 +
5 files changed, 108 insertions(+), 3 deletions(-)
create mode 100644 src/register/data/optimizelyExperiment/track.js
diff --git a/src/common-components/SocialAuthProviders.jsx b/src/common-components/SocialAuthProviders.jsx
index abe06da7..5cdb04e0 100644
--- a/src/common-components/SocialAuthProviders.jsx
+++ b/src/common-components/SocialAuthProviders.jsx
@@ -9,14 +9,24 @@ import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
+import { CONTROL, MULTI_STEP_REGISTRATION_EXP_VARIATION } from '../register/data/optimizelyExperiment/helper';
+import { trackMultiStepRegistrationSSOBtnClicked } from '../register/data/optimizelyExperiment/track';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
- const { referrer, socialAuthProviders } = props;
+ const {
+ referrer,
+ socialAuthProviders,
+ multiStepRegistrationExpVariation,
+ } = props;
function handleSubmit(e) {
e.preventDefault();
+ if (multiStepRegistrationExpVariation === CONTROL
+ || multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION) {
+ trackMultiStepRegistrationSSOBtnClicked(multiStepRegistrationExpVariation);
+ }
const url = e.currentTarget.dataset.providerUrl;
window.location.href = getConfig().LMS_BASE_URL + url;
}
@@ -60,6 +70,7 @@ const SocialAuthProviders = (props) => {
SocialAuthProviders.defaultProps = {
referrer: LOGIN_PAGE,
socialAuthProviders: [],
+ multiStepRegistrationExpVariation: '',
};
SocialAuthProviders.propTypes = {
@@ -73,6 +84,7 @@ SocialAuthProviders.propTypes = {
registerUrl: PropTypes.string,
skipRegistrationForm: PropTypes.bool,
})),
+ multiStepRegistrationExpVariation: PropTypes.string,
};
export default SocialAuthProviders;
diff --git a/src/common-components/ThirdPartyAuth.jsx b/src/common-components/ThirdPartyAuth.jsx
index 3b782036..d64615e0 100644
--- a/src/common-components/ThirdPartyAuth.jsx
+++ b/src/common-components/ThirdPartyAuth.jsx
@@ -32,6 +32,7 @@ const ThirdPartyAuth = (props) => {
handleInstitutionLogin,
thirdPartyAuthApiStatus,
isLoginPage,
+ multiStepRegistrationExpVariation,
} = props;
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
const isSocialAuthActive = !!providers.length && !currentProvider;
@@ -78,6 +79,7 @@ const ThirdPartyAuth = (props) => {
)}
@@ -93,6 +95,7 @@ ThirdPartyAuth.defaultProps = {
secondaryProviders: [],
thirdPartyAuthApiStatus: PENDING_STATE,
isLoginPage: false,
+ multiStepRegistrationExpVariation: '',
};
ThirdPartyAuth.propTypes = {
@@ -120,6 +123,7 @@ ThirdPartyAuth.propTypes = {
),
thirdPartyAuthApiStatus: PropTypes.string,
isLoginPage: PropTypes.bool,
+ multiStepRegistrationExpVariation: PropTypes.string,
};
export default ThirdPartyAuth;
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 70897a31..19b3d774 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -34,10 +34,17 @@ import {
getRegisterButtonLabelInExperiment,
getRegisterButtonSubmitStateInExperiment,
MULTI_STEP_REGISTRATION_EXP_VARIATION,
- NOT_INITIALIZED,
SECOND_STEP,
shouldDisplayFieldInExperiment, THIRD_STEP,
} from './data/optimizelyExperiment/helper';
+import {
+ trackMultiStepRegistrationFormSubmitBtnClicked,
+ trackMultiStepRegistrationStep1SubmitBtnClicked,
+ trackMultiStepRegistrationStep2SubmitBtnClicked,
+ trackMultiStepRegistrationStep2Viewed,
+ trackMultiStepRegistrationStep3SubmitBtnClicked,
+ trackMultiStepRegistrationStep3Viewed,
+} from './data/optimizelyExperiment/track';
import useMultiStepRegistrationExperimentVariation
from './data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import getBackendValidations from './data/selectors';
@@ -136,6 +143,17 @@ const RegistrationPage = (props) => {
) {
setErrorCode({ type: '', count: 0 });
const nextStep = getMultiStepRegistrationNextStep(multiStepRegistrationPageStep);
+ if (nextStep === SECOND_STEP) {
+ trackMultiStepRegistrationStep2Viewed(multiStepRegistrationExpVariation);
+ if (multiStepRegistrationExpVariation === CONTROL) {
+ trackMultiStepRegistrationFormSubmitBtnClicked(multiStepRegistrationExpVariation);
+ }
+ } else if (nextStep === THIRD_STEP) {
+ trackMultiStepRegistrationStep3Viewed();
+ if (multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION) {
+ trackMultiStepRegistrationFormSubmitBtnClicked(multiStepRegistrationExpVariation);
+ }
+ }
dispatch(setMultiStepRegistrationExpData(multiStepRegistrationExpVariation, nextStep));
}
}, [ // eslint-disable-line react-hooks/exhaustive-deps
@@ -215,7 +233,8 @@ const RegistrationPage = (props) => {
if (registrationResult.success) {
let registeredEventProps = {};
- if (multiStepRegistrationExpVariation !== NOT_INITIALIZED) {
+ if (multiStepRegistrationExpVariation === CONTROL
+ || multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION) {
registeredEventProps = {
variation: multiStepRegistrationExpVariation,
};
@@ -300,15 +319,25 @@ const RegistrationPage = (props) => {
const handleSubmit = (e) => {
e.preventDefault();
+ if (multiStepRegistrationExpVariation === CONTROL
+ && multiStepRegistrationPageStep === SECOND_STEP) {
+ trackMultiStepRegistrationStep2SubmitBtnClicked(multiStepRegistrationExpVariation);
+ }
+ if (multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION
+ && multiStepRegistrationPageStep === THIRD_STEP) {
+ trackMultiStepRegistrationStep3SubmitBtnClicked();
+ }
if (multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION
&& multiStepRegistrationPageStep !== THIRD_STEP) {
let formFieldsPayload = {};
if (multiStepRegistrationPageStep === FIRST_STEP) {
+ trackMultiStepRegistrationStep1SubmitBtnClicked(multiStepRegistrationExpVariation);
// We only want to validate email in the first step of registration
// Doing manual validations to avoid the case where user clicks CTA without focusing out of field.
formFieldsPayload = { email: formFields.email };
} else if (multiStepRegistrationPageStep === SECOND_STEP) {
+ trackMultiStepRegistrationStep2SubmitBtnClicked(multiStepRegistrationExpVariation);
// We only want to validate name and password field in the second step of registration
// Doing manual validations to avoid the case where user clicks CTA without focusing out of field.
formFieldsPayload = { name: formFields.name, password: formFields.password };
@@ -328,6 +357,7 @@ const RegistrationPage = (props) => {
dispatch(fetchRealtimeValidations(formFieldsPayload, true));
}
} else if (multiStepRegistrationExpVariation === CONTROL && multiStepRegistrationPageStep !== SECOND_STEP) {
+ trackMultiStepRegistrationStep1SubmitBtnClicked(multiStepRegistrationExpVariation);
// We only want to validate name, email and password fields in the first step of CONTROL registration
// Doing manual validations to avoid the case where user clicks CTA without focusing out of field.
const formFieldsPayload = { name: formFields.name, email: formFields.email, password: formFields.password };
@@ -518,6 +548,7 @@ const RegistrationPage = (props) => {
secondaryProviders={secondaryProviders}
handleInstitutionLogin={handleInstitutionLogin}
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
+ multiStepRegistrationExpVariation={multiStepRegistrationExpVariation}
/>
)}
diff --git a/src/register/data/optimizelyExperiment/track.js b/src/register/data/optimizelyExperiment/track.js
new file mode 100644
index 00000000..8db71809
--- /dev/null
+++ b/src/register/data/optimizelyExperiment/track.js
@@ -0,0 +1,56 @@
+import { sendTrackEvent } from '@edx/frontend-platform/analytics';
+
+export const eventNames = {
+ multiStepRegistrationStep1Viewed: 'edx.bi.user.multistepregistration.step1.viewed',
+ multiStepRegistrationStep2Viewed: 'edx.bi.user.multistepregistration.step2.viewed',
+ multiStepRegistrationStep3Viewed: 'edx.bi.user.multistepregistration.step3.viewed',
+ multiStepRegistrationStep1SubmitBtnClicked: 'edx.bi.user.registration.step1.submit.click',
+ multiStepRegistrationStep2SubmitBtnClicked: 'edx.bi.user.registration.step2.submit.click',
+ multiStepRegistrationStep3SubmitBtnClicked: 'edx.bi.user.registration.step3.submit.click',
+ multiStepRegistrationFormSubmitBtnClicked: 'edx.bi.user.registration.form.submit.click',
+ multiStepRegistrationSSOBtnClicked: 'edx.bi.user.registration.sso.btn.click',
+};
+
+export const trackMultiStepRegistrationStep1Viewed = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep1Viewed, {
+ variation: expVariation,
+ });
+};
+
+export const trackMultiStepRegistrationStep2Viewed = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep2Viewed, {
+ variation: expVariation,
+ });
+};
+
+export const trackMultiStepRegistrationStep3Viewed = () => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep3Viewed, {});
+};
+
+export const trackMultiStepRegistrationStep1SubmitBtnClicked = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep1SubmitBtnClicked, {
+ variation: expVariation,
+ });
+};
+
+export const trackMultiStepRegistrationStep2SubmitBtnClicked = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep2SubmitBtnClicked, {
+ variation: expVariation,
+ });
+};
+
+export const trackMultiStepRegistrationStep3SubmitBtnClicked = () => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep3SubmitBtnClicked, {});
+};
+
+export const trackMultiStepRegistrationFormSubmitBtnClicked = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationFormSubmitBtnClicked, {
+ variation: expVariation,
+ });
+};
+
+export const trackMultiStepRegistrationSSOBtnClicked = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationSSOBtnClicked, {
+ variation: expVariation,
+ });
+};
diff --git a/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx b/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
index 692101b7..97d0a233 100644
--- a/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
+++ b/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
@@ -5,6 +5,7 @@ import {
getMultiStepRegistrationExperimentVariation,
NOT_INITIALIZED,
} from './helper';
+import { trackMultiStepRegistrationStep1Viewed } from './track';
import { COMPLETE_STATE } from '../../../data/constants';
/**
@@ -30,6 +31,7 @@ const useMultiStepRegistrationExperimentVariation = (
const expVariation = getMultiStepRegistrationExperimentVariation();
if (expVariation) {
setVariation(expVariation);
+ trackMultiStepRegistrationStep1Viewed(expVariation);
} else {
// This is to handle the case when user dont get variation for some reason, the register page
// shows unlimited spinner.
From b219fe3683c97e4547257e2b61e936e67caefa20 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
Date: Mon, 22 Apr 2024 11:34:47 +0500
Subject: [PATCH 11/82] fix: fix register button width
---
src/register/RegistrationPage.jsx | 6 +++++-
src/register/data/optimizelyExperiment/helper.js | 7 +++++++
src/sass/_registration.scss | 8 ++++++++
3 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 19b3d774..7bca6255 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -31,6 +31,7 @@ import {
CONTROL,
FIRST_STEP,
getMultiStepRegistrationNextStep,
+ getRegisterButtonClassInExperiment,
getRegisterButtonLabelInExperiment,
getRegisterButtonSubmitStateInExperiment,
MULTI_STEP_REGISTRATION_EXP_VARIATION,
@@ -522,7 +523,10 @@ const RegistrationPage = (props) => {
name="register-user"
type="submit"
variant="brand"
- className="mt-4 mb-4"
+ className={`
+ mt-4 mb-4
+ ${getRegisterButtonClassInExperiment(multiStepRegistrationExpVariation, multiStepRegistrationPageStep)}
+ `}
state={getRegisterButtonSubmitStateInExperiment(
submitState,
validationsSubmitState,
diff --git a/src/register/data/optimizelyExperiment/helper.js b/src/register/data/optimizelyExperiment/helper.js
index e59ec718..7140d5ec 100644
--- a/src/register/data/optimizelyExperiment/helper.js
+++ b/src/register/data/optimizelyExperiment/helper.js
@@ -75,6 +75,13 @@ export const getRegisterButtonLabelInExperiment = (
return existingButtonLabel;
};
+export const getRegisterButtonClassInExperiment = (expVariation, registerPageStep) => {
+ if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep === FIRST_STEP) {
+ return 'continue-button';
+ }
+ return 'register-button';
+};
+
export const getRegisterButtonSubmitStateInExperiment = (
registerSubmitState, validationsSubmitState, expVariation, registerPageStep,
) => {
diff --git a/src/sass/_registration.scss b/src/sass/_registration.scss
index 4126da9a..884af10d 100644
--- a/src/sass/_registration.scss
+++ b/src/sass/_registration.scss
@@ -1,3 +1,11 @@
+.register-button {
+ min-width: 14.4rem;
+}
+
+.continue-button {
+ min-width: 7rem;
+}
+
.pgn__form-autosuggest__wrapper > .pgn__form-group {
margin-bottom: 0 !important;
}
From 378250398394c86d4821efc3eadcc9dd3ce5cadf Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
Date: Mon, 22 Apr 2024 15:17:08 +0500
Subject: [PATCH 12/82] fix: fix register button loader for control
---
src/register/data/optimizelyExperiment/helper.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/register/data/optimizelyExperiment/helper.js b/src/register/data/optimizelyExperiment/helper.js
index 7140d5ec..f3d21381 100644
--- a/src/register/data/optimizelyExperiment/helper.js
+++ b/src/register/data/optimizelyExperiment/helper.js
@@ -85,7 +85,8 @@ export const getRegisterButtonClassInExperiment = (expVariation, registerPageSte
export const getRegisterButtonSubmitStateInExperiment = (
registerSubmitState, validationsSubmitState, expVariation, registerPageStep,
) => {
- if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep !== THIRD_STEP) {
+ if ((expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep !== THIRD_STEP)
+ || (expVariation === CONTROL && registerPageStep !== SECOND_STEP)) {
return validationsSubmitState;
}
return registerSubmitState;
From 03d1666c2cf7a341f57c9e0c93c1ab518f0e1af0 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Thu, 25 Apr 2024 15:28:05 +0500
Subject: [PATCH 13/82] feat: capture marketing lead in experiment events
(#1243)
---
src/register/RegistrationPage.jsx | 3 ++-
src/register/data/optimizelyExperiment/track.js | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 7bca6255..e4da4b0a 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -145,7 +145,8 @@ const RegistrationPage = (props) => {
setErrorCode({ type: '', count: 0 });
const nextStep = getMultiStepRegistrationNextStep(multiStepRegistrationPageStep);
if (nextStep === SECOND_STEP) {
- trackMultiStepRegistrationStep2Viewed(multiStepRegistrationExpVariation);
+ const isMarketingLead = formFields.email && configurableFormFields?.marketingEmailsOptIn;
+ trackMultiStepRegistrationStep2Viewed(multiStepRegistrationExpVariation, isMarketingLead);
if (multiStepRegistrationExpVariation === CONTROL) {
trackMultiStepRegistrationFormSubmitBtnClicked(multiStepRegistrationExpVariation);
}
diff --git a/src/register/data/optimizelyExperiment/track.js b/src/register/data/optimizelyExperiment/track.js
index 8db71809..68a6347a 100644
--- a/src/register/data/optimizelyExperiment/track.js
+++ b/src/register/data/optimizelyExperiment/track.js
@@ -17,9 +17,10 @@ export const trackMultiStepRegistrationStep1Viewed = (expVariation) => {
});
};
-export const trackMultiStepRegistrationStep2Viewed = (expVariation) => {
+export const trackMultiStepRegistrationStep2Viewed = (expVariation, isMarketingLead) => {
sendTrackEvent(eventNames.multiStepRegistrationStep2Viewed, {
variation: expVariation,
+ is_marketing_lead: isMarketingLead,
});
};
From d8947a4c0ad7fd4dcd28224f49fa68545a589601 Mon Sep 17 00:00:00 2001
From: mubbsharanwar
Date: Mon, 6 May 2024 15:57:54 +0500
Subject: [PATCH 14/82] revert: multistep registration experiment revert
multistep registration experiment changes
VAN-1930
---
src/common-components/SocialAuthProviders.jsx | 14 +-
src/common-components/ThirdPartyAuth.jsx | 4 -
src/common-components/messages.jsx | 6 -
src/config/index.js | 2 -
src/logistration/Logistration.jsx | 55 +--
src/logistration/Logistration.test.jsx | 7 -
src/register/RegistrationPage.jsx | 377 +++++-------------
src/register/RegistrationPage.test.jsx | 5 -
.../ConfigurableRegistrationForm.jsx | 24 +-
.../components/RegistrationFailure.jsx | 11 +-
.../ConfigurableRegistrationForm.test.jsx | 8 -
.../tests/RegistrationFailure.test.jsx | 5 -
.../components/tests/ThirdPartyAuth.test.jsx | 5 -
src/register/data/actions.js | 21 +-
.../data/optimizelyExperiment/helper.js | 113 ------
.../data/optimizelyExperiment/track.js | 57 ---
...ltiStepRegistrationExperimentVariation.jsx | 56 ---
src/register/data/reducers.js | 25 --
src/register/data/sagas.js | 7 +-
src/register/data/tests/reducers.test.js | 5 -
src/register/data/utils.js | 36 +-
src/register/messages.jsx | 23 --
src/sass/_registration.scss | 4 -
23 files changed, 136 insertions(+), 734 deletions(-)
delete mode 100644 src/register/data/optimizelyExperiment/helper.js
delete mode 100644 src/register/data/optimizelyExperiment/track.js
delete mode 100644 src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
diff --git a/src/common-components/SocialAuthProviders.jsx b/src/common-components/SocialAuthProviders.jsx
index 5cdb04e0..abe06da7 100644
--- a/src/common-components/SocialAuthProviders.jsx
+++ b/src/common-components/SocialAuthProviders.jsx
@@ -9,24 +9,14 @@ import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
-import { CONTROL, MULTI_STEP_REGISTRATION_EXP_VARIATION } from '../register/data/optimizelyExperiment/helper';
-import { trackMultiStepRegistrationSSOBtnClicked } from '../register/data/optimizelyExperiment/track';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
- const {
- referrer,
- socialAuthProviders,
- multiStepRegistrationExpVariation,
- } = props;
+ const { referrer, socialAuthProviders } = props;
function handleSubmit(e) {
e.preventDefault();
- if (multiStepRegistrationExpVariation === CONTROL
- || multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION) {
- trackMultiStepRegistrationSSOBtnClicked(multiStepRegistrationExpVariation);
- }
const url = e.currentTarget.dataset.providerUrl;
window.location.href = getConfig().LMS_BASE_URL + url;
}
@@ -70,7 +60,6 @@ const SocialAuthProviders = (props) => {
SocialAuthProviders.defaultProps = {
referrer: LOGIN_PAGE,
socialAuthProviders: [],
- multiStepRegistrationExpVariation: '',
};
SocialAuthProviders.propTypes = {
@@ -84,7 +73,6 @@ SocialAuthProviders.propTypes = {
registerUrl: PropTypes.string,
skipRegistrationForm: PropTypes.bool,
})),
- multiStepRegistrationExpVariation: PropTypes.string,
};
export default SocialAuthProviders;
diff --git a/src/common-components/ThirdPartyAuth.jsx b/src/common-components/ThirdPartyAuth.jsx
index d64615e0..3b782036 100644
--- a/src/common-components/ThirdPartyAuth.jsx
+++ b/src/common-components/ThirdPartyAuth.jsx
@@ -32,7 +32,6 @@ const ThirdPartyAuth = (props) => {
handleInstitutionLogin,
thirdPartyAuthApiStatus,
isLoginPage,
- multiStepRegistrationExpVariation,
} = props;
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
const isSocialAuthActive = !!providers.length && !currentProvider;
@@ -79,7 +78,6 @@ const ThirdPartyAuth = (props) => {
)}
@@ -95,7 +93,6 @@ ThirdPartyAuth.defaultProps = {
secondaryProviders: [],
thirdPartyAuthApiStatus: PENDING_STATE,
isLoginPage: false,
- multiStepRegistrationExpVariation: '',
};
ThirdPartyAuth.propTypes = {
@@ -123,7 +120,6 @@ ThirdPartyAuth.propTypes = {
),
thirdPartyAuthApiStatus: PropTypes.string,
isLoginPage: PropTypes.bool,
- multiStepRegistrationExpVariation: PropTypes.string,
};
export default ThirdPartyAuth;
diff --git a/src/common-components/messages.jsx b/src/common-components/messages.jsx
index 6f0e69ca..08e88b8f 100644
--- a/src/common-components/messages.jsx
+++ b/src/common-components/messages.jsx
@@ -132,12 +132,6 @@ const messages = defineMessages({
defaultMessage: 'Company or school credentials',
description: 'Company or school login link text.',
},
- // multi step registration experiment messages
- 'tab.back.btn.text': {
- id: 'tab.back.btn.text',
- defaultMessage: 'Back',
- description: 'Tab back button text',
- },
});
export default messages;
diff --git a/src/config/index.js b/src/config/index.js
index 5d221faa..fa3613aa 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -34,8 +34,6 @@ const configuration = {
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
- // Multi Step Registration Experiment
- MULTI_STEP_REGISTRATION_EXPERIMENT_ID: process.env.MULTI_STEP_REGISTRATION_EXPERIMENT_ID || '',
};
export default configuration;
diff --git a/src/logistration/Logistration.jsx b/src/logistration/Logistration.jsx
index 5199e1b3..9451aa27 100644
--- a/src/logistration/Logistration.jsx
+++ b/src/logistration/Logistration.jsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import { connect, useDispatch, useSelector } from 'react-redux';
+import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -7,11 +7,10 @@ import { getAuthService } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Icon,
- IconButton,
Tab,
Tabs,
} from '@openedx/paragon';
-import { ArrowBackIos, ChevronLeft } from '@openedx/paragon/icons';
+import { ChevronLeft } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { Navigate, useNavigate } from 'react-router-dom';
@@ -28,11 +27,7 @@ import {
import { LoginPage } from '../login';
import { backupLoginForm } from '../login/data/actions';
import { RegistrationPage } from '../register';
-import { backupRegistrationForm, setMultiStepRegistrationExpData } from '../register/data/actions';
-import {
- FIRST_STEP,
- getMultiStepRegistrationPreviousStep,
-} from '../register/data/optimizelyExperiment/helper';
+import { backupRegistrationForm } from '../register/data/actions';
const Logistration = (props) => {
const { selectedPage, tpaProviders } = props;
@@ -47,10 +42,6 @@ const Logistration = (props) => {
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
const hideRegistrationLink = getConfig().SHOW_REGISTRATION_LINKS === false;
- const dispatch = useDispatch();
- const multiStepRegExpVariation = useSelector(state => state.register.multiStepRegExpVariation);
- const multiStepRegistrationPageStep = useSelector(state => state.register.multiStepRegistrationPageStep);
-
useEffect(() => {
const authService = getAuthService();
if (authService) {
@@ -100,39 +91,6 @@ const Logistration = (props) => {
);
- /**
- * Temporary function created to resolve the complexity in tabs conditioning for multi-step
- * registration experiment
- */
- const getTabs = () => {
- if (multiStepRegistrationPageStep !== FIRST_STEP) {
- const prevStep = getMultiStepRegistrationPreviousStep(multiStepRegistrationPageStep);
- return (
-
- {
- dispatch(setMultiStepRegistrationExpData(multiStepRegExpVariation, prevStep));
- }}
- variant="primary"
- size="inline"
- className="mr-1"
- />
- {formatMessage(messages['tab.back.btn.text'])}
-
- );
- }
- return (
- handleOnSelect(tabKey, selectedPage)}>
-
-
-
- );
- };
-
const isValidTpaHint = () => {
const { provider } = getTpaProvider(tpaHint, providers, secondaryProviders);
return !!provider;
@@ -165,7 +123,12 @@ const Logistration = (props) => {
)
- : (!isValidTpaHint() && !hideRegistrationLink && getTabs())}
+ : (!isValidTpaHint() && !hideRegistrationLink && (
+ handleOnSelect(tabKey, selectedPage)}>
+
+
+
+ ))}
{ key && (
)}
diff --git a/src/logistration/Logistration.test.jsx b/src/logistration/Logistration.test.jsx
index ce7af88f..87bf3e70 100644
--- a/src/logistration/Logistration.test.jsx
+++ b/src/logistration/Logistration.test.jsx
@@ -15,16 +15,12 @@ import {
} from '../data/constants';
import { backupLoginForm } from '../login/data/actions';
import { backupRegistrationForm } from '../register/data/actions';
-import { FIRST_STEP, NOT_INITIALIZED } from '../register/data/optimizelyExperiment/helper';
-import useMultiStepRegistrationExperimentVariation
- from '../register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
-jest.mock('../register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
@@ -67,8 +63,6 @@ describe('Logistration', () => {
registrationError: {},
usernameSuggestions: [],
validationApiRateLimited: false,
- multiStepRegExpVariation: '',
- multiStepRegistrationPageStep: FIRST_STEP,
},
commonComponents: {
thirdPartyAuthContext: {
@@ -89,7 +83,6 @@ describe('Logistration', () => {
username: 'test-user',
})),
}));
- useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({
loggingService: { logError: jest.fn() },
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index e4da4b0a..80ac1f6b 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -17,37 +17,14 @@ import RegistrationFailure from './components/RegistrationFailure';
import {
backupRegistrationFormBegin,
clearRegistrationBackendError,
- fetchRealtimeValidations,
registerNewUser,
setEmailSuggestionInStore,
- setMultiStepRegistrationExpData,
setUserPipelineDataLoaded,
} from './data/actions';
import {
FORM_SUBMISSION_ERROR,
TPA_AUTHENTICATION_FAILURE,
} from './data/constants';
-import {
- CONTROL,
- FIRST_STEP,
- getMultiStepRegistrationNextStep,
- getRegisterButtonClassInExperiment,
- getRegisterButtonLabelInExperiment,
- getRegisterButtonSubmitStateInExperiment,
- MULTI_STEP_REGISTRATION_EXP_VARIATION,
- SECOND_STEP,
- shouldDisplayFieldInExperiment, THIRD_STEP,
-} from './data/optimizelyExperiment/helper';
-import {
- trackMultiStepRegistrationFormSubmitBtnClicked,
- trackMultiStepRegistrationStep1SubmitBtnClicked,
- trackMultiStepRegistrationStep2SubmitBtnClicked,
- trackMultiStepRegistrationStep2Viewed,
- trackMultiStepRegistrationStep3SubmitBtnClicked,
- trackMultiStepRegistrationStep3Viewed,
-} from './data/optimizelyExperiment/track';
-import useMultiStepRegistrationExperimentVariation
- from './data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import getBackendValidations from './data/selectors';
import {
isFormValid, prepareRegistrationPayload,
@@ -96,13 +73,6 @@ const RegistrationPage = (props) => {
const shouldBackupState = useSelector(state => state.register.shouldBackupState);
const userPipelineDataLoaded = useSelector(state => state.register.userPipelineDataLoaded);
const submitState = useSelector(state => state.register.submitState);
- const backendValidations = useSelector(getBackendValidations);
- const multiStepRegExpVariation = useSelector(state => state.register.multiStepRegExpVariation);
- const multiStepRegistrationPageStep = useSelector(state => state.register.multiStepRegistrationPageStep);
- const isValidatingMultiStepRegistrationPage = useSelector(
- state => state.register.isValidatingMultiStepRegistrationPage,
- );
- const validationsSubmitState = useSelector(state => state.register.validationsSubmitState);
const fieldDescriptions = useSelector(state => state.commonComponents.fieldDescriptions);
const optionalFields = useSelector(state => state.commonComponents.optionalFields);
@@ -115,6 +85,7 @@ const RegistrationPage = (props) => {
const secondaryProviders = useSelector(state => state.commonComponents.thirdPartyAuthContext.secondaryProviders);
const pipelineUserDetails = useSelector(state => state.commonComponents.thirdPartyAuthContext.pipelineUserDetails);
+ const backendValidations = useSelector(getBackendValidations);
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
const tpaHint = useMemo(() => getTpaHint(), []);
@@ -131,38 +102,6 @@ const RegistrationPage = (props) => {
? formatMessage(messages['create.account.cta.button'], { label: cta })
: formatMessage(messages['create.account.for.free.button']);
- /**
- * Multi-Step Registration Page Experiment
- */
- const multiStepRegistrationExpVariation = useMultiStepRegistrationExperimentVariation(
- multiStepRegExpVariation, registrationEmbedded, tpaHint, currentProvider, thirdPartyAuthApiStatus,
- );
-
- useEffect(() => {
- if (isValidatingMultiStepRegistrationPage && backendValidations
- && Object.values(backendValidations).every(value => value === '')
- ) {
- setErrorCode({ type: '', count: 0 });
- const nextStep = getMultiStepRegistrationNextStep(multiStepRegistrationPageStep);
- if (nextStep === SECOND_STEP) {
- const isMarketingLead = formFields.email && configurableFormFields?.marketingEmailsOptIn;
- trackMultiStepRegistrationStep2Viewed(multiStepRegistrationExpVariation, isMarketingLead);
- if (multiStepRegistrationExpVariation === CONTROL) {
- trackMultiStepRegistrationFormSubmitBtnClicked(multiStepRegistrationExpVariation);
- }
- } else if (nextStep === THIRD_STEP) {
- trackMultiStepRegistrationStep3Viewed();
- if (multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION) {
- trackMultiStepRegistrationFormSubmitBtnClicked(multiStepRegistrationExpVariation);
- }
- }
- dispatch(setMultiStepRegistrationExpData(multiStepRegistrationExpVariation, nextStep));
- }
- }, [ // eslint-disable-line react-hooks/exhaustive-deps
- isValidatingMultiStepRegistrationPage,
- backendValidations,
- ]);
-
/**
* Set the userPipelineDetails data in formFields for only first time
*/
@@ -209,11 +148,8 @@ const RegistrationPage = (props) => {
formFields: { ...formFields },
errors: { ...errors },
}));
- dispatch(setMultiStepRegistrationExpData(
- multiStepRegistrationExpVariation, multiStepRegistrationPageStep, false,
- ));
}
- }, [shouldBackupState]); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [shouldBackupState, configurableFormFields, formFields, errors, dispatch, backedUpFormData]);
useEffect(() => {
if (backendValidations) {
@@ -233,22 +169,13 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (registrationResult.success) {
- let registeredEventProps = {};
-
- if (multiStepRegistrationExpVariation === CONTROL
- || multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION) {
- registeredEventProps = {
- variation: multiStepRegistrationExpVariation,
- };
- }
-
// This event is used by GTM
- sendTrackEvent('edx.bi.user.account.registered.client', registeredEventProps);
+ sendTrackEvent('edx.bi.user.account.registered.client', {});
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
}
- }, [registrationResult]); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [registrationResult]);
const handleOnChange = (event) => {
const { name } = event.target;
@@ -320,67 +247,7 @@ const RegistrationPage = (props) => {
const handleSubmit = (e) => {
e.preventDefault();
-
- if (multiStepRegistrationExpVariation === CONTROL
- && multiStepRegistrationPageStep === SECOND_STEP) {
- trackMultiStepRegistrationStep2SubmitBtnClicked(multiStepRegistrationExpVariation);
- }
- if (multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION
- && multiStepRegistrationPageStep === THIRD_STEP) {
- trackMultiStepRegistrationStep3SubmitBtnClicked();
- }
- if (multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION
- && multiStepRegistrationPageStep !== THIRD_STEP) {
- let formFieldsPayload = {};
-
- if (multiStepRegistrationPageStep === FIRST_STEP) {
- trackMultiStepRegistrationStep1SubmitBtnClicked(multiStepRegistrationExpVariation);
- // We only want to validate email in the first step of registration
- // Doing manual validations to avoid the case where user clicks CTA without focusing out of field.
- formFieldsPayload = { email: formFields.email };
- } else if (multiStepRegistrationPageStep === SECOND_STEP) {
- trackMultiStepRegistrationStep2SubmitBtnClicked(multiStepRegistrationExpVariation);
- // We only want to validate name and password field in the second step of registration
- // Doing manual validations to avoid the case where user clicks CTA without focusing out of field.
- formFieldsPayload = { name: formFields.name, password: formFields.password };
- }
-
- const { isValid, fieldErrors } = isFormValid(
- formFieldsPayload, errors, {}, {}, formatMessage,
- );
- setErrors(prevErrors => ({
- ...prevErrors,
- ...fieldErrors,
- }));
- // returning if not valid
- if (!isValid) {
- setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
- } else {
- dispatch(fetchRealtimeValidations(formFieldsPayload, true));
- }
- } else if (multiStepRegistrationExpVariation === CONTROL && multiStepRegistrationPageStep !== SECOND_STEP) {
- trackMultiStepRegistrationStep1SubmitBtnClicked(multiStepRegistrationExpVariation);
- // We only want to validate name, email and password fields in the first step of CONTROL registration
- // Doing manual validations to avoid the case where user clicks CTA without focusing out of field.
- const formFieldsPayload = { name: formFields.name, email: formFields.email, password: formFields.password };
-
- const { isValid, fieldErrors } = isFormValid(
- formFieldsPayload, errors, {}, {}, formatMessage,
- );
-
- setErrors(prevErrors => ({
- ...prevErrors,
- ...fieldErrors,
- }));
- // returning if not valid
- if (!isValid) {
- setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
- } else {
- dispatch(fetchRealtimeValidations(formFieldsPayload, true));
- }
- } else {
- registerUser();
- }
+ registerUser();
};
useEffect(() => {
@@ -415,150 +282,104 @@ const RegistrationPage = (props) => {
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
}
/>
- {(autoSubmitRegForm && !errorCode.type)
- || (!multiStepRegistrationExpVariation && !(registrationEmbedded || !!tpaHint || !!currentProvider))
- ? (
-
-
-
- ) : (
-
+
+
+ ) : (
+
- )}
+ )}
+
+
+ )}
+
>
);
};
diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx
index 1f6fd1a3..2634713f 100644
--- a/src/register/RegistrationPage.test.jsx
+++ b/src/register/RegistrationPage.test.jsx
@@ -17,9 +17,6 @@ import {
setUserPipelineDataLoaded,
} from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants';
-import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
-import useMultiStepRegistrationExperimentVariation
- from './data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
@@ -33,7 +30,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
-jest.mock('./data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -132,7 +128,6 @@ describe('RegistrationPage', () => {
institutionLogin: false,
};
window.location = { search: '' };
- useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx
index 5e69c7c0..be1f9c27 100644
--- a/src/register/components/ConfigurableRegistrationForm.jsx
+++ b/src/register/components/ConfigurableRegistrationForm.jsx
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import { FormFieldRenderer } from '../../field-renderer';
import { FIELDS } from '../data/constants';
-import { FIRST_STEP, shouldDisplayFieldInExperiment } from '../data/optimizelyExperiment/helper';
import messages from '../messages';
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
@@ -32,8 +31,6 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors,
setFormFields,
autoSubmitRegistrationForm,
- multiStepRegistrationExpVariation,
- multiStepRegistrationPageStep,
} = props;
const countryList = useMemo(() => getCountryList(getLocale()), []);
@@ -108,9 +105,7 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
};
- if (flags.showConfigurableRegistrationFields && shouldDisplayFieldInExperiment(
- 'other_fields', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
- )) {
+ if (flags.showConfigurableRegistrationFields) {
Object.keys(fieldDescriptions).forEach(fieldName => {
const fieldData = fieldDescriptions[fieldName];
switch (fieldData.name) {
@@ -162,9 +157,7 @@ const ConfigurableRegistrationForm = (props) => {
});
}
- if ((flags.showConfigurableEdxFields || showCountryField) && shouldDisplayFieldInExperiment(
- 'country', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
- )) {
+ if (flags.showConfigurableEdxFields || showCountryField) {
formFieldDescriptions.push(
{
);
}
- if (flags.showMarketingEmailOptInCheckbox && shouldDisplayFieldInExperiment(
- 'marketing_email_opt_in', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
- )) {
+ if (flags.showMarketingEmailOptInCheckbox) {
formFieldDescriptions.push(
{
);
}
- if ((flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode)
- && shouldDisplayFieldInExperiment(
- 'honor_code', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
- )) {
+ if (flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode) {
formFieldDescriptions.push(
@@ -239,15 +227,11 @@ ConfigurableRegistrationForm.propTypes = {
setFieldErrors: PropTypes.func.isRequired,
setFormFields: PropTypes.func.isRequired,
autoSubmitRegistrationForm: PropTypes.bool,
- multiStepRegistrationExpVariation: PropTypes.string,
- multiStepRegistrationPageStep: PropTypes.string,
};
ConfigurableRegistrationForm.defaultProps = {
fieldDescriptions: {},
autoSubmitRegistrationForm: false,
- multiStepRegistrationExpVariation: '',
- multiStepRegistrationPageStep: FIRST_STEP,
};
export default ConfigurableRegistrationForm;
diff --git a/src/register/components/RegistrationFailure.jsx b/src/register/components/RegistrationFailure.jsx
index f2a83585..c34c2af7 100644
--- a/src/register/components/RegistrationFailure.jsx
+++ b/src/register/components/RegistrationFailure.jsx
@@ -13,13 +13,12 @@ import {
TPA_AUTHENTICATION_FAILURE,
TPA_SESSION_EXPIRED,
} from '../data/constants';
-import { FIRST_STEP } from '../data/optimizelyExperiment/helper';
import messages from '../messages';
const RegistrationFailureMessage = (props) => {
const { formatMessage } = useIntl();
const {
- context, errorCode, failureCount, multiStepRegistrationPageStep,
+ context, errorCode, failureCount,
} = props;
useEffect(() => {
@@ -50,11 +49,7 @@ const RegistrationFailureMessage = (props) => {
errorMessage = formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider });
break;
default:
- if (multiStepRegistrationPageStep !== FIRST_STEP) {
- errorMessage = formatMessage(messages['multistep.registration.form.submission.error']);
- } else {
- errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
- }
+ errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
break;
}
@@ -70,7 +65,6 @@ RegistrationFailureMessage.defaultProps = {
context: {
errorMessage: null,
},
- multiStepRegistrationPageStep: FIRST_STEP,
};
RegistrationFailureMessage.propTypes = {
@@ -80,7 +74,6 @@ RegistrationFailureMessage.propTypes = {
}),
errorCode: PropTypes.string.isRequired,
failureCount: PropTypes.number.isRequired,
- multiStepRegistrationPageStep: PropTypes.string,
};
export default RegistrationFailureMessage;
diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
index 28269072..537d11eb 100644
--- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
+++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
@@ -11,9 +11,6 @@ import configureStore from 'redux-mock-store';
import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants';
-import { FIRST_STEP, NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
-import useMultiStepRegistrationExperimentVariation
- from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
@@ -25,7 +22,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
-jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
@@ -97,9 +93,6 @@ describe('ConfigurableRegistrationForm', () => {
registrationError: {},
registrationFormData,
usernameSuggestions: [],
- multiStepRegistrationPageStep: FIRST_STEP,
- multiStepRegExpVariation: '',
- isValidatingMultiStepRegistrationPage: false,
},
commonComponents: {
thirdPartyAuthApiStatus: null,
@@ -128,7 +121,6 @@ describe('ConfigurableRegistrationForm', () => {
};
window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us'));
- useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/RegistrationFailure.test.jsx b/src/register/components/tests/RegistrationFailure.test.jsx
index b9667c9c..003cc966 100644
--- a/src/register/components/tests/RegistrationFailure.test.jsx
+++ b/src/register/components/tests/RegistrationFailure.test.jsx
@@ -12,9 +12,6 @@ import configureStore from 'redux-mock-store';
import {
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
} from '../../data/constants';
-import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
-import useMultiStepRegistrationExperimentVariation
- from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import RegistrationFailureMessage from '../RegistrationFailure';
@@ -26,7 +23,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
-jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
@@ -125,7 +121,6 @@ describe('RegistrationFailure', () => {
institutionLogin: false,
};
window.location = { search: '' };
- useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/ThirdPartyAuth.test.jsx b/src/register/components/tests/ThirdPartyAuth.test.jsx
index 45672ad5..917f10f9 100644
--- a/src/register/components/tests/ThirdPartyAuth.test.jsx
+++ b/src/register/components/tests/ThirdPartyAuth.test.jsx
@@ -12,9 +12,6 @@ import configureStore from 'redux-mock-store';
import {
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants';
-import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
-import useMultiStepRegistrationExperimentVariation
- from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -25,7 +22,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
-jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -124,7 +120,6 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false,
};
window.location = { search: '' };
- useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/data/actions.js b/src/register/data/actions.js
index 530f3bc8..68bfe9e6 100644
--- a/src/register/data/actions.js
+++ b/src/register/data/actions.js
@@ -8,8 +8,6 @@ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERRO
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
-export const REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA = 'REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA';
-
// Backup registration form
export const backupRegistrationForm = () => ({
type: BACKUP_REGISTRATION_DATA.BASE,
@@ -21,19 +19,18 @@ export const backupRegistrationFormBegin = (data) => ({
});
// Validate fields from the backend
-export const fetchRealtimeValidations = (formPayload, isValidatingMultiStepRegistrationPage) => ({
+export const fetchRealtimeValidations = (formPayload) => ({
type: REGISTER_FORM_VALIDATIONS.BASE,
- payload: { formPayload, isValidatingMultiStepRegistrationPage },
+ payload: { formPayload },
});
-export const fetchRealtimeValidationsBegin = (isValidatingMultiStepRegistrationPage) => ({
+export const fetchRealtimeValidationsBegin = () => ({
type: REGISTER_FORM_VALIDATIONS.BEGIN,
- payload: { isValidatingMultiStepRegistrationPage },
});
-export const fetchRealtimeValidationsSuccess = (validations, isValidatingMultiStepRegistrationPage) => ({
+export const fetchRealtimeValidationsSuccess = (validations) => ({
type: REGISTER_FORM_VALIDATIONS.SUCCESS,
- payload: { validations, isValidatingMultiStepRegistrationPage },
+ payload: { validations },
});
export const fetchRealtimeValidationsFailure = () => ({
@@ -85,11 +82,3 @@ export const setUserPipelineDataLoaded = (value) => ({
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
payload: { value },
});
-
-// Multi Step Registration Experiment Actions
-export const setMultiStepRegistrationExpData = (
- multiStepRegExpVariation, multiStepRegistrationPageStep, isValidatingMultiStepRegistrationPage,
-) => ({
- type: REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA,
- payload: { multiStepRegExpVariation, multiStepRegistrationPageStep, isValidatingMultiStepRegistrationPage },
-});
diff --git a/src/register/data/optimizelyExperiment/helper.js b/src/register/data/optimizelyExperiment/helper.js
deleted file mode 100644
index f3d21381..00000000
--- a/src/register/data/optimizelyExperiment/helper.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * This file contains data for Multi Step Registration Optimizely experiment
- */
-import { getConfig } from '@edx/frontend-platform';
-
-import messages from '../../messages';
-
-export const NOT_INITIALIZED = 'experiment-not-initialized';
-export const CONTROL = 'control-registration-page';
-export const MULTI_STEP_REGISTRATION_EXP_VARIATION = 'multi-step-registration-page';
-
-export const FIRST_STEP = 'first-step';
-export const SECOND_STEP = 'second-step';
-export const THIRD_STEP = 'third-step';
-
-export const CONTROL_REGISTER_PAGE_FIRST_STEP_FIELDS = ['name', 'email', 'password', 'marketing_email_opt_in', 'ThirdPartyAuth'];
-export const CONTROL_REGISTER_PAGE_SECOND_STEP_FIELDS = ['username', 'country'];
-export const CONTROL_REGISTER_PAGE_COMMON_FIELDS = ['tos_and_honor_code', 'honor_code'];
-
-export const MULTI_STEP_REGISTER_PAGE_FIRST_STEP_FIELDS = ['email', 'marketing_email_opt_in', 'ThirdPartyAuth'];
-export const MULTI_STEP_REGISTER_PAGE_SECOND_STEP_FIELDS = ['name', 'password'];
-export const MULTI_STEP_REGISTER_PAGE_THIRD_STEP_FIELDS = ['username', 'country'];
-export const MULTI_STEP_REGISTER_PAGE_COMMON_FIELDS = ['tos_and_honor_code', 'honor_code'];
-
-const MULTI_STEP_REGISTRATION_EXP_PAGE = 'authn_register_page';
-
-export function getMultiStepRegistrationExperimentVariation() {
- try {
- if (window.optimizely
- && window.optimizely.get('data').experiments[getConfig().MULTI_STEP_REGISTRATION_EXPERIMENT_ID]) {
- const selectedVariant = window.optimizely.get('state').getVariationMap()[
- getConfig().MULTI_STEP_REGISTRATION_EXPERIMENT_ID
- ];
- return selectedVariant?.name;
- }
- } catch (e) { /* empty */ }
- return '';
-}
-
-export function activateMultiStepRegistrationExperiment() {
- window.optimizely = window.optimizely || [];
- window.optimizely.push({
- type: 'page',
- pageName: MULTI_STEP_REGISTRATION_EXP_PAGE,
- });
-}
-
-/**
- * We want to display username and honor_code fields in second page if user is in multi-step
- * registration page experiment
- */
-export const shouldDisplayFieldInExperiment = (fieldName, expVariation, registerPageStep) => (
- !expVariation || expVariation === NOT_INITIALIZED
- || (expVariation === CONTROL
- && (
- CONTROL_REGISTER_PAGE_COMMON_FIELDS.includes(fieldName)
- || (registerPageStep === FIRST_STEP && CONTROL_REGISTER_PAGE_FIRST_STEP_FIELDS.includes(fieldName))
- || (registerPageStep === SECOND_STEP && CONTROL_REGISTER_PAGE_SECOND_STEP_FIELDS.includes(fieldName))
- ))
- || (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION
- && (
- MULTI_STEP_REGISTER_PAGE_COMMON_FIELDS.includes(fieldName)
- || (registerPageStep === FIRST_STEP && MULTI_STEP_REGISTER_PAGE_FIRST_STEP_FIELDS.includes(fieldName))
- || (registerPageStep === SECOND_STEP && MULTI_STEP_REGISTER_PAGE_SECOND_STEP_FIELDS.includes(fieldName))
- || (registerPageStep === THIRD_STEP && MULTI_STEP_REGISTER_PAGE_THIRD_STEP_FIELDS.includes(fieldName))
- ))
-);
-
-export const getRegisterButtonLabelInExperiment = (
- existingButtonLabel, expVariation, registerPageStep, formatMessage,
-) => {
- if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep === FIRST_STEP) {
- return formatMessage(messages['multistep.registration.exp.continue.button']);
- }
- return existingButtonLabel;
-};
-
-export const getRegisterButtonClassInExperiment = (expVariation, registerPageStep) => {
- if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep === FIRST_STEP) {
- return 'continue-button';
- }
- return 'register-button';
-};
-
-export const getRegisterButtonSubmitStateInExperiment = (
- registerSubmitState, validationsSubmitState, expVariation, registerPageStep,
-) => {
- if ((expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep !== THIRD_STEP)
- || (expVariation === CONTROL && registerPageStep !== SECOND_STEP)) {
- return validationsSubmitState;
- }
- return registerSubmitState;
-};
-
-export const getMultiStepRegistrationPreviousStep = (currentStep) => {
- if (currentStep === THIRD_STEP) {
- return SECOND_STEP;
- }
- if (currentStep === SECOND_STEP) {
- return FIRST_STEP;
- }
- return currentStep;
-};
-
-export const getMultiStepRegistrationNextStep = (currentStep) => {
- if (currentStep === FIRST_STEP) {
- return SECOND_STEP;
- }
- if (currentStep === SECOND_STEP) {
- return THIRD_STEP;
- }
- return currentStep;
-};
diff --git a/src/register/data/optimizelyExperiment/track.js b/src/register/data/optimizelyExperiment/track.js
deleted file mode 100644
index 68a6347a..00000000
--- a/src/register/data/optimizelyExperiment/track.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { sendTrackEvent } from '@edx/frontend-platform/analytics';
-
-export const eventNames = {
- multiStepRegistrationStep1Viewed: 'edx.bi.user.multistepregistration.step1.viewed',
- multiStepRegistrationStep2Viewed: 'edx.bi.user.multistepregistration.step2.viewed',
- multiStepRegistrationStep3Viewed: 'edx.bi.user.multistepregistration.step3.viewed',
- multiStepRegistrationStep1SubmitBtnClicked: 'edx.bi.user.registration.step1.submit.click',
- multiStepRegistrationStep2SubmitBtnClicked: 'edx.bi.user.registration.step2.submit.click',
- multiStepRegistrationStep3SubmitBtnClicked: 'edx.bi.user.registration.step3.submit.click',
- multiStepRegistrationFormSubmitBtnClicked: 'edx.bi.user.registration.form.submit.click',
- multiStepRegistrationSSOBtnClicked: 'edx.bi.user.registration.sso.btn.click',
-};
-
-export const trackMultiStepRegistrationStep1Viewed = (expVariation) => {
- sendTrackEvent(eventNames.multiStepRegistrationStep1Viewed, {
- variation: expVariation,
- });
-};
-
-export const trackMultiStepRegistrationStep2Viewed = (expVariation, isMarketingLead) => {
- sendTrackEvent(eventNames.multiStepRegistrationStep2Viewed, {
- variation: expVariation,
- is_marketing_lead: isMarketingLead,
- });
-};
-
-export const trackMultiStepRegistrationStep3Viewed = () => {
- sendTrackEvent(eventNames.multiStepRegistrationStep3Viewed, {});
-};
-
-export const trackMultiStepRegistrationStep1SubmitBtnClicked = (expVariation) => {
- sendTrackEvent(eventNames.multiStepRegistrationStep1SubmitBtnClicked, {
- variation: expVariation,
- });
-};
-
-export const trackMultiStepRegistrationStep2SubmitBtnClicked = (expVariation) => {
- sendTrackEvent(eventNames.multiStepRegistrationStep2SubmitBtnClicked, {
- variation: expVariation,
- });
-};
-
-export const trackMultiStepRegistrationStep3SubmitBtnClicked = () => {
- sendTrackEvent(eventNames.multiStepRegistrationStep3SubmitBtnClicked, {});
-};
-
-export const trackMultiStepRegistrationFormSubmitBtnClicked = (expVariation) => {
- sendTrackEvent(eventNames.multiStepRegistrationFormSubmitBtnClicked, {
- variation: expVariation,
- });
-};
-
-export const trackMultiStepRegistrationSSOBtnClicked = (expVariation) => {
- sendTrackEvent(eventNames.multiStepRegistrationSSOBtnClicked, {
- variation: expVariation,
- });
-};
diff --git a/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx b/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
deleted file mode 100644
index 97d0a233..00000000
--- a/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import {
- activateMultiStepRegistrationExperiment,
- getMultiStepRegistrationExperimentVariation,
- NOT_INITIALIZED,
-} from './helper';
-import { trackMultiStepRegistrationStep1Viewed } from './track';
-import { COMPLETE_STATE } from '../../../data/constants';
-
-/**
- * This hook returns activates multi step registration experiment and returns the experiment
- * variation for the user.
- */
-const useMultiStepRegistrationExperimentVariation = (
- initExpVariation,
- registrationEmbedded,
- tpaHint,
- currentProvider,
- thirdPartyAuthApiStatus,
-) => {
- const [variation, setVariation] = useState(initExpVariation);
-
- useEffect(() => {
- if (initExpVariation || registrationEmbedded || !!tpaHint || !!currentProvider
- || thirdPartyAuthApiStatus !== COMPLETE_STATE) {
- return variation;
- }
-
- const getVariation = () => {
- const expVariation = getMultiStepRegistrationExperimentVariation();
- if (expVariation) {
- setVariation(expVariation);
- trackMultiStepRegistrationStep1Viewed(expVariation);
- } else {
- // This is to handle the case when user dont get variation for some reason, the register page
- // shows unlimited spinner.
- setVariation(NOT_INITIALIZED);
- }
- };
-
- activateMultiStepRegistrationExperiment();
-
- const timer = setTimeout(getVariation, 300);
-
- return () => {
- clearTimeout(timer);
- };
- }, [ // eslint-disable-line react-hooks/exhaustive-deps
- currentProvider, initExpVariation, registrationEmbedded, thirdPartyAuthApiStatus, tpaHint,
- ]);
-
- return variation;
-};
-
-export default useMultiStepRegistrationExperimentVariation;
diff --git a/src/register/data/reducers.js b/src/register/data/reducers.js
index 32438877..70c3a994 100644
--- a/src/register/data/reducers.js
+++ b/src/register/data/reducers.js
@@ -5,11 +5,9 @@ import {
REGISTER_NEW_USER,
REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS,
- REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA,
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from './actions';
-import { FIRST_STEP } from './optimizelyExperiment/helper';
import {
DEFAULT_STATE,
PENDING_STATE,
@@ -37,14 +35,10 @@ export const defaultState = {
},
validations: null,
submitState: DEFAULT_STATE,
- validationsSubmitState: DEFAULT_STATE,
userPipelineDataLoaded: false,
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
- multiStepRegExpVariation: '',
- multiStepRegistrationPageStep: FIRST_STEP,
- isValidatingMultiStepRegistrationPage: false,
};
const reducer = (state = defaultState, action = {}) => {
@@ -91,22 +85,12 @@ const reducer = (state = defaultState, action = {}) => {
registrationError: { ...registrationErrorTemp },
};
}
- case REGISTER_FORM_VALIDATIONS.BEGIN: {
- return {
- ...state,
- validationsSubmitState: action.payload?.isValidatingMultiStepRegistrationPage
- ? PENDING_STATE
- : state.validationsSubmitState,
- };
- }
case REGISTER_FORM_VALIDATIONS.SUCCESS: {
const { usernameSuggestions, ...validationWithoutUsernameSuggestions } = action.payload.validations;
return {
...state,
validations: validationWithoutUsernameSuggestions,
- isValidatingMultiStepRegistrationPage: !!action.payload?.isValidatingMultiStepRegistrationPage,
usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
- validationsSubmitState: DEFAULT_STATE,
};
}
case REGISTER_FORM_VALIDATIONS.FAILURE:
@@ -114,7 +98,6 @@ const reducer = (state = defaultState, action = {}) => {
...state,
validationApiRateLimited: true,
validations: null,
- validationsSubmitState: DEFAULT_STATE,
};
case REGISTER_CLEAR_USERNAME_SUGGESTIONS:
return {
@@ -146,14 +129,6 @@ const reducer = (state = defaultState, action = {}) => {
emailSuggestion: action.payload.emailSuggestion,
},
};
- case REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA: {
- return {
- ...state,
- multiStepRegExpVariation: action.payload.multiStepRegExpVariation,
- multiStepRegistrationPageStep: action.payload.multiStepRegistrationPageStep,
- isValidatingMultiStepRegistrationPage: !!action.payload?.isValidatingMultiStepRegistrationPage,
- };
- }
default:
return {
...state,
diff --git a/src/register/data/sagas.js b/src/register/data/sagas.js
index 3edd395e..0325cd4b 100644
--- a/src/register/data/sagas.js
+++ b/src/register/data/sagas.js
@@ -40,13 +40,10 @@ export function* handleNewUserRegistration(action) {
export function* fetchRealtimeValidations(action) {
try {
- yield put(fetchRealtimeValidationsBegin(action.payload?.isValidatingMultiStepRegistrationPage));
+ yield put(fetchRealtimeValidationsBegin());
const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
- yield put(fetchRealtimeValidationsSuccess(
- camelCaseObject(fieldValidations),
- action.payload?.isValidatingMultiStepRegistrationPage,
- ));
+ yield put(fetchRealtimeValidationsSuccess(camelCaseObject(fieldValidations)));
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(fetchRealtimeValidationsFailure());
diff --git a/src/register/data/tests/reducers.test.js b/src/register/data/tests/reducers.test.js
index 909704d2..3e2270ab 100644
--- a/src/register/data/tests/reducers.test.js
+++ b/src/register/data/tests/reducers.test.js
@@ -11,7 +11,6 @@ import {
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from '../actions';
-import { FIRST_STEP } from '../optimizelyExperiment/helper';
import reducer from '../reducers';
describe('Registration Reducer Tests', () => {
@@ -35,14 +34,10 @@ describe('Registration Reducer Tests', () => {
},
validations: null,
submitState: DEFAULT_STATE,
- validationsSubmitState: DEFAULT_STATE,
userPipelineDataLoaded: false,
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
- multiStepRegExpVariation: '',
- multiStepRegistrationPageStep: FIRST_STEP,
- isValidatingMultiStepRegistrationPage: false,
};
it('should return the initial state', () => {
diff --git a/src/register/data/utils.js b/src/register/data/utils.js
index 747f2f2c..ee08f0b6 100644
--- a/src/register/data/utils.js
+++ b/src/register/data/utils.js
@@ -43,39 +43,31 @@ export const isFormValid = (
Object.keys(payload).forEach(key => {
switch (key) {
case 'name':
- if (!fieldErrors.name) {
- fieldErrors.name = validateName(payload.name, formatMessage);
- }
+ fieldErrors.name = validateName(payload.name, formatMessage);
if (fieldErrors.name) { isValid = false; }
break;
case 'email': {
- if (!fieldErrors.email) {
- const {
- fieldError, confirmEmailError, suggestion,
- } = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
- if (fieldError) {
- fieldErrors.email = fieldError;
- isValid = false;
- }
- if (confirmEmailError) {
- fieldErrors.confirm_email = confirmEmailError;
- isValid = false;
- }
- emailSuggestion = suggestion;
+ const {
+ fieldError, confirmEmailError, suggestion,
+ } = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
+ if (fieldError) {
+ fieldErrors.email = fieldError;
+ isValid = false;
}
+ if (confirmEmailError) {
+ fieldErrors.confirm_email = confirmEmailError;
+ isValid = false;
+ }
+ emailSuggestion = suggestion;
if (fieldErrors.email) { isValid = false; }
break;
}
case 'username':
- if (!fieldErrors.username) {
- fieldErrors.username = validateUsername(payload.username, formatMessage);
- }
+ fieldErrors.username = validateUsername(payload.username, formatMessage);
if (fieldErrors.username) { isValid = false; }
break;
case 'password':
- if (!fieldErrors.password) {
- fieldErrors.password = validatePasswordField(payload.password, formatMessage);
- }
+ fieldErrors.password = validatePasswordField(payload.password, formatMessage);
if (fieldErrors.password) { isValid = false; }
break;
default:
diff --git a/src/register/messages.jsx b/src/register/messages.jsx
index cea5e5df..39d9e7f5 100644
--- a/src/register/messages.jsx
+++ b/src/register/messages.jsx
@@ -201,29 +201,6 @@ const messages = defineMessages({
defaultMessage: 'Did you mean',
description: 'Did you mean alert suggestion',
},
- // MultiStep Registration experiment
- 'multistep.registration.exp.continue.button': {
- id: 'multistep.registration.exp.continue.button',
- defaultMessage: 'Continue',
- description: 'Label text for multistep registration page second step',
- },
- 'multistep.registration.username.second.step.guideline.content': {
- id: 'multistep.registration.username.second.step.guideline.content',
- defaultMessage: 'Finish Registration',
- description: 'Guideline content for username field in multi-step registration experiment step 2',
- },
- 'multistep.registration.username.third.step.guideline.content': {
- id: 'multistep.registration.username.third.step.guideline.content',
- defaultMessage: 'To finalize your registration, please confirm your country of residence '
- + 'and create a public username that will identify you in your course communication forums. '
- + 'The username cannot be changed.',
- description: 'Guideline content for username field in multi-step registration experiment step 2',
- },
- 'multistep.registration.form.submission.error': {
- id: 'multistep.registration.form.submission.error',
- defaultMessage: 'Please check your responses for this and the previous step and try again.',
- description: 'Error message that appears on top of the form when invalid form is submitted',
- },
});
export default messages;
diff --git a/src/sass/_registration.scss b/src/sass/_registration.scss
index 884af10d..75889601 100644
--- a/src/sass/_registration.scss
+++ b/src/sass/_registration.scss
@@ -2,10 +2,6 @@
min-width: 14.4rem;
}
-.continue-button {
- min-width: 7rem;
-}
-
.pgn__form-autosuggest__wrapper > .pgn__form-group {
margin-bottom: 0 !important;
}
From 52e438652c306ea60b80308a2450d1a1381fbfb1 Mon Sep 17 00:00:00 2001
From: Blue
Date: Tue, 7 May 2024 16:44:47 +0500
Subject: [PATCH 15/82] 2u-main rebase with master (#1246)
Rebase 2u-main with master
---
.env | 1 +
src/common-components/ThirdPartyAuth.jsx | 3 +-
src/config/index.js | 1 +
.../CountryField/CountryField.jsx | 2 +-
src/register/RegistrationPage.jsx | 26 ++++++----
src/register/RegistrationPage.test.jsx | 49 ++++++++++++++++++-
.../ConfigurableRegistrationForm.jsx | 6 ++-
7 files changed, 73 insertions(+), 15 deletions(-)
diff --git a/.env b/.env
index 661e6802..9d2c9658 100644
--- a/.env
+++ b/.env
@@ -23,6 +23,7 @@ POST_REGISTRATION_REDIRECT_URL=''
SEARCH_CATALOG_URL=''
# ***** Features flags *****
DISABLE_ENTERPRISE_LOGIN=''
+ENABLE_AUTO_GENERATED_USERNAME=''
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
diff --git a/src/common-components/ThirdPartyAuth.jsx b/src/common-components/ThirdPartyAuth.jsx
index 3b782036..7dcef3e1 100644
--- a/src/common-components/ThirdPartyAuth.jsx
+++ b/src/common-components/ThirdPartyAuth.jsx
@@ -37,6 +37,7 @@ const ThirdPartyAuth = (props) => {
const isSocialAuthActive = !!providers.length && !currentProvider;
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
+ const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
return (
<>
@@ -61,7 +62,7 @@ const ThirdPartyAuth = (props) => {
)}
- {thirdPartyAuthApiStatus === PENDING_STATE ? (
+ {thirdPartyAuthApiStatus === PENDING_STATE && isThirdPartyAuthActive ? (
diff --git a/src/config/index.js b/src/config/index.js
index fa3613aa..badb6fe7 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -4,6 +4,7 @@ const configuration = {
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
// Features
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
+ ENABLE_AUTO_GENERATED_USERNAME: process.env.ENABLE_AUTO_GENERATED_USERNAME || false,
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,
diff --git a/src/register/RegistrationFields/CountryField/CountryField.jsx b/src/register/RegistrationFields/CountryField/CountryField.jsx
index c411750b..4f9e06c6 100644
--- a/src/register/RegistrationFields/CountryField/CountryField.jsx
+++ b/src/register/RegistrationFields/CountryField/CountryField.jsx
@@ -97,7 +97,7 @@ const CountryField = (props) => {
};
const getCountryList = () => countryList.map((country) => (
-
+
{country[COUNTRY_DISPLAY_KEY]}
));
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 80ac1f6b..8c5ea79f 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -60,6 +60,7 @@ const RegistrationPage = (props) => {
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
+ autoGeneratedUsernameEnabled: getConfig().ENABLE_AUTO_GENERATED_USERNAME,
};
const {
handleInstitutionLogin,
@@ -215,6 +216,9 @@ const RegistrationPage = (props) => {
delete payload.password;
payload.social_auth_provider = currentProvider;
}
+ if (flags.autoGeneratedUsernameEnabled) {
+ delete payload.username;
+ }
// Validating form data before submitting
const { isValid, fieldErrors, emailSuggestion } = isFormValid(
@@ -324,16 +328,18 @@ const RegistrationPage = (props) => {
helpText={[formatMessage(messages['help.text.email'])]}
floatingLabel={formatMessage(messages['registration.email.label'])}
/>
-
+ {!flags.autoGeneratedUsernameEnabled && (
+
+ )}
{!currentProvider && (
{
jest.clearAllMocks();
});
- const populateRequiredFields = (getByLabelText, payload, isThirdPartyAuth = false) => {
+ const populateRequiredFields = (
+ getByLabelText,
+ payload,
+ isThirdPartyAuth = false,
+ autoGeneratedUsernameEnabled = false,
+ ) => {
fireEvent.change(getByLabelText('Full name'), { target: { value: payload.name, name: 'name' } });
- fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
+ if (!autoGeneratedUsernameEnabled) {
+ fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
+ }
fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } });
fireEvent.change(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } });
@@ -299,6 +306,44 @@ describe('RegistrationPage', () => {
});
});
+ it('should submit form without UsernameField when autoGeneratedUsernameEnabled is true', () => {
+ mergeConfig({
+ ENABLE_AUTO_GENERATED_USERNAME: true,
+ });
+ jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
+ const payload = {
+ name: 'John Doe',
+ email: 'john.doe@gmail.com',
+ password: 'password1',
+ country: 'Pakistan',
+ honor_code: true,
+ totalRegistrationTime: 0,
+ };
+
+ store.dispatch = jest.fn(store.dispatch);
+ const { getByLabelText, container } = render(routerWrapper(reduxWrapper()));
+ populateRequiredFields(getByLabelText, payload, false, true);
+ const button = container.querySelector('button.btn-brand');
+ fireEvent.click(button);
+ expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
+ mergeConfig({
+ ENABLE_AUTO_GENERATED_USERNAME: false,
+ });
+ });
+
+ it('should not display UsernameField when ENABLE_AUTO_GENERATED_USERNAME is true', () => {
+ mergeConfig({
+ ENABLE_AUTO_GENERATED_USERNAME: true,
+ });
+
+ const { queryByLabelText } = render(routerWrapper(reduxWrapper()));
+ expect(queryByLabelText('Username')).toBeNull();
+
+ mergeConfig({
+ ENABLE_AUTO_GENERATED_USERNAME: false,
+ });
+ });
+
it('should not dispatch registerNewUser on empty form Submission', () => {
store.dispatch = jest.fn(store.dispatch);
diff --git a/src/register/components/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx
index be1f9c27..8c300b7e 100644
--- a/src/register/components/ConfigurableRegistrationForm.jsx
+++ b/src/register/components/ConfigurableRegistrationForm.jsx
@@ -33,7 +33,11 @@ const ConfigurableRegistrationForm = (props) => {
autoSubmitRegistrationForm,
} = props;
- const countryList = useMemo(() => getCountryList(getLocale()), []);
+ /** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
+ States' instead of 'United States of America' which does not exist in country dropdown list and gets the user
+ confused and unable to create an account. So we added the United States entry in the dropdown list.
+ */
+ const countryList = useMemo(() => getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []);
let showTermsOfServiceAndHonorCode = false;
let showCountryField = false;
From dcbd644a257573306d4ae64f3b034925cc51f388 Mon Sep 17 00:00:00 2001
From: Blue
Date: Mon, 13 May 2024 14:11:03 +0500
Subject: [PATCH 16/82] feat: implement auto generated username experiment
(#1248)
* feat: implement auto generated username registration exp
---
src/config/index.js | 1 +
src/logistration/Logistration.test.jsx | 5 +
src/register/RegistrationPage.jsx | 203 ++++++++++--------
src/register/RegistrationPage.test.jsx | 5 +
.../ConfigurableRegistrationForm.test.jsx | 5 +
.../tests/RegistrationFailure.test.jsx | 5 +
.../components/tests/ThirdPartyAuth.test.jsx | 5 +
src/register/data/actions.js | 7 +
.../data/optimizelyExperiment/helper.js | 30 +++
...utoGeneratedUsernameExperimentVariation.js | 53 +++++
src/register/data/reducers.js | 8 +
src/register/data/tests/reducers.test.js | 1 +
12 files changed, 234 insertions(+), 94 deletions(-)
create mode 100644 src/register/data/optimizelyExperiment/helper.js
create mode 100644 src/register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation.js
diff --git a/src/config/index.js b/src/config/index.js
index badb6fe7..78cea882 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -35,6 +35,7 @@ const configuration = {
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
+ AUTO_GENERATED_USERNAME_EXPERIMENT_ID: process.env.AUTO_GENERATED_USERNAME_EXPERIMENT_ID || '',
};
export default configuration;
diff --git a/src/logistration/Logistration.test.jsx b/src/logistration/Logistration.test.jsx
index 87bf3e70..b16ae216 100644
--- a/src/logistration/Logistration.test.jsx
+++ b/src/logistration/Logistration.test.jsx
@@ -15,12 +15,16 @@ import {
} from '../data/constants';
import { backupLoginForm } from '../login/data/actions';
import { backupRegistrationForm } from '../register/data/actions';
+import { NOT_INITIALIZED } from '../register/data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation
+ from '../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
+jest.mock('../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
@@ -84,6 +88,7 @@ describe('Logistration', () => {
})),
}));
+ useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({
loggingService: { logError: jest.fn() },
config: {
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 8c5ea79f..22f12e14 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -18,6 +18,7 @@ import {
backupRegistrationFormBegin,
clearRegistrationBackendError,
registerNewUser,
+ setAutoGeneratedUsernameExperimentData,
setEmailSuggestionInStore,
setUserPipelineDataLoaded,
} from './data/actions';
@@ -25,6 +26,8 @@ import {
FORM_SUBMISSION_ERROR,
TPA_AUTHENTICATION_FAILURE,
} from './data/constants';
+import { AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION, NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import getBackendValidations from './data/selectors';
import {
isFormValid, prepareRegistrationPayload,
@@ -68,6 +71,7 @@ const RegistrationPage = (props) => {
} = props;
const backedUpFormData = useSelector(state => state.register.registrationFormData);
+ const initExpVariation = useSelector(state => state.register.autoGeneratedUsernameExperimentVariation);
const registrationError = useSelector(state => state.register.registrationError);
const registrationErrorCode = registrationError?.errorCode;
const registrationResult = useSelector(state => state.register.registrationResult);
@@ -103,6 +107,12 @@ const RegistrationPage = (props) => {
? formatMessage(messages['create.account.cta.button'], { label: cta })
: formatMessage(messages['create.account.for.free.button']);
+ const autoGeneratedUsernameExpVariation = useAutoGeneratedUsernameExperimentVariation(
+ initExpVariation, registrationEmbedded, tpaHint, currentProvider, thirdPartyAuthApiStatus,
+ );
+
+ const hideUsernameField = flags.autoGeneratedUsernameEnabled
+ || autoGeneratedUsernameExpVariation === AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION;
/**
* Set the userPipelineDetails data in formFields for only first time
*/
@@ -149,8 +159,10 @@ const RegistrationPage = (props) => {
formFields: { ...formFields },
errors: { ...errors },
}));
+ dispatch(setAutoGeneratedUsernameExperimentData(autoGeneratedUsernameExpVariation));
}
- }, [shouldBackupState, configurableFormFields, formFields, errors, dispatch, backedUpFormData]);
+ }, [shouldBackupState, configurableFormFields, // eslint-disable-line react-hooks/exhaustive-deps
+ formFields, errors, dispatch, backedUpFormData]);
useEffect(() => {
if (backendValidations) {
@@ -216,7 +228,7 @@ const RegistrationPage = (props) => {
delete payload.password;
payload.social_auth_provider = currentProvider;
}
- if (flags.autoGeneratedUsernameEnabled) {
+ if (hideUsernameField) {
delete payload.username;
}
@@ -286,106 +298,109 @@ const RegistrationPage = (props) => {
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
}
/>
- {autoSubmitRegForm && !errorCode.type ? (
-
-
-
- ) : (
-
-
-
-
+ ) : (
+
+
-
- {!flags.autoGeneratedUsernameEnabled && (
-
+
- )}
- {!currentProvider && (
-
- )}
-
- e.preventDefault()}
- />
- {!registrationEmbedded && (
-
+ )}
+ {!currentProvider && (
+
+ )}
+
- )}
-
-
- )}
-
+ e.preventDefault()}
+ />
+ {!registrationEmbedded && (
+
+ )}
+
+
+ )}
>
);
};
diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx
index 92c74854..9773bf03 100644
--- a/src/register/RegistrationPage.test.jsx
+++ b/src/register/RegistrationPage.test.jsx
@@ -17,6 +17,9 @@ import {
setUserPipelineDataLoaded,
} from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants';
+import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation
+ from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
@@ -30,6 +33,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('./data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -128,6 +132,7 @@ describe('RegistrationPage', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
index 537d11eb..93978509 100644
--- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
+++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
@@ -11,6 +11,9 @@ import configureStore from 'redux-mock-store';
import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants';
+import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation
+ from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
@@ -22,6 +25,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
@@ -121,6 +125,7 @@ describe('ConfigurableRegistrationForm', () => {
};
window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us'));
+ useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/RegistrationFailure.test.jsx b/src/register/components/tests/RegistrationFailure.test.jsx
index 003cc966..da8867cd 100644
--- a/src/register/components/tests/RegistrationFailure.test.jsx
+++ b/src/register/components/tests/RegistrationFailure.test.jsx
@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
} from '../../data/constants';
+import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation
+ from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import RegistrationFailureMessage from '../RegistrationFailure';
@@ -23,6 +26,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
@@ -121,6 +125,7 @@ describe('RegistrationFailure', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/ThirdPartyAuth.test.jsx b/src/register/components/tests/ThirdPartyAuth.test.jsx
index 917f10f9..7706d185 100644
--- a/src/register/components/tests/ThirdPartyAuth.test.jsx
+++ b/src/register/components/tests/ThirdPartyAuth.test.jsx
@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants';
+import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation
+ from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -22,6 +25,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -120,6 +124,7 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/data/actions.js b/src/register/data/actions.js
index 68bfe9e6..2c122515 100644
--- a/src/register/data/actions.js
+++ b/src/register/data/actions.js
@@ -8,6 +8,7 @@ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERRO
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
+export const REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA = 'REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA';
// Backup registration form
export const backupRegistrationForm = () => ({
type: BACKUP_REGISTRATION_DATA.BASE,
@@ -82,3 +83,9 @@ export const setUserPipelineDataLoaded = (value) => ({
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
payload: { value },
});
+
+// Auto Generated Username Registration Experiment Actions
+export const setAutoGeneratedUsernameExperimentData = (autoGeneratedRegExpVariation) => ({
+ type: REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
+ payload: { autoGeneratedRegExpVariation },
+});
diff --git a/src/register/data/optimizelyExperiment/helper.js b/src/register/data/optimizelyExperiment/helper.js
new file mode 100644
index 00000000..0cc54183
--- /dev/null
+++ b/src/register/data/optimizelyExperiment/helper.js
@@ -0,0 +1,30 @@
+/**
+ * This file contains data for auto generated username Optimizely experiment
+ */
+import { getConfig } from '@edx/frontend-platform';
+
+export const NOT_INITIALIZED = 'experiment-not-initialized';
+export const CONTROL = 'control-registration-page';
+export const AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION = 'auto-generated-username-register-page';
+const AUTO_GENERATED_USERNAME_EXP_PAGE = 'targeting_for_auto_generated_username_page';
+
+export function getAutoGeneratedUsernameExperimentVariation() {
+ try {
+ if (window.optimizely
+ && window.optimizely.get('data').experiments[getConfig().AUTO_GENERATED_USERNAME_EXPERIMENT_ID]) {
+ const selectedVariant = window.optimizely.get('state').getVariationMap()[
+ getConfig().AUTO_GENERATED_USERNAME_EXPERIMENT_ID
+ ];
+ return selectedVariant?.name;
+ }
+ } catch (e) { /* empty */ }
+ return '';
+}
+
+export function activateAutoGeneratedUsernameExperiment() {
+ window.optimizely = window.optimizely || [];
+ window.optimizely.push({
+ type: 'page',
+ pageName: AUTO_GENERATED_USERNAME_EXP_PAGE,
+ });
+}
diff --git a/src/register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation.js b/src/register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation.js
new file mode 100644
index 00000000..5442a652
--- /dev/null
+++ b/src/register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation.js
@@ -0,0 +1,53 @@
+import { useEffect, useState } from 'react';
+
+import {
+ activateAutoGeneratedUsernameExperiment,
+ getAutoGeneratedUsernameExperimentVariation,
+ NOT_INITIALIZED,
+} from './helper';
+import { COMPLETE_STATE } from '../../../data/constants';
+
+/**
+ * This hook returns activates multi step registration experiment and returns the experiment
+ * variation for the user.
+ */
+const useAutoGeneratedUsernameExperimentVariation = (
+ initExpVariation,
+ registrationEmbedded,
+ tpaHint,
+ currentProvider,
+ thirdPartyAuthApiStatus,
+) => {
+ const [variation, setVariation] = useState(initExpVariation);
+ useEffect(() => {
+ if (initExpVariation || registrationEmbedded || !!tpaHint || !!currentProvider
+ || thirdPartyAuthApiStatus !== COMPLETE_STATE) {
+ return variation;
+ }
+
+ const getVariation = () => {
+ const expVariation = getAutoGeneratedUsernameExperimentVariation();
+ if (expVariation) {
+ setVariation(expVariation);
+ } else {
+ // This is to handle the case when user dont get variation for some reason, the register page
+ // shows unlimited spinner.
+ setVariation(NOT_INITIALIZED);
+ }
+ };
+
+ activateAutoGeneratedUsernameExperiment();
+
+ const timer = setTimeout(getVariation, 300);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [ // eslint-disable-line react-hooks/exhaustive-deps
+ initExpVariation, currentProvider, registrationEmbedded, thirdPartyAuthApiStatus, tpaHint,
+ ]);
+
+ return variation;
+};
+
+export default useAutoGeneratedUsernameExperimentVariation;
diff --git a/src/register/data/reducers.js b/src/register/data/reducers.js
index 70c3a994..d8a822df 100644
--- a/src/register/data/reducers.js
+++ b/src/register/data/reducers.js
@@ -3,6 +3,7 @@ import {
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTER_FORM_VALIDATIONS,
REGISTER_NEW_USER,
+ REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS,
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
@@ -39,6 +40,7 @@ export const defaultState = {
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
+ autoGeneratedUsernameExperimentVariation: '',
};
const reducer = (state = defaultState, action = {}) => {
@@ -55,6 +57,12 @@ const reducer = (state = defaultState, action = {}) => {
registrationFormData: { ...action.payload },
userPipelineDataLoaded: state.userPipelineDataLoaded,
};
+ case REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA: {
+ return {
+ ...state,
+ autoGeneratedUsernameExperimentVariation: action.payload.autoGeneratedRegExpVariation,
+ };
+ }
case REGISTER_NEW_USER.BEGIN:
return {
...state,
diff --git a/src/register/data/tests/reducers.test.js b/src/register/data/tests/reducers.test.js
index 3e2270ab..39fa4cee 100644
--- a/src/register/data/tests/reducers.test.js
+++ b/src/register/data/tests/reducers.test.js
@@ -38,6 +38,7 @@ describe('Registration Reducer Tests', () => {
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
+ autoGeneratedUsernameExperimentVariation: '',
};
it('should return the initial state', () => {
From 5754c2961acc535401d12a3db21b6be5609b8298 Mon Sep 17 00:00:00 2001
From: Blue
Date: Tue, 4 Jun 2024 16:27:14 +0500
Subject: [PATCH 17/82] feat: add page event for reset password (#1253)
Description: Add page event for reset password page
VAN-1929
---
src/forgot-password/ForgotPasswordPage.jsx | 2 +-
src/reset-password/ResetPasswordPage.jsx | 5 +++++
src/reset-password/tests/ResetPasswordPage.test.jsx | 4 ++++
3 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/forgot-password/ForgotPasswordPage.jsx
index ff381340..f4c7b314 100644
--- a/src/forgot-password/ForgotPasswordPage.jsx
+++ b/src/forgot-password/ForgotPasswordPage.jsx
@@ -41,7 +41,7 @@ const ForgotPasswordPage = (props) => {
const navigate = useNavigate();
useEffect(() => {
- sendPageEvent('login_and_registration', 'reset');
+ sendPageEvent('login_and_registration', 'forgot-password');
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
}, []);
diff --git a/src/reset-password/ResetPasswordPage.jsx b/src/reset-password/ResetPasswordPage.jsx
index 9b4b6758..0393d279 100644
--- a/src/reset-password/ResetPasswordPage.jsx
+++ b/src/reset-password/ResetPasswordPage.jsx
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
+import { sendPageEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form,
@@ -42,6 +43,10 @@ const ResetPasswordPage = (props) => {
const { token } = useParams();
const navigate = useNavigate();
+ useEffect(() => {
+ sendPageEvent('login_and_registration', 'reset-password');
+ }, []);
+
useEffect(() => {
if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) {
setErrorCode(props.status);
diff --git a/src/reset-password/tests/ResetPasswordPage.test.jsx b/src/reset-password/tests/ResetPasswordPage.test.jsx
index 421edc9e..0c9ba1a2 100644
--- a/src/reset-password/tests/ResetPasswordPage.test.jsx
+++ b/src/reset-password/tests/ResetPasswordPage.test.jsx
@@ -19,6 +19,10 @@ import ResetPasswordPage from '../ResetPasswordPage';
const mockedNavigator = jest.fn();
const token = '1c-bmjdkc-5e60e084cf8113048ca7';
+jest.mock('@edx/frontend-platform/analytics', () => ({
+ sendPageEvent: jest.fn(),
+}));
+
jest.mock('@edx/frontend-platform/auth');
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')),
From bd63bb1f15f4f7ba8a22bb683e10839f64182077 Mon Sep 17 00:00:00 2001
From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com>
Date: Fri, 7 Jun 2024 07:57:42 +0500
Subject: [PATCH 18/82] Update 2u-main with master (#1254)
* feat: Hide preloaders for third party auth providers if they are disabled
* feat: remove username from the registration from (#1201) (#1241)
Co-authored-by: Attiya Ishaque
* fix: add new entry for another US label (#1244)
Add new entry for for another US label which is United States
* feat: implement multi step registration experiment
Rebase 2u main with master (#1228)
* chore(deps): update dependency babel-plugin-formatjs to v10.5.14
* fix(deps): update dependency @edx/frontend-platform to v7.1.3
* fix(deps): update font awesome to v6.5.2
* chore(deps): update dependency @openedx/frontend-build to v13.1.4
* fix(deps): update dependency @openedx/paragon to v22.2.1
* fix(deps): update dependency algoliasearch to v4.23.3
* fix(deps): update dependency algoliasearch-helper to v3.17.0
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* feat: add multi step registration eventing (#1226)
* feat: implement multi step registration experiment
* feat: add multi step registration eventing
* fix: fix register button width
* fix: fix register button loader for control
* feat: capture marketing lead in experiment events (#1243)
* revert: multistep registration experiment
revert multistep registration experiment changes
VAN-1930
* feat: implement auto generated username experiment (#1248)
* feat: implement auto generated username registration exp
* feat: add page event for reset password (#1253)
Description: Add page event for reset password page
VAN-1929
---------
Co-authored-by: Stanislav Lunyachek
Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
Co-authored-by: Attiya Ishaque
Co-authored-by: Blue
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syed Sajjad Hussain Shah
From efaa83a1bcaeeba300b1228af520aab6f3f844a5 Mon Sep 17 00:00:00 2001
From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com>
Date: Tue, 11 Jun 2024 12:01:30 +0500
Subject: [PATCH 19/82] feat: hard code fields on frontend (#1256)
* feat: hard code fields
hard code configurable fields on frontend which includes country field on register page & level of education & gender field on progressive profiling
VAN-1971
* fix: fix secondary provider null name issue
---
.../InstitutionLogistration.jsx | 2 +-
src/common-components/data/constants.js | 79 +++++++++++++++++++
src/common-components/data/sagas.js | 13 ++-
src/config/index.js | 1 +
4 files changed, 93 insertions(+), 2 deletions(-)
create mode 100644 src/common-components/data/constants.js
diff --git a/src/common-components/InstitutionLogistration.jsx b/src/common-components/InstitutionLogistration.jsx
index b773bc6e..ba26cf3c 100644
--- a/src/common-components/InstitutionLogistration.jsx
+++ b/src/common-components/InstitutionLogistration.jsx
@@ -60,7 +60,7 @@ const InstitutionLogistration = props => {
className="btn nav-item p-0 mb-1 institutions--provider-link"
destination={lmsBaseUrl + provider.loginUrl}
>
- {provider.name}
+ {provider?.name}
diff --git a/src/common-components/data/constants.js b/src/common-components/data/constants.js
new file mode 100644
index 00000000..4f9ec72c
--- /dev/null
+++ b/src/common-components/data/constants.js
@@ -0,0 +1,79 @@
+export const registerFields = {
+ fields: {
+ country: {
+ name: 'country',
+ error_message: 'Select your country or region of residence',
+ },
+ honor_code: {
+ name: 'honor_code',
+ type: 'tos_and_honor_code',
+ error_message: '',
+ },
+ },
+};
+
+export const progressiveProfilingFields = {
+ extended_profile: [],
+ fields: {
+ level_of_education: {
+ name: 'level_of_education',
+ type: 'select',
+ label: 'Highest level of education completed',
+ error_message: '',
+ options: [
+ [
+ 'p',
+ 'Doctorate',
+ ],
+ [
+ 'm',
+ "Master's or professional degree",
+ ],
+ [
+ 'b',
+ "Bachelor's degree",
+ ],
+ [
+ 'a',
+ 'Associate degree',
+ ],
+ [
+ 'hs',
+ 'Secondary/high school',
+ ],
+ [
+ 'jhs',
+ 'Junior secondary/junior high/middle school',
+ ],
+ [
+ 'none',
+ 'No formal education',
+ ],
+ [
+ 'other',
+ 'Other education',
+ ],
+ ],
+ },
+ gender: {
+ name: 'gender',
+ type: 'select',
+ label: 'Gender',
+ error_message: '',
+ options: [
+ [
+ 'm',
+ 'Male',
+ ],
+ [
+ 'f',
+ 'Female',
+ ],
+ [
+ 'o',
+ 'Other/Prefer Not to Say',
+ ],
+ ],
+ },
+ },
+};
diff --git a/src/common-components/data/sagas.js b/src/common-components/data/sagas.js
index ffe0be37..65105866 100644
--- a/src/common-components/data/sagas.js
+++ b/src/common-components/data/sagas.js
@@ -1,3 +1,4 @@
+import { getConfig } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
@@ -7,6 +8,7 @@ import {
getThirdPartyAuthContextSuccess,
THIRD_PARTY_AUTH_CONTEXT,
} from './actions';
+import { progressiveProfilingFields, registerFields } from './constants';
import {
getThirdPartyAuthContext,
} from './service';
@@ -20,7 +22,16 @@ export function* fetchThirdPartyAuthContext(action) {
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
- yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
+ // hard code country field, level of education and gender fields
+ if (getConfig().ENABLE_HARD_CODE_OPTIONAL_FIELDS) {
+ yield put(getThirdPartyAuthContextSuccess(
+ registerFields,
+ progressiveProfilingFields,
+ thirdPartyAuthContext,
+ ));
+ } else {
+ yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
+ }
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);
diff --git a/src/config/index.js b/src/config/index.js
index 78cea882..6399b549 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -11,6 +11,7 @@ const configuration = {
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
SHOW_REGISTRATION_LINKS: process.env.SHOW_REGISTRATION_LINKS !== 'false',
+ ENABLE_HARD_CODE_OPTIONAL_FIELDS: process.env.ENABLE_HARD_CODE_OPTIONAL_FIELDS || false,
ENABLE_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false,
// Links
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
From 0d603b5fa102e7cdd1a0807836c5f19a64d289f9 Mon Sep 17 00:00:00 2001
From: Muhammad Abdullah Waheed
<42172960+abdullahwaheed@users.noreply.github.com>
Date: Wed, 3 Jul 2024 17:08:44 +0500
Subject: [PATCH 20/82] feat: added app name identifier in segment events
(#1277)
* feat: added app name identifier in registration call
* feat: added utils for tracking events
* refactor: mapped login events
* refactor: mapped forgot password events
* refactor: mapped reset password events
* refactor: mapped register events
* fix: fixed unit tests
* refactor: mapped progressive prifiling events
* fix: fixed unit tests
* refactor: added app name in logistration events
* refactor: resolved PR reviews and fixed tests
---
src/data/constants.js | 1 +
src/data/segment/utils.js | 37 ++++++++++++++++
src/forgot-password/ForgotPasswordPage.jsx | 9 ++--
src/login/LoginPage.jsx | 15 ++++---
src/login/tests/LoginPage.test.jsx | 8 ++--
src/logistration/Logistration.jsx | 10 ++---
src/logistration/Logistration.test.jsx | 5 ++-
.../ProgressiveProfiling.jsx | 44 +++++++++----------
.../tests/ProgressiveProfiling.test.jsx | 10 +++--
src/register/RegistrationPage.jsx | 10 ++---
src/register/RegistrationPage.test.jsx | 11 +++--
.../ConfigurableRegistrationForm.test.jsx | 3 +-
src/reset-password/ResetPasswordPage.jsx | 15 ++++---
.../tests/ResetPasswordPage.test.jsx | 1 +
src/tracking/trackers/forgotpassword.js | 22 ++++++++++
src/tracking/trackers/login.js | 29 ++++++++++++
.../trackers/progressive-profiling.js | 37 ++++++++++++++++
src/tracking/trackers/register.js | 22 ++++++++++
src/tracking/trackers/reset-password.js | 14 ++++++
.../trackers/tests/forgot-password.test.jsx | 37 ++++++++++++++++
src/tracking/trackers/tests/login.test.jsx | 37 ++++++++++++++++
.../tests/progressive-profiling.test.jsx | 37 ++++++++++++++++
src/tracking/trackers/tests/register.test.jsx | 36 +++++++++++++++
.../trackers/tests/reset-password.test.jsx | 26 +++++++++++
24 files changed, 419 insertions(+), 57 deletions(-)
create mode 100644 src/data/segment/utils.js
create mode 100644 src/tracking/trackers/forgotpassword.js
create mode 100644 src/tracking/trackers/login.js
create mode 100644 src/tracking/trackers/progressive-profiling.js
create mode 100644 src/tracking/trackers/register.js
create mode 100644 src/tracking/trackers/reset-password.js
create mode 100644 src/tracking/trackers/tests/forgot-password.test.jsx
create mode 100644 src/tracking/trackers/tests/login.test.jsx
create mode 100644 src/tracking/trackers/tests/progressive-profiling.test.jsx
create mode 100644 src/tracking/trackers/tests/register.test.jsx
create mode 100644 src/tracking/trackers/tests/reset-password.test.jsx
diff --git a/src/data/constants.js b/src/data/constants.js
index 90fdf75f..5adf4382 100644
--- a/src/data/constants.js
+++ b/src/data/constants.js
@@ -37,3 +37,4 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
// things like auto-enrollment upon login and registration.
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta'];
export const REDIRECT = 'redirect';
+export const APP_NAME = 'authn_mfe';
diff --git a/src/data/segment/utils.js b/src/data/segment/utils.js
new file mode 100644
index 00000000..fa84443d
--- /dev/null
+++ b/src/data/segment/utils.js
@@ -0,0 +1,37 @@
+/* eslint-disable import/prefer-default-export */
+import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
+
+import { APP_NAME } from '../constants';
+
+export const LINK_TIMEOUT = 300;
+
+/**
+ * Creates an event tracker function that sends a tracking event with the given name and options.
+ *
+ * @param {string} name - The name of the event to be tracked.
+ * @param {object} [options={}] - Additional options to be included with the event.
+ * @returns {function} - A function that, when called, sends the tracking event.
+ */
+export const createEventTracker = (name, options = {}) => () => sendTrackEvent(
+ name,
+ { ...options, app_name: APP_NAME },
+);
+
+/**
+ * Creates an event tracker function that sends a tracking event with the given name and options.
+ *
+ * @param {string} name - The name of the event to be tracked.
+ * @param {object} [options={}] - Additional options to be included with the event.
+ * @returns {function} - A function that, when called, sends the tracking event.
+ */
+export const createPageEventTracker = (name, options = null) => () => sendPageEvent(
+ name,
+ options,
+ { app_name: APP_NAME },
+);
+
+export const createLinkTracker = (tracker, href) => (e) => {
+ e.preventDefault();
+ tracker();
+ return setTimeout(() => { window.location.href = href; }, LINK_TIMEOUT);
+};
diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/forgot-password/ForgotPasswordPage.jsx
index f4c7b314..fad31300 100644
--- a/src/forgot-password/ForgotPasswordPage.jsx
+++ b/src/forgot-password/ForgotPasswordPage.jsx
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
-import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form,
@@ -25,6 +24,10 @@ import BaseContainer from '../base-container';
import { FormGroup } from '../common-components';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
+import {
+ trackForgotPasswordPageEvent,
+ trackForgotPasswordPageViewed,
+} from '../tracking/trackers/forgotpassword';
const ForgotPasswordPage = (props) => {
const platformName = getConfig().SITE_NAME;
@@ -41,8 +44,8 @@ const ForgotPasswordPage = (props) => {
const navigate = useNavigate();
useEffect(() => {
- sendPageEvent('login_and_registration', 'forgot-password');
- sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
+ trackForgotPasswordPageEvent();
+ trackForgotPasswordPageViewed();
}, []);
useEffect(() => {
diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx
index 62815726..0ff25258 100644
--- a/src/login/LoginPage.jsx
+++ b/src/login/LoginPage.jsx
@@ -2,7 +2,6 @@ import React, { useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
-import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import {
Form, StatefulButton,
@@ -43,6 +42,9 @@ import {
updatePathWithQueryParams,
} from '../data/utils';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
+import {
+ trackForgotPasswordLinkClick, trackLoginPageViewed, trackLoginSuccess,
+} from '../tracking/trackers/login';
const LoginPage = (props) => {
const {
@@ -78,9 +80,15 @@ const LoginPage = (props) => {
const tpaHint = getTpaHint();
useEffect(() => {
- sendPageEvent('login_and_registration', 'login');
+ trackLoginPageViewed();
}, []);
+ useEffect(() => {
+ if (loginResult.success) {
+ trackLoginSuccess();
+ }
+ }, [loginResult]);
+
useEffect(() => {
const payload = { ...queryParams };
if (tpaHint) {
@@ -170,9 +178,6 @@ const LoginPage = (props) => {
const { name } = event.target;
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
};
- const trackForgotPasswordLinkClick = () => {
- sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
- };
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
diff --git a/src/login/tests/LoginPage.test.jsx b/src/login/tests/LoginPage.test.jsx
index 9c337bf2..38778b8c 100644
--- a/src/login/tests/LoginPage.test.jsx
+++ b/src/login/tests/LoginPage.test.jsx
@@ -11,7 +11,9 @@ import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
-import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants';
+import {
+ APP_NAME, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE,
+} from '../../data/constants';
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions';
import { INTERNAL_SERVER_ERROR } from '../data/constants';
import LoginPage from '../LoginPage';
@@ -751,7 +753,7 @@ describe('LoginPage', () => {
it('should send page event when login page is rendered', () => {
render(reduxWrapper());
- expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
+ expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login', { app_name: APP_NAME });
});
it('tests that form is in invalid state when it is submitted', () => {
@@ -784,7 +786,7 @@ describe('LoginPage', () => {
{ selector: '#forgot-password' },
));
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
});
it('should backup the login form state when shouldBackupState is true', () => {
diff --git a/src/logistration/Logistration.jsx b/src/logistration/Logistration.jsx
index 9451aa27..1a65b259 100644
--- a/src/logistration/Logistration.jsx
+++ b/src/logistration/Logistration.jsx
@@ -20,7 +20,7 @@ import {
tpaProvidersSelector,
} from '../common-components/data/selectors';
import messages from '../common-components/messages';
-import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
+import { APP_NAME, LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import {
getTpaHint, getTpaProvider, updatePathWithQueryParams,
} from '../data/utils';
@@ -56,11 +56,11 @@ const Logistration = (props) => {
}, [navigate, disablePublicAccountCreation]);
const handleInstitutionLogin = (e) => {
- sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
+ sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
if (typeof e === 'string') {
- sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register');
+ sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register', { app_name: APP_NAME });
} else {
- sendPageEvent('login_and_registration', e.target.dataset.eventName);
+ sendPageEvent('login_and_registration', e.target.dataset.eventName, { app_name: APP_NAME });
}
setInstitutionLogin(!institutionLogin);
@@ -70,7 +70,7 @@ const Logistration = (props) => {
if (tabKey === currentTab) {
return;
}
- sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
+ sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement', app_name: APP_NAME });
props.clearThirdPartyAuthContextErrorMessage();
if (tabKey === LOGIN_PAGE) {
props.backupRegistrationForm();
diff --git a/src/logistration/Logistration.test.jsx b/src/logistration/Logistration.test.jsx
index b16ae216..b4b0d1e9 100644
--- a/src/logistration/Logistration.test.jsx
+++ b/src/logistration/Logistration.test.jsx
@@ -11,6 +11,7 @@ import configureStore from 'redux-mock-store';
import Logistration from './Logistration';
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import {
+ APP_NAME,
COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE,
} from '../data/constants';
import { backupLoginForm } from '../login/data/actions';
@@ -229,8 +230,8 @@ describe('Logistration', () => {
render(reduxWrapper());
fireEvent.click(screen.getByText('Institution/campus credentials'));
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
- expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
+ expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login', { app_name: APP_NAME });
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
diff --git a/src/progressive-profiling/ProgressiveProfiling.jsx b/src/progressive-profiling/ProgressiveProfiling.jsx
index d42e3d52..55b96462 100644
--- a/src/progressive-profiling/ProgressiveProfiling.jsx
+++ b/src/progressive-profiling/ProgressiveProfiling.jsx
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
-import { identifyAuthenticatedUser, sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
+import { identifyAuthenticatedUser } from '@edx/frontend-platform/analytics';
import {
AxiosJwtAuthService,
configure as configureAuth,
@@ -39,6 +39,13 @@ import {
import isOneTrustFunctionalCookieEnabled from '../data/oneTrust';
import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils';
import { FormFieldRenderer } from '../field-renderer';
+import {
+ trackDisablePostRegistrationRecommendations,
+ trackProgressiveProfilingPageViewed,
+ trackProgressiveProfilingSkipLinkClick,
+ trackProgressiveProfilingSubmitClick,
+ trackProgressiveProfilingSupportLinkCLick,
+} from '../tracking/trackers/progressive-profiling';
const ProgressiveProfiling = (props) => {
const { formatMessage } = useIntl();
@@ -98,14 +105,13 @@ const ProgressiveProfiling = (props) => {
useEffect(() => {
if (authenticatedUser?.userId) {
identifyAuthenticatedUser(authenticatedUser.userId);
- sendPageEvent('login_and_registration', 'welcome');
+ trackProgressiveProfilingPageViewed();
}
}, [authenticatedUser]);
useEffect(() => {
if (!enablePostRegistrationRecommendations) {
- sendTrackEvent(
- 'edx.bi.user.recommendations.not.enabled',
+ trackDisablePostRegistrationRecommendations(
{ functionalCookiesConsent, page: 'authn_recommendations' },
);
return;
@@ -149,29 +155,23 @@ const ProgressiveProfiling = (props) => {
});
}
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
-
- sendTrackEvent(
- 'edx.bi.welcome.page.submit.clicked',
- {
- isGenderSelected: !!values.gender,
- isYearOfBirthSelected: !!values.year_of_birth,
- isLevelOfEducationSelected: !!values.level_of_education,
- isWorkExperienceSelected: !!values.work_experience,
- host: queryParams?.host || '',
- },
- );
+ const eventProperties = {
+ isGenderSelected: !!values.gender,
+ isYearOfBirthSelected: !!values.year_of_birth,
+ isLevelOfEducationSelected: !!values.level_of_education,
+ isWorkExperienceSelected: !!values.work_experience,
+ host: queryParams?.host || '',
+ };
+ trackProgressiveProfilingSubmitClick(eventProperties);
};
const handleSkip = (e) => {
e.preventDefault();
window.history.replaceState(location.state, null, '');
setShowModal(true);
- sendTrackEvent(
- 'edx.bi.welcome.page.skip.link.clicked',
- {
- host: queryParams?.host || '',
- },
- );
+ trackProgressiveProfilingSkipLinkClick({
+ host: queryParams?.host || '',
+ });
};
const onChangeHandler = (e) => {
@@ -242,7 +242,7 @@ const ProgressiveProfiling = (props) => {
destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
target="_blank"
showLaunchIcon={false}
- onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
+ onClick={() => (trackProgressiveProfilingSupportLinkCLick())}
>
{formatMessage(messages['optional.fields.information.link'])}
diff --git a/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx b/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx
index 9bc4439c..c5786b2e 100644
--- a/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx
+++ b/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx
@@ -12,6 +12,7 @@ import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
+ APP_NAME,
AUTHN_PROGRESSIVE_PROFILING,
COMPLETE_STATE, DEFAULT_REDIRECT_URL,
EMBEDDED,
@@ -143,8 +144,9 @@ describe('ProgressiveProfilingTests', () => {
const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container');
expect(modalContentContainer).toBeTruthy();
+ const payload = { host: '', app_name: APP_NAME };
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host: '' });
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', payload);
});
// ******** test event functionality ********
@@ -165,7 +167,7 @@ describe('ProgressiveProfilingTests', () => {
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
fireEvent.click(supportLink);
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked');
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked', { app_name: APP_NAME });
});
it('should set empty host property value for non-embedded experience', () => {
@@ -175,6 +177,7 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false,
isWorkExperienceSelected: false,
host: '',
+ app_name: APP_NAME,
};
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
@@ -316,7 +319,7 @@ describe('ProgressiveProfilingTests', () => {
const skipLinkButton = screen.getByText('Skip for now');
fireEvent.click(skipLinkButton);
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host });
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host, app_name: APP_NAME });
});
it('should show spinner while fetching the optional fields', () => {
@@ -349,6 +352,7 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false,
isWorkExperienceSelected: false,
host: 'http://example.com',
+ app_name: APP_NAME,
};
delete window.location;
window.location = {
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 22f12e14..0a0a5b53 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -4,7 +4,6 @@ import React, {
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
-import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form, Spinner, StatefulButton } from '@openedx/paragon';
import classNames from 'classnames';
@@ -44,11 +43,12 @@ import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../c
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import {
- COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
+ APP_NAME, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
import {
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
} from '../data/utils';
+import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
/**
* Main Registration Page component
@@ -138,7 +138,7 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (!formStartTime) {
- sendPageEvent('login_and_registration', 'register');
+ trackRegistrationPageViewed();
const payload = { ...queryParams, is_register_page: true };
if (tpaHint) {
payload.tpa_hint = tpaHint;
@@ -183,7 +183,7 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (registrationResult.success) {
// This event is used by GTM
- sendTrackEvent('edx.bi.user.account.registered.client', {});
+ trackRegistrationSuccess();
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
@@ -222,7 +222,7 @@ const RegistrationPage = (props) => {
const registerUser = () => {
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
- let payload = { ...formFields };
+ let payload = { ...formFields, app_name: APP_NAME };
if (currentProvider) {
delete payload.password;
diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx
index 9773bf03..5633c753 100644
--- a/src/register/RegistrationPage.test.jsx
+++ b/src/register/RegistrationPage.test.jsx
@@ -22,7 +22,7 @@ import useAutoGeneratedUsernameExperimentVariation
from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
- AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
+ APP_NAME, AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -190,6 +190,7 @@ describe('RegistrationPage', () => {
honor_code: true,
totalRegistrationTime: 0,
next: '/course/demo-course-url',
+ app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -212,6 +213,7 @@ describe('RegistrationPage', () => {
honor_code: true,
social_auth_provider: 'Apple',
totalRegistrationTime: 0,
+ app_name: APP_NAME,
};
store = mockStore({
@@ -297,6 +299,7 @@ describe('RegistrationPage', () => {
honor_code: true,
totalRegistrationTime: 0,
marketing_emails_opt_in: true,
+ app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -323,6 +326,7 @@ describe('RegistrationPage', () => {
country: 'Pakistan',
honor_code: true,
totalRegistrationTime: 0,
+ app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -597,7 +601,7 @@ describe('RegistrationPage', () => {
it('should send page event when register page is rendered', () => {
render(routerWrapper(reduxWrapper()));
- expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
+ expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register', { app_name: APP_NAME });
});
it('should send track event when user has successfully registered', () => {
@@ -615,7 +619,7 @@ describe('RegistrationPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper()));
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {});
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', { app_name: APP_NAME });
});
it('should populate form with pipeline user details', () => {
@@ -888,6 +892,7 @@ describe('RegistrationPage', () => {
country: 'PK',
social_auth_provider: 'Apple',
totalRegistrationTime: 0,
+ app_name: APP_NAME,
}));
});
});
diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
index 93978509..670415f7 100644
--- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
+++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
@@ -9,6 +9,7 @@ import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
+import { APP_NAME } from '../../../data/constants';
import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
@@ -265,7 +266,7 @@ describe('ConfigurableRegistrationForm', () => {
fireEvent.click(submitButton);
- expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
+ expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK', app_name: APP_NAME }));
});
it('should show error messages for required fields on empty form submission', () => {
diff --git a/src/reset-password/ResetPasswordPage.jsx b/src/reset-password/ResetPasswordPage.jsx
index 0393d279..ceb37e8c 100644
--- a/src/reset-password/ResetPasswordPage.jsx
+++ b/src/reset-password/ResetPasswordPage.jsx
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
-import { sendPageEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form,
@@ -19,7 +18,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { resetPassword, validateToken } from './data/actions';
import {
- FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, TOKEN_STATE,
+ FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, SUCCESS, TOKEN_STATE,
} from './data/constants';
import { resetPasswordResultSelector } from './data/selectors';
import { validatePassword } from './data/service';
@@ -31,6 +30,7 @@ import {
LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE,
} from '../data/constants';
import { getAllPossibleQueryParams, updatePathWithQueryParams, windowScrollTo } from '../data/utils';
+import { trackPasswordResetSuccess, trackResetPasswordPageViewed } from '../tracking/trackers/reset-password';
const ResetPasswordPage = (props) => {
const { formatMessage } = useIntl();
@@ -44,8 +44,13 @@ const ResetPasswordPage = (props) => {
const navigate = useNavigate();
useEffect(() => {
- sendPageEvent('login_and_registration', 'reset-password');
- }, []);
+ if (props.status === TOKEN_STATE.VALID) {
+ trackResetPasswordPageViewed();
+ }
+ if (props.status === SUCCESS) {
+ trackPasswordResetSuccess();
+ }
+ }, [props.status]);
useEffect(() => {
if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) {
@@ -144,7 +149,7 @@ const ResetPasswordPage = (props) => {
}
} else if (props.status === PASSWORD_RESET_ERROR) {
navigate(updatePathWithQueryParams(RESET_PAGE));
- } else if (props.status === 'success') {
+ } else if (props.status === SUCCESS) {
navigate(updatePathWithQueryParams(LOGIN_PAGE));
} else {
return (
diff --git a/src/reset-password/tests/ResetPasswordPage.test.jsx b/src/reset-password/tests/ResetPasswordPage.test.jsx
index 0c9ba1a2..f3619be7 100644
--- a/src/reset-password/tests/ResetPasswordPage.test.jsx
+++ b/src/reset-password/tests/ResetPasswordPage.test.jsx
@@ -21,6 +21,7 @@ const token = '1c-bmjdkc-5e60e084cf8113048ca7';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
+ sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
diff --git a/src/tracking/trackers/forgotpassword.js b/src/tracking/trackers/forgotpassword.js
new file mode 100644
index 00000000..3918478d
--- /dev/null
+++ b/src/tracking/trackers/forgotpassword.js
@@ -0,0 +1,22 @@
+import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
+
+export const eventNames = {
+ loginAndRegistration: 'login_and_registration',
+ forgotPasswordPageViewed: 'edx.bi.password_reset_form.viewed',
+};
+
+export const categories = {
+ userEngagement: 'user-engagement',
+};
+
+// Event tracker for forgot password page viewed
+export const trackForgotPasswordPageViewed = () => createEventTracker(
+ eventNames.forgotPasswordPageViewed,
+ {
+ category: categories.userEngagement,
+ },
+)();
+
+export const trackForgotPasswordPageEvent = () => {
+ createPageEventTracker(eventNames.loginAndRegistration, 'forgot-password')();
+};
diff --git a/src/tracking/trackers/login.js b/src/tracking/trackers/login.js
new file mode 100644
index 00000000..3c6a4a2a
--- /dev/null
+++ b/src/tracking/trackers/login.js
@@ -0,0 +1,29 @@
+import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
+
+export const eventNames = {
+ forgotPasswordLinkClicked: 'edx.bi.password-reset_form.toggled',
+ loginAndRegistration: 'login_and_registration',
+ registerFormToggled: 'edx.bi.register_form.toggled',
+ loginSuccess: 'edx.bi.user.account.authenticated.client',
+};
+
+export const categories = {
+ userEngagement: 'user-engagement',
+};
+
+// Event tracker for Forgot Password link click
+export const trackForgotPasswordLinkClick = () => createEventTracker(
+ eventNames.forgotPasswordLinkClicked,
+ { category: categories.userEngagement },
+)();
+
+// Tracks the login page event.
+export const trackLoginPageViewed = () => {
+ createPageEventTracker(eventNames.loginAndRegistration, 'login')();
+};
+
+// Tracks the login sucess event.
+export const trackLoginSuccess = () => createEventTracker(
+ eventNames.loginSuccess,
+ {},
+)();
diff --git a/src/tracking/trackers/progressive-profiling.js b/src/tracking/trackers/progressive-profiling.js
new file mode 100644
index 00000000..11a6f53e
--- /dev/null
+++ b/src/tracking/trackers/progressive-profiling.js
@@ -0,0 +1,37 @@
+import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
+
+export const eventNames = {
+ progressiveProfilingSubmitClick: 'edx.bi.welcome.page.submit.clicked',
+ progressiveProfilingSkipLinkClick: 'edx.bi.welcome.page.skip.link.clicked',
+ disablePostRegistrationRecommendations: 'edx.bi.user.recommendations.not.enabled',
+ progressiveProfilingSupportLinkCLick: 'edx.bi.welcome.page.support.link.clicked',
+ loginAndRegistration: 'login_and_registration',
+};
+
+// Event link tracker for Progressive profiling skip button click
+export const trackProgressiveProfilingSkipLinkClick = evenProperties => createEventTracker(
+ eventNames.progressiveProfilingSkipLinkClick, { ...evenProperties },
+)();
+
+// Event tracker for progressive profiling submit button click
+export const trackProgressiveProfilingSubmitClick = (evenProperties) => createEventTracker(
+ eventNames.progressiveProfilingSubmitClick,
+ { ...evenProperties },
+)();
+
+// Event tracker for progressive profiling submit button click
+export const trackDisablePostRegistrationRecommendations = (evenProperties) => createEventTracker(
+ eventNames.disablePostRegistrationRecommendations,
+ { ...evenProperties },
+)();
+
+// Tracks the progressive profiling page event.
+export const trackProgressiveProfilingPageViewed = () => {
+ createPageEventTracker(eventNames.loginAndRegistration, 'welcome')();
+};
+
+// Tracks the progressive profiling spport link click.
+export const trackProgressiveProfilingSupportLinkCLick = () => createEventTracker(
+ eventNames.progressiveProfilingSupportLinkCLick,
+ {},
+)();
diff --git a/src/tracking/trackers/register.js b/src/tracking/trackers/register.js
new file mode 100644
index 00000000..3a860856
--- /dev/null
+++ b/src/tracking/trackers/register.js
@@ -0,0 +1,22 @@
+import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
+
+export const eventNames = {
+ loginAndRegistration: 'login_and_registration',
+ registrationSuccess: 'edx.bi.user.account.registered.client',
+ loginFormToggled: 'edx.bi.login_form.toggled',
+};
+
+export const categories = {
+ userEngagement: 'user-engagement',
+};
+
+// Event tracker for successful registration
+export const trackRegistrationSuccess = () => createEventTracker(
+ eventNames.registrationSuccess,
+ {},
+)();
+
+// Tracks the progressive profiling page event.
+export const trackRegistrationPageViewed = () => {
+ createPageEventTracker(eventNames.loginAndRegistration, 'register')();
+};
diff --git a/src/tracking/trackers/reset-password.js b/src/tracking/trackers/reset-password.js
new file mode 100644
index 00000000..cb79340f
--- /dev/null
+++ b/src/tracking/trackers/reset-password.js
@@ -0,0 +1,14 @@
+import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
+
+export const eventNames = {
+ loginAndRegistration: 'login_and_registration',
+ resetPasswordSuccess: 'edx.bi.user.password.reset.success',
+};
+
+export const trackResetPasswordPageViewed = () => {
+ createPageEventTracker(eventNames.loginAndRegistration, 'reset-password')();
+};
+
+export const trackPasswordResetSuccess = () => {
+ createEventTracker(eventNames.resetPasswordSuccess, {})();
+};
diff --git a/src/tracking/trackers/tests/forgot-password.test.jsx b/src/tracking/trackers/tests/forgot-password.test.jsx
new file mode 100644
index 00000000..6a9b87c4
--- /dev/null
+++ b/src/tracking/trackers/tests/forgot-password.test.jsx
@@ -0,0 +1,37 @@
+import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
+import {
+ categories,
+ eventNames,
+ trackForgotPasswordPageEvent,
+ trackForgotPasswordPageViewed,
+} from '../forgotpassword';
+
+// Mock createEventTracker function
+jest.mock('../../../data/segment/utils', () => ({
+ createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+}));
+
+describe('Tracking Functions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should fire trackForgotPasswordPageEvent', () => {
+ trackForgotPasswordPageEvent();
+
+ expect(createPageEventTracker).toHaveBeenCalledWith(
+ eventNames.loginAndRegistration,
+ 'forgot-password',
+ );
+ });
+
+ it('should fire forgotPasswordPageViewedEvent', () => {
+ trackForgotPasswordPageViewed();
+
+ expect(createEventTracker).toHaveBeenCalledWith(
+ eventNames.forgotPasswordPageViewed,
+ { category: categories.userEngagement },
+ );
+ });
+});
diff --git a/src/tracking/trackers/tests/login.test.jsx b/src/tracking/trackers/tests/login.test.jsx
new file mode 100644
index 00000000..fac7d082
--- /dev/null
+++ b/src/tracking/trackers/tests/login.test.jsx
@@ -0,0 +1,37 @@
+import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
+import {
+ categories,
+ eventNames,
+ trackForgotPasswordLinkClick,
+ trackLoginPageViewed,
+} from '../login';
+
+// Mock createEventTracker function
+jest.mock('../../../data/segment/utils', () => ({
+ createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+}));
+
+describe('Tracking Functions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('trackForgotPasswordLinkClick function', () => {
+ trackForgotPasswordLinkClick();
+
+ expect(createEventTracker).toHaveBeenCalledWith(
+ eventNames.forgotPasswordLinkClicked,
+ { category: categories.userEngagement },
+ );
+ });
+
+ it('trackLoginPageEvent function', () => {
+ trackLoginPageViewed();
+
+ expect(createPageEventTracker).toHaveBeenCalledWith(
+ eventNames.loginAndRegistration,
+ 'login',
+ );
+ });
+});
diff --git a/src/tracking/trackers/tests/progressive-profiling.test.jsx b/src/tracking/trackers/tests/progressive-profiling.test.jsx
new file mode 100644
index 00000000..4cda593d
--- /dev/null
+++ b/src/tracking/trackers/tests/progressive-profiling.test.jsx
@@ -0,0 +1,37 @@
+import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
+import {
+ eventNames,
+ trackProgressiveProfilingPageViewed,
+ trackProgressiveProfilingSkipLinkClick,
+} from '../progressive-profiling';
+
+// Mock createEventTracker function
+jest.mock('../../../data/segment/utils', () => ({
+ createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createLinkTracker: jest.fn().mockImplementation(() => jest.fn()),
+}));
+
+describe('Tracking Functions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should fire trackProgressiveProfilingSkipLinkClickEvent', () => {
+ trackProgressiveProfilingSkipLinkClick();
+
+ expect(createEventTracker).toHaveBeenCalledWith(
+ eventNames.progressiveProfilingSkipLinkClick,
+ {},
+ );
+ });
+
+ it('should fire trackProgressiveProfilingPageEvent', () => {
+ trackProgressiveProfilingPageViewed();
+
+ expect(createPageEventTracker).toHaveBeenCalledWith(
+ eventNames.loginAndRegistration,
+ 'welcome',
+ );
+ });
+});
diff --git a/src/tracking/trackers/tests/register.test.jsx b/src/tracking/trackers/tests/register.test.jsx
new file mode 100644
index 00000000..d058908f
--- /dev/null
+++ b/src/tracking/trackers/tests/register.test.jsx
@@ -0,0 +1,36 @@
+import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
+import {
+ eventNames,
+ trackRegistrationPageViewed,
+ trackRegistrationSuccess,
+} from '../register';
+
+// Mock createEventTracker function
+jest.mock('../../../data/segment/utils', () => ({
+ createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+}));
+
+describe('Tracking Functions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should fire registrationSuccessEvent', () => {
+ trackRegistrationSuccess();
+
+ expect(createEventTracker).toHaveBeenCalledWith(
+ eventNames.registrationSuccess,
+ {},
+ );
+ });
+
+ it('should fire trackRegistrationPageEvent', () => {
+ trackRegistrationPageViewed();
+
+ expect(createPageEventTracker).toHaveBeenCalledWith(
+ eventNames.loginAndRegistration,
+ 'register',
+ );
+ });
+});
diff --git a/src/tracking/trackers/tests/reset-password.test.jsx b/src/tracking/trackers/tests/reset-password.test.jsx
new file mode 100644
index 00000000..15d6ed79
--- /dev/null
+++ b/src/tracking/trackers/tests/reset-password.test.jsx
@@ -0,0 +1,26 @@
+import { createPageEventTracker } from '../../../data/segment/utils';
+import {
+ eventNames,
+ trackResetPasswordPageViewed,
+} from '../reset-password';
+
+// Mock createEventTracker function
+jest.mock('../../../data/segment/utils', () => ({
+ createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+}));
+
+describe('Tracking Functions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should fire trackResettPasswordPageEvent', () => {
+ trackResetPasswordPageViewed();
+
+ expect(createPageEventTracker).toHaveBeenCalledWith(
+ eventNames.loginAndRegistration,
+ 'reset-password',
+ );
+ });
+});
From 327210192caebfe5ccc9b7917301fc483885c6c9 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Fri, 12 Jul 2024 13:18:42 +0500
Subject: [PATCH 21/82] fix: set marketing opt in in cookie for sso (#1285)
---
src/common-components/SocialAuthProviders.jsx | 8 +++++++-
.../tests/SocialAuthProviders.test.jsx | 7 ++++---
.../components/ConfigurableRegistrationForm.jsx | 15 +++++++++++++++
3 files changed, 26 insertions(+), 4 deletions(-)
diff --git a/src/common-components/SocialAuthProviders.jsx b/src/common-components/SocialAuthProviders.jsx
index abe06da7..f76e4968 100644
--- a/src/common-components/SocialAuthProviders.jsx
+++ b/src/common-components/SocialAuthProviders.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -8,15 +9,20 @@ import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';
-import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
+import { LOGIN_PAGE, REGISTER_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
+import { setCookie } from '../data/utils';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
const { referrer, socialAuthProviders } = props;
+ const registrationFields = useSelector(state => state.register.registrationFormData);
function handleSubmit(e) {
e.preventDefault();
+ if (referrer === REGISTER_PAGE) {
+ setCookie('marketingEmailsOptIn', registrationFields?.configurableFormFields?.marketingEmailsOptIn);
+ }
const url = e.currentTarget.dataset.providerUrl;
window.location.href = getConfig().LMS_BASE_URL + url;
}
diff --git a/src/common-components/tests/SocialAuthProviders.test.jsx b/src/common-components/tests/SocialAuthProviders.test.jsx
index 850708ec..ccc96157 100644
--- a/src/common-components/tests/SocialAuthProviders.test.jsx
+++ b/src/common-components/tests/SocialAuthProviders.test.jsx
@@ -27,7 +27,8 @@ describe('SocialAuthProviders', () => {
loginUrl: '/auth/login/facebook/?auth_entry=login&next=/dashboard',
};
- it('should match social auth provider with iconImage snapshot', () => {
+ // Skipped tests will be fixed later.
+ it.skip('should match social auth provider with iconImage snapshot', () => {
props = { socialAuthProviders: [appleProvider, facebookProvider] };
const tree = renderer.create(
@@ -39,7 +40,7 @@ describe('SocialAuthProviders', () => {
expect(tree).toMatchSnapshot();
});
- it('should match social auth provider with iconClass snapshot', () => {
+ it.skip('should match social auth provider with iconClass snapshot', () => {
props = {
socialAuthProviders: [{
...appleProvider,
@@ -57,7 +58,7 @@ describe('SocialAuthProviders', () => {
expect(tree).toMatchSnapshot();
});
- it('should match social auth provider with default icon snapshot', () => {
+ it.skip('should match social auth provider with default icon snapshot', () => {
props = {
socialAuthProviders: [{
...appleProvider,
diff --git a/src/register/components/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx
index 8c300b7e..971bdd4a 100644
--- a/src/register/components/ConfigurableRegistrationForm.jsx
+++ b/src/register/components/ConfigurableRegistrationForm.jsx
@@ -1,10 +1,12 @@
import React, { useEffect, useMemo } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { FormFieldRenderer } from '../../field-renderer';
+import { backupRegistrationFormBegin } from '../data/actions';
import { FIELDS } from '../data/constants';
import messages from '../messages';
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
@@ -32,6 +34,7 @@ const ConfigurableRegistrationForm = (props) => {
setFormFields,
autoSubmitRegistrationForm,
} = props;
+ const dispatch = useDispatch();
/** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
States' instead of 'United States of America' which does not exist in country dropdown list and gets the user
@@ -50,6 +53,8 @@ const ConfigurableRegistrationForm = (props) => {
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
};
+ const backedUpFormData = useSelector(state => state.register.registrationFormData);
+
/**
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
*/
@@ -90,6 +95,16 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
}
}
+ // setting marketingEmailsOptIn state for SSO authentication flow for register API call
+ if (name === 'marketingEmailsOptIn') {
+ dispatch(backupRegistrationFormBegin({
+ ...backedUpFormData,
+ configurableFormFields: {
+ ...backedUpFormData.configurableFormFields,
+ [name]: value,
+ },
+ }));
+ }
setFormFields(prevState => ({ ...prevState, [name]: value }));
};
From 6b983e18d3ee4a4c550d0f9ecee81347c3808067 Mon Sep 17 00:00:00 2001
From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com>
Date: Fri, 12 Jul 2024 17:05:50 +0500
Subject: [PATCH 22/82] fix: remove cookie (#1286)
-remove marketingEmailsOptIn cookie on successful registration
- fix tests
---
.../RedirectLogistration.jsx | 2 +-
.../tests/SocialAuthProviders.test.jsx | 38 ++++++++++++++-----
src/data/utils/cookies.js | 8 ++++
src/data/utils/index.js | 2 +-
src/register/RegistrationPage.jsx | 5 ++-
5 files changed, 42 insertions(+), 13 deletions(-)
diff --git a/src/common-components/RedirectLogistration.jsx b/src/common-components/RedirectLogistration.jsx
index f6c30b50..24603f6a 100644
--- a/src/common-components/RedirectLogistration.jsx
+++ b/src/common-components/RedirectLogistration.jsx
@@ -5,7 +5,7 @@ import { Navigate } from 'react-router-dom';
import {
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
} from '../data/constants';
-import { setCookie } from '../data/utils';
+import setCookie from '../data/utils/cookies';
const RedirectLogistration = (props) => {
const {
diff --git a/src/common-components/tests/SocialAuthProviders.test.jsx b/src/common-components/tests/SocialAuthProviders.test.jsx
index ccc96157..b1066a19 100644
--- a/src/common-components/tests/SocialAuthProviders.test.jsx
+++ b/src/common-components/tests/SocialAuthProviders.test.jsx
@@ -1,16 +1,35 @@
import React from 'react';
+import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
+import configureStore from 'redux-mock-store';
import registerIcons from '../RegisterFaIcons';
import SocialAuthProviders from '../SocialAuthProviders';
registerIcons();
+const mockStore = configureStore();
describe('SocialAuthProviders', () => {
let props = {};
+ const initialState = {
+ register: {
+ registrationFormData: {
+ configurableFormFields: {
+ marketingEmailsOptIn: true,
+ },
+ },
+ },
+ };
+ const store = mockStore(initialState);
+ const reduxWrapper = children => (
+
+ {children}
+
+ );
+
const appleProvider = {
id: 'oa2-apple-id',
name: 'Apple',
@@ -27,20 +46,19 @@ describe('SocialAuthProviders', () => {
loginUrl: '/auth/login/facebook/?auth_entry=login&next=/dashboard',
};
- // Skipped tests will be fixed later.
- it.skip('should match social auth provider with iconImage snapshot', () => {
+ it('should match social auth provider with iconImage snapshot', () => {
props = { socialAuthProviders: [appleProvider, facebookProvider] };
- const tree = renderer.create(
+ const tree = renderer.create(reduxWrapper(
,
- ).toJSON();
+ )).toJSON();
expect(tree).toMatchSnapshot();
});
- it.skip('should match social auth provider with iconClass snapshot', () => {
+ it('should match social auth provider with iconClass snapshot', () => {
props = {
socialAuthProviders: [{
...appleProvider,
@@ -49,16 +67,16 @@ describe('SocialAuthProviders', () => {
}],
};
- const tree = renderer.create(
+ const tree = renderer.create(reduxWrapper(
,
- ).toJSON();
+ )).toJSON();
expect(tree).toMatchSnapshot();
});
- it.skip('should match social auth provider with default icon snapshot', () => {
+ it('should match social auth provider with default icon snapshot', () => {
props = {
socialAuthProviders: [{
...appleProvider,
@@ -67,11 +85,11 @@ describe('SocialAuthProviders', () => {
}],
};
- const tree = renderer.create(
+ const tree = renderer.create(reduxWrapper(
,
- ).toJSON();
+ )).toJSON();
expect(tree).toMatchSnapshot();
});
diff --git a/src/data/utils/cookies.js b/src/data/utils/cookies.js
index 1aad2858..cfddf5ec 100644
--- a/src/data/utils/cookies.js
+++ b/src/data/utils/cookies.js
@@ -11,3 +11,11 @@ export default function setCookie(cookieName, cookieValue, cookieExpiry) {
cookies.set(cookieName, cookieValue, options);
}
}
+
+export function removeCookie(cookieName) {
+ if (cookieName) {
+ const cookies = new Cookies();
+ const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
+ cookies.remove(cookieName, options);
+ }
+}
diff --git a/src/data/utils/index.js b/src/data/utils/index.js
index 05532111..1c9eebbb 100644
--- a/src/data/utils/index.js
+++ b/src/data/utils/index.js
@@ -8,4 +8,4 @@ export {
windowScrollTo,
} from './dataUtils';
export { default as AsyncActionType } from './reduxUtils';
-export { default as setCookie } from './cookies';
+export { default as setCookie, removeCookie } from './cookies';
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 0a0a5b53..50f689bf 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -46,7 +46,7 @@ import {
APP_NAME, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
import {
- getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
+ getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, removeCookie, setCookie,
} from '../data/utils';
import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
@@ -187,6 +187,9 @@ const RegistrationPage = (props) => {
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
+
+ // remove marketingEmailsOptIn cookie that was set on SSO registration flow
+ removeCookie('marketingEmailsOptIn');
}
}, [registrationResult]);
From 9a30f053c7f92f797e73c6ab41cb44ebc2e4a632 Mon Sep 17 00:00:00 2001
From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com>
Date: Fri, 26 Jul 2024 15:57:18 +0500
Subject: [PATCH 23/82] fix: fix marketingEmailsOptIn null value (#1294)
Fix marketingEmailsOptIn null value issue for SSO flow on onboarding component
VAN-2013
---
src/common-components/ThirdPartyAuthAlert.jsx | 6 +++++-
src/login/LoginPage.jsx | 4 ++++
src/register/RegistrationPage.jsx | 4 +++-
3 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/src/common-components/ThirdPartyAuthAlert.jsx b/src/common-components/ThirdPartyAuthAlert.jsx
index fb79a6a6..fa2c6ee6 100644
--- a/src/common-components/ThirdPartyAuthAlert.jsx
+++ b/src/common-components/ThirdPartyAuthAlert.jsx
@@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
+import setCookie from '../data/utils/cookies';
const ThirdPartyAuthAlert = (props) => {
const { formatMessage } = useIntl();
@@ -20,7 +21,10 @@ const ThirdPartyAuthAlert = (props) => {
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
}
- if (!currentProvider) {
+ if (currentProvider) {
+ // Setting this cookie to capture marketingEmailsOptIn for SSO flow on the onboarding component
+ setCookie('ssoPipelineRedirectionDone', true);
+ } else {
return null;
}
diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx
index 0ff25258..6a935935 100644
--- a/src/login/LoginPage.jsx
+++ b/src/login/LoginPage.jsx
@@ -41,6 +41,7 @@ import {
getTpaProvider,
updatePathWithQueryParams,
} from '../data/utils';
+import { removeCookie } from '../data/utils/cookies';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
import {
trackForgotPasswordLinkClick, trackLoginPageViewed, trackLoginSuccess,
@@ -86,6 +87,9 @@ const LoginPage = (props) => {
useEffect(() => {
if (loginResult.success) {
trackLoginSuccess();
+
+ // Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
+ removeCookie('ssoPipelineRedirectionDone');
}
}, [loginResult]);
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 50f689bf..8b6dc9fd 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -188,8 +188,10 @@ const RegistrationPage = (props) => {
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
- // remove marketingEmailsOptIn cookie that was set on SSO registration flow
+ // Remove marketingEmailsOptIn cookie that was set on SSO registration flow
removeCookie('marketingEmailsOptIn');
+ // Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
+ removeCookie('ssoPipelineRedirectionDone');
}
}, [registrationResult]);
From 4755540be8f345edfd6a22859860c54b820699ab Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Mon, 29 Jul 2024 11:23:48 +0500
Subject: [PATCH 24/82] fix: retain query params in authenticated user
redirection (#1288)
---
src/common-components/UnAuthOnlyRoute.jsx | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/common-components/UnAuthOnlyRoute.jsx b/src/common-components/UnAuthOnlyRoute.jsx
index 7be62aae..3e05fe0b 100644
--- a/src/common-components/UnAuthOnlyRoute.jsx
+++ b/src/common-components/UnAuthOnlyRoute.jsx
@@ -4,9 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import PropTypes from 'prop-types';
-import {
- DEFAULT_REDIRECT_URL,
-} from '../data/constants';
+import { updatePathWithQueryParams } from '../data/utils';
/**
* This wrapper redirects the requester to our default redirect url if they are
@@ -25,7 +23,8 @@ const UnAuthOnlyRoute = ({ children }) => {
if (isReady) {
if (authUser && authUser.username) {
- global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
+ const updatedPath = updatePathWithQueryParams(window.location.pathname);
+ global.location.href = getConfig().LMS_BASE_URL.concat(updatedPath);
return null;
}
From 099fe8d717e7d24f5b7420a94d32ee4c9cab3312 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Thu, 1 Aug 2024 16:06:20 +0500
Subject: [PATCH 25/82] fix: fix datadog js errors (#1296)
---
src/recommendations/track.js | 4 ++--
src/register/RegistrationFields/NameField/validator.js | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/recommendations/track.js b/src/recommendations/track.js
index 5d826a3e..9a18b974 100644
--- a/src/recommendations/track.js
+++ b/src/recommendations/track.js
@@ -15,7 +15,7 @@ const generateProductKey = (product) => (
export const getProductMapping = (recommendedProducts) => recommendedProducts.map((product) => ({
product_key: generateProductKey(product),
product_line: product.cardType,
- product_source: product.productSource.name,
+ product_source: product?.productSource?.name,
}));
export const trackRecommendationClick = (product, position, userId) => {
@@ -25,7 +25,7 @@ export const trackRecommendationClick = (product, position, userId) => {
recommendation_type: product.recommendationType,
product_key: generateProductKey(product),
product_line: product.cardType,
- product_source: product.productSource.name,
+ product_source: product?.productSource?.name,
user_id: userId,
});
diff --git a/src/register/RegistrationFields/NameField/validator.js b/src/register/RegistrationFields/NameField/validator.js
index aefaedfb..c8e1d09c 100644
--- a/src/register/RegistrationFields/NameField/validator.js
+++ b/src/register/RegistrationFields/NameField/validator.js
@@ -11,7 +11,7 @@ export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g;
const validateName = (value, formatMessage) => {
let fieldError = '';
- if (!value.trim()) {
+ if (!value || (value && !value.trim())) {
fieldError = formatMessage(messages['empty.name.field.error']);
} else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) {
fieldError = formatMessage(messages['name.validation.message']);
From 2428b4c38958ee76572d07aefdfc7d122a0116f9 Mon Sep 17 00:00:00 2001
From: Blue
Date: Wed, 28 Aug 2024 14:58:40 +0500
Subject: [PATCH 26/82] fix: covert totalRegistrationTime to snake case (#1302)
Description:
Convert totalRegistrationTime to snake case
VAN-1816
Co-authored-by: Ahtesham Quraish
---
src/register/RegistrationPage.test.jsx | 14 +++++++-------
.../tests/ConfigurableRegistrationForm.test.jsx | 4 ++--
src/register/data/utils.js | 2 +-
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx
index 5633c753..5f23e2c2 100644
--- a/src/register/RegistrationPage.test.jsx
+++ b/src/register/RegistrationPage.test.jsx
@@ -188,7 +188,7 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Pakistan',
honor_code: true,
- totalRegistrationTime: 0,
+ total_registration_time: 0,
next: '/course/demo-course-url',
app_name: APP_NAME,
};
@@ -212,7 +212,7 @@ describe('RegistrationPage', () => {
country: 'Pakistan',
honor_code: true,
social_auth_provider: 'Apple',
- totalRegistrationTime: 0,
+ total_registration_time: 0,
app_name: APP_NAME,
};
@@ -246,7 +246,7 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Ukraine',
honor_code: true,
- totalRegistrationTime: 0,
+ total_registration_time: 0,
};
store.dispatch = jest.fn(store.dispatch);
@@ -271,7 +271,7 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Ukraine',
honor_code: true,
- totalRegistrationTime: 0,
+ total_registration_time: 0,
};
store.dispatch = jest.fn(store.dispatch);
@@ -297,7 +297,7 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Pakistan',
honor_code: true,
- totalRegistrationTime: 0,
+ total_registration_time: 0,
marketing_emails_opt_in: true,
app_name: APP_NAME,
};
@@ -325,7 +325,7 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Pakistan',
honor_code: true,
- totalRegistrationTime: 0,
+ total_registration_time: 0,
app_name: APP_NAME,
};
@@ -891,7 +891,7 @@ describe('RegistrationPage', () => {
email: 'john.doe@example.com',
country: 'PK',
social_auth_provider: 'Apple',
- totalRegistrationTime: 0,
+ total_registration_time: 0,
app_name: APP_NAME,
}));
});
diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
index 670415f7..61a25848 100644
--- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
+++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
@@ -251,7 +251,7 @@ describe('ConfigurableRegistrationForm', () => {
country: 'Pakistan',
honor_code: true,
profession: 'Engineer',
- totalRegistrationTime: 0,
+ total_registration_time: 0,
};
store.dispatch = jest.fn(store.dispatch);
@@ -362,7 +362,7 @@ describe('ConfigurableRegistrationForm', () => {
password: 'password1',
country: 'Ukraine',
honor_code: true,
- totalRegistrationTime: 0,
+ total_registration_time: 0,
};
store = mockStore({
diff --git a/src/register/data/utils.js b/src/register/data/utils.js
index ee08f0b6..e7773e2b 100644
--- a/src/register/data/utils.js
+++ b/src/register/data/utils.js
@@ -126,8 +126,8 @@ export const prepareRegistrationPayload = (
delete payload.marketingEmailsOptIn;
}
- payload = snakeCaseObject(payload);
payload.totalRegistrationTime = totalRegistrationTime;
+ payload = snakeCaseObject(payload);
// add query params to the payload
payload = { ...payload, ...queryParams };
From ecf4c3ae53e92436525aba1d0a237a5b327ee27e Mon Sep 17 00:00:00 2001
From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com>
Date: Thu, 29 Aug 2024 09:48:21 +0500
Subject: [PATCH 27/82] fix: password reset redirection (#1300)
fix authenticated user redirects to 404 if token is invalide for password reset
VAN-2052
---
src/common-components/UnAuthOnlyRoute.jsx | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/common-components/UnAuthOnlyRoute.jsx b/src/common-components/UnAuthOnlyRoute.jsx
index 3e05fe0b..2e9df557 100644
--- a/src/common-components/UnAuthOnlyRoute.jsx
+++ b/src/common-components/UnAuthOnlyRoute.jsx
@@ -4,6 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import PropTypes from 'prop-types';
+import { RESET_PAGE } from '../data/constants';
import { updatePathWithQueryParams } from '../data/utils';
/**
@@ -24,6 +25,10 @@ const UnAuthOnlyRoute = ({ children }) => {
if (isReady) {
if (authUser && authUser.username) {
const updatedPath = updatePathWithQueryParams(window.location.pathname);
+ if (updatedPath.startsWith(RESET_PAGE)) {
+ global.location.href = getConfig().LMS_BASE_URL;
+ return null;
+ }
global.location.href = getConfig().LMS_BASE_URL.concat(updatedPath);
return null;
}
From c1e63da7787a717873b65baaa4b1c238c5436eae Mon Sep 17 00:00:00 2001
From: Awais Ansari <79941147+awais-ansari@users.noreply.github.com>
Date: Tue, 10 Sep 2024 21:11:44 +0500
Subject: [PATCH 28/82] feat: removed Russian Federation from country list
(#1315)
---
src/register/components/ConfigurableRegistrationForm.jsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/register/components/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx
index 971bdd4a..3013be5f 100644
--- a/src/register/components/ConfigurableRegistrationForm.jsx
+++ b/src/register/components/ConfigurableRegistrationForm.jsx
@@ -40,7 +40,10 @@ const ConfigurableRegistrationForm = (props) => {
States' instead of 'United States of America' which does not exist in country dropdown list and gets the user
confused and unable to create an account. So we added the United States entry in the dropdown list.
*/
- const countryList = useMemo(() => getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []);
+
+ const countryList = useMemo(() => (
+ getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]).filter(country => country.code !== 'RU')
+ ), []);
let showTermsOfServiceAndHonorCode = false;
let showCountryField = false;
From e2cdfce832bcfc04192928445620274370f2018e Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Wed, 17 Apr 2024 17:00:28 +0500
Subject: [PATCH 29/82] Rebase 2u main with master (#1228)
* chore(deps): update dependency babel-plugin-formatjs to v10.5.14
* fix(deps): update dependency @edx/frontend-platform to v7.1.3
* fix(deps): update font awesome to v6.5.2
* chore(deps): update dependency @openedx/frontend-build to v13.1.4
* fix(deps): update dependency @openedx/paragon to v22.2.1
* fix(deps): update dependency algoliasearch to v4.23.3
* fix(deps): update dependency algoliasearch-helper to v3.17.0
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
From d66afe98f032952abceecf876827ce37f4241792 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Wed, 3 Apr 2024 11:28:39 +0500
Subject: [PATCH 30/82] feat: implement multi step registration experiment
---
src/common-components/messages.jsx | 6 +
src/config/index.js | 2 +
src/logistration/Logistration.jsx | 55 +++++++--
src/logistration/Logistration.test.jsx | 7 ++
src/register/RegistrationPage.jsx | 2 +-
src/register/RegistrationPage.test.jsx | 5 +
.../ConfigurableRegistrationForm.jsx | 24 +++-
.../components/RegistrationFailure.jsx | 11 +-
.../ConfigurableRegistrationForm.test.jsx | 8 ++
.../tests/RegistrationFailure.test.jsx | 5 +
.../components/tests/ThirdPartyAuth.test.jsx | 5 +
src/register/data/actions.js | 20 +++-
.../data/optimizelyExperiment/helper.js | 105 ++++++++++++++++++
...ltiStepRegistrationExperimentVariation.jsx | 54 +++++++++
src/register/data/reducers.js | 25 +++++
src/register/data/sagas.js | 7 +-
src/register/data/tests/reducers.test.js | 5 +
src/register/messages.jsx | 23 ++++
src/sass/_registration.scss | 4 -
19 files changed, 346 insertions(+), 27 deletions(-)
create mode 100644 src/register/data/optimizelyExperiment/helper.js
create mode 100644 src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
diff --git a/src/common-components/messages.jsx b/src/common-components/messages.jsx
index 08e88b8f..6f0e69ca 100644
--- a/src/common-components/messages.jsx
+++ b/src/common-components/messages.jsx
@@ -132,6 +132,12 @@ const messages = defineMessages({
defaultMessage: 'Company or school credentials',
description: 'Company or school login link text.',
},
+ // multi step registration experiment messages
+ 'tab.back.btn.text': {
+ id: 'tab.back.btn.text',
+ defaultMessage: 'Back',
+ description: 'Tab back button text',
+ },
});
export default messages;
diff --git a/src/config/index.js b/src/config/index.js
index badb6fe7..781db13c 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -35,6 +35,8 @@ const configuration = {
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
+ // Multi Step Registration Experiment
+ MULTI_STEP_REGISTRATION_EXPERIMENT_ID: process.env.MULTI_STEP_REGISTRATION_EXPERIMENT_ID || '',
};
export default configuration;
diff --git a/src/logistration/Logistration.jsx b/src/logistration/Logistration.jsx
index 9451aa27..5199e1b3 100644
--- a/src/logistration/Logistration.jsx
+++ b/src/logistration/Logistration.jsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import { connect } from 'react-redux';
+import { connect, useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -7,10 +7,11 @@ import { getAuthService } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Icon,
+ IconButton,
Tab,
Tabs,
} from '@openedx/paragon';
-import { ChevronLeft } from '@openedx/paragon/icons';
+import { ArrowBackIos, ChevronLeft } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { Navigate, useNavigate } from 'react-router-dom';
@@ -27,7 +28,11 @@ import {
import { LoginPage } from '../login';
import { backupLoginForm } from '../login/data/actions';
import { RegistrationPage } from '../register';
-import { backupRegistrationForm } from '../register/data/actions';
+import { backupRegistrationForm, setMultiStepRegistrationExpData } from '../register/data/actions';
+import {
+ FIRST_STEP,
+ getMultiStepRegistrationPreviousStep,
+} from '../register/data/optimizelyExperiment/helper';
const Logistration = (props) => {
const { selectedPage, tpaProviders } = props;
@@ -42,6 +47,10 @@ const Logistration = (props) => {
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
const hideRegistrationLink = getConfig().SHOW_REGISTRATION_LINKS === false;
+ const dispatch = useDispatch();
+ const multiStepRegExpVariation = useSelector(state => state.register.multiStepRegExpVariation);
+ const multiStepRegistrationPageStep = useSelector(state => state.register.multiStepRegistrationPageStep);
+
useEffect(() => {
const authService = getAuthService();
if (authService) {
@@ -91,6 +100,39 @@ const Logistration = (props) => {
);
+ /**
+ * Temporary function created to resolve the complexity in tabs conditioning for multi-step
+ * registration experiment
+ */
+ const getTabs = () => {
+ if (multiStepRegistrationPageStep !== FIRST_STEP) {
+ const prevStep = getMultiStepRegistrationPreviousStep(multiStepRegistrationPageStep);
+ return (
+
+ {
+ dispatch(setMultiStepRegistrationExpData(multiStepRegExpVariation, prevStep));
+ }}
+ variant="primary"
+ size="inline"
+ className="mr-1"
+ />
+ {formatMessage(messages['tab.back.btn.text'])}
+
+ );
+ }
+ return (
+ handleOnSelect(tabKey, selectedPage)}>
+
+
+
+ );
+ };
+
const isValidTpaHint = () => {
const { provider } = getTpaProvider(tpaHint, providers, secondaryProviders);
return !!provider;
@@ -123,12 +165,7 @@ const Logistration = (props) => {
)
- : (!isValidTpaHint() && !hideRegistrationLink && (
- handleOnSelect(tabKey, selectedPage)}>
-
-
-
- ))}
+ : (!isValidTpaHint() && !hideRegistrationLink && getTabs())}
{ key && (
)}
diff --git a/src/logistration/Logistration.test.jsx b/src/logistration/Logistration.test.jsx
index 87bf3e70..ce7af88f 100644
--- a/src/logistration/Logistration.test.jsx
+++ b/src/logistration/Logistration.test.jsx
@@ -15,12 +15,16 @@ import {
} from '../data/constants';
import { backupLoginForm } from '../login/data/actions';
import { backupRegistrationForm } from '../register/data/actions';
+import { FIRST_STEP, NOT_INITIALIZED } from '../register/data/optimizelyExperiment/helper';
+import useMultiStepRegistrationExperimentVariation
+ from '../register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
+jest.mock('../register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
@@ -63,6 +67,8 @@ describe('Logistration', () => {
registrationError: {},
usernameSuggestions: [],
validationApiRateLimited: false,
+ multiStepRegExpVariation: '',
+ multiStepRegistrationPageStep: FIRST_STEP,
},
commonComponents: {
thirdPartyAuthContext: {
@@ -83,6 +89,7 @@ describe('Logistration', () => {
username: 'test-user',
})),
}));
+ useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({
loggingService: { logError: jest.fn() },
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 8c5ea79f..145c5a38 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -176,7 +176,7 @@ const RegistrationPage = (props) => {
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
}
- }, [registrationResult]);
+ }, [registrationResult]); // eslint-disable-line react-hooks/exhaustive-deps
const handleOnChange = (event) => {
const { name } = event.target;
diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx
index 18e4e513..59a4d8aa 100644
--- a/src/register/RegistrationPage.test.jsx
+++ b/src/register/RegistrationPage.test.jsx
@@ -17,6 +17,9 @@ import {
setUserPipelineDataLoaded,
} from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants';
+import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
+import useMultiStepRegistrationExperimentVariation
+ from './data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
@@ -30,6 +33,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('./data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -128,6 +132,7 @@ describe('RegistrationPage', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx
index 8c300b7e..ce0fbea5 100644
--- a/src/register/components/ConfigurableRegistrationForm.jsx
+++ b/src/register/components/ConfigurableRegistrationForm.jsx
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import { FormFieldRenderer } from '../../field-renderer';
import { FIELDS } from '../data/constants';
+import { FIRST_STEP, shouldDisplayFieldInExperiment } from '../data/optimizelyExperiment/helper';
import messages from '../messages';
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
@@ -31,6 +32,8 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors,
setFormFields,
autoSubmitRegistrationForm,
+ multiStepRegistrationExpVariation,
+ multiStepRegistrationPageStep,
} = props;
/** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
@@ -109,7 +112,9 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
};
- if (flags.showConfigurableRegistrationFields) {
+ if (flags.showConfigurableRegistrationFields && shouldDisplayFieldInExperiment(
+ 'other_fields', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
+ )) {
Object.keys(fieldDescriptions).forEach(fieldName => {
const fieldData = fieldDescriptions[fieldName];
switch (fieldData.name) {
@@ -161,7 +166,9 @@ const ConfigurableRegistrationForm = (props) => {
});
}
- if (flags.showConfigurableEdxFields || showCountryField) {
+ if ((flags.showConfigurableEdxFields || showCountryField) && shouldDisplayFieldInExperiment(
+ 'country', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
+ )) {
formFieldDescriptions.push(
{
);
}
- if (flags.showMarketingEmailOptInCheckbox) {
+ if (flags.showMarketingEmailOptInCheckbox && shouldDisplayFieldInExperiment(
+ 'marketing_email_opt_in', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
+ )) {
formFieldDescriptions.push(
{
);
}
- if (flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode) {
+ if ((flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode)
+ && shouldDisplayFieldInExperiment(
+ 'honor_code', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
+ )) {
formFieldDescriptions.push(
@@ -231,11 +243,15 @@ ConfigurableRegistrationForm.propTypes = {
setFieldErrors: PropTypes.func.isRequired,
setFormFields: PropTypes.func.isRequired,
autoSubmitRegistrationForm: PropTypes.bool,
+ multiStepRegistrationExpVariation: PropTypes.string,
+ multiStepRegistrationPageStep: PropTypes.string,
};
ConfigurableRegistrationForm.defaultProps = {
fieldDescriptions: {},
autoSubmitRegistrationForm: false,
+ multiStepRegistrationExpVariation: '',
+ multiStepRegistrationPageStep: FIRST_STEP,
};
export default ConfigurableRegistrationForm;
diff --git a/src/register/components/RegistrationFailure.jsx b/src/register/components/RegistrationFailure.jsx
index c34c2af7..f2a83585 100644
--- a/src/register/components/RegistrationFailure.jsx
+++ b/src/register/components/RegistrationFailure.jsx
@@ -13,12 +13,13 @@ import {
TPA_AUTHENTICATION_FAILURE,
TPA_SESSION_EXPIRED,
} from '../data/constants';
+import { FIRST_STEP } from '../data/optimizelyExperiment/helper';
import messages from '../messages';
const RegistrationFailureMessage = (props) => {
const { formatMessage } = useIntl();
const {
- context, errorCode, failureCount,
+ context, errorCode, failureCount, multiStepRegistrationPageStep,
} = props;
useEffect(() => {
@@ -49,7 +50,11 @@ const RegistrationFailureMessage = (props) => {
errorMessage = formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider });
break;
default:
- errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
+ if (multiStepRegistrationPageStep !== FIRST_STEP) {
+ errorMessage = formatMessage(messages['multistep.registration.form.submission.error']);
+ } else {
+ errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
+ }
break;
}
@@ -65,6 +70,7 @@ RegistrationFailureMessage.defaultProps = {
context: {
errorMessage: null,
},
+ multiStepRegistrationPageStep: FIRST_STEP,
};
RegistrationFailureMessage.propTypes = {
@@ -74,6 +80,7 @@ RegistrationFailureMessage.propTypes = {
}),
errorCode: PropTypes.string.isRequired,
failureCount: PropTypes.number.isRequired,
+ multiStepRegistrationPageStep: PropTypes.string,
};
export default RegistrationFailureMessage;
diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
index 3d664260..00fe5595 100644
--- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
+++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
@@ -11,6 +11,9 @@ import configureStore from 'redux-mock-store';
import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants';
+import { FIRST_STEP, NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useMultiStepRegistrationExperimentVariation
+ from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
@@ -22,6 +25,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
@@ -93,6 +97,9 @@ describe('ConfigurableRegistrationForm', () => {
registrationError: {},
registrationFormData,
usernameSuggestions: [],
+ multiStepRegistrationPageStep: FIRST_STEP,
+ multiStepRegExpVariation: '',
+ isValidatingMultiStepRegistrationPage: false,
},
commonComponents: {
thirdPartyAuthApiStatus: null,
@@ -121,6 +128,7 @@ describe('ConfigurableRegistrationForm', () => {
};
window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us'));
+ useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/RegistrationFailure.test.jsx b/src/register/components/tests/RegistrationFailure.test.jsx
index 003cc966..b9667c9c 100644
--- a/src/register/components/tests/RegistrationFailure.test.jsx
+++ b/src/register/components/tests/RegistrationFailure.test.jsx
@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
} from '../../data/constants';
+import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useMultiStepRegistrationExperimentVariation
+ from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import RegistrationFailureMessage from '../RegistrationFailure';
@@ -23,6 +26,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
@@ -121,6 +125,7 @@ describe('RegistrationFailure', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/ThirdPartyAuth.test.jsx b/src/register/components/tests/ThirdPartyAuth.test.jsx
index 917f10f9..45672ad5 100644
--- a/src/register/components/tests/ThirdPartyAuth.test.jsx
+++ b/src/register/components/tests/ThirdPartyAuth.test.jsx
@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants';
+import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useMultiStepRegistrationExperimentVariation
+ from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -22,6 +25,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -120,6 +124,7 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/data/actions.js b/src/register/data/actions.js
index 9fa5aed5..530f3bc8 100644
--- a/src/register/data/actions.js
+++ b/src/register/data/actions.js
@@ -8,6 +8,7 @@ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERRO
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
+export const REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA = 'REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA';
// Backup registration form
export const backupRegistrationForm = () => ({
@@ -20,18 +21,19 @@ export const backupRegistrationFormBegin = (data) => ({
});
// Validate fields from the backend
-export const fetchRealtimeValidations = (formPayload) => ({
+export const fetchRealtimeValidations = (formPayload, isValidatingMultiStepRegistrationPage) => ({
type: REGISTER_FORM_VALIDATIONS.BASE,
- payload: { formPayload },
+ payload: { formPayload, isValidatingMultiStepRegistrationPage },
});
-export const fetchRealtimeValidationsBegin = () => ({
+export const fetchRealtimeValidationsBegin = (isValidatingMultiStepRegistrationPage) => ({
type: REGISTER_FORM_VALIDATIONS.BEGIN,
+ payload: { isValidatingMultiStepRegistrationPage },
});
-export const fetchRealtimeValidationsSuccess = (validations) => ({
+export const fetchRealtimeValidationsSuccess = (validations, isValidatingMultiStepRegistrationPage) => ({
type: REGISTER_FORM_VALIDATIONS.SUCCESS,
- payload: { validations },
+ payload: { validations, isValidatingMultiStepRegistrationPage },
});
export const fetchRealtimeValidationsFailure = () => ({
@@ -83,3 +85,11 @@ export const setUserPipelineDataLoaded = (value) => ({
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
payload: { value },
});
+
+// Multi Step Registration Experiment Actions
+export const setMultiStepRegistrationExpData = (
+ multiStepRegExpVariation, multiStepRegistrationPageStep, isValidatingMultiStepRegistrationPage,
+) => ({
+ type: REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA,
+ payload: { multiStepRegExpVariation, multiStepRegistrationPageStep, isValidatingMultiStepRegistrationPage },
+});
diff --git a/src/register/data/optimizelyExperiment/helper.js b/src/register/data/optimizelyExperiment/helper.js
new file mode 100644
index 00000000..e59ec718
--- /dev/null
+++ b/src/register/data/optimizelyExperiment/helper.js
@@ -0,0 +1,105 @@
+/**
+ * This file contains data for Multi Step Registration Optimizely experiment
+ */
+import { getConfig } from '@edx/frontend-platform';
+
+import messages from '../../messages';
+
+export const NOT_INITIALIZED = 'experiment-not-initialized';
+export const CONTROL = 'control-registration-page';
+export const MULTI_STEP_REGISTRATION_EXP_VARIATION = 'multi-step-registration-page';
+
+export const FIRST_STEP = 'first-step';
+export const SECOND_STEP = 'second-step';
+export const THIRD_STEP = 'third-step';
+
+export const CONTROL_REGISTER_PAGE_FIRST_STEP_FIELDS = ['name', 'email', 'password', 'marketing_email_opt_in', 'ThirdPartyAuth'];
+export const CONTROL_REGISTER_PAGE_SECOND_STEP_FIELDS = ['username', 'country'];
+export const CONTROL_REGISTER_PAGE_COMMON_FIELDS = ['tos_and_honor_code', 'honor_code'];
+
+export const MULTI_STEP_REGISTER_PAGE_FIRST_STEP_FIELDS = ['email', 'marketing_email_opt_in', 'ThirdPartyAuth'];
+export const MULTI_STEP_REGISTER_PAGE_SECOND_STEP_FIELDS = ['name', 'password'];
+export const MULTI_STEP_REGISTER_PAGE_THIRD_STEP_FIELDS = ['username', 'country'];
+export const MULTI_STEP_REGISTER_PAGE_COMMON_FIELDS = ['tos_and_honor_code', 'honor_code'];
+
+const MULTI_STEP_REGISTRATION_EXP_PAGE = 'authn_register_page';
+
+export function getMultiStepRegistrationExperimentVariation() {
+ try {
+ if (window.optimizely
+ && window.optimizely.get('data').experiments[getConfig().MULTI_STEP_REGISTRATION_EXPERIMENT_ID]) {
+ const selectedVariant = window.optimizely.get('state').getVariationMap()[
+ getConfig().MULTI_STEP_REGISTRATION_EXPERIMENT_ID
+ ];
+ return selectedVariant?.name;
+ }
+ } catch (e) { /* empty */ }
+ return '';
+}
+
+export function activateMultiStepRegistrationExperiment() {
+ window.optimizely = window.optimizely || [];
+ window.optimizely.push({
+ type: 'page',
+ pageName: MULTI_STEP_REGISTRATION_EXP_PAGE,
+ });
+}
+
+/**
+ * We want to display username and honor_code fields in second page if user is in multi-step
+ * registration page experiment
+ */
+export const shouldDisplayFieldInExperiment = (fieldName, expVariation, registerPageStep) => (
+ !expVariation || expVariation === NOT_INITIALIZED
+ || (expVariation === CONTROL
+ && (
+ CONTROL_REGISTER_PAGE_COMMON_FIELDS.includes(fieldName)
+ || (registerPageStep === FIRST_STEP && CONTROL_REGISTER_PAGE_FIRST_STEP_FIELDS.includes(fieldName))
+ || (registerPageStep === SECOND_STEP && CONTROL_REGISTER_PAGE_SECOND_STEP_FIELDS.includes(fieldName))
+ ))
+ || (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION
+ && (
+ MULTI_STEP_REGISTER_PAGE_COMMON_FIELDS.includes(fieldName)
+ || (registerPageStep === FIRST_STEP && MULTI_STEP_REGISTER_PAGE_FIRST_STEP_FIELDS.includes(fieldName))
+ || (registerPageStep === SECOND_STEP && MULTI_STEP_REGISTER_PAGE_SECOND_STEP_FIELDS.includes(fieldName))
+ || (registerPageStep === THIRD_STEP && MULTI_STEP_REGISTER_PAGE_THIRD_STEP_FIELDS.includes(fieldName))
+ ))
+);
+
+export const getRegisterButtonLabelInExperiment = (
+ existingButtonLabel, expVariation, registerPageStep, formatMessage,
+) => {
+ if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep === FIRST_STEP) {
+ return formatMessage(messages['multistep.registration.exp.continue.button']);
+ }
+ return existingButtonLabel;
+};
+
+export const getRegisterButtonSubmitStateInExperiment = (
+ registerSubmitState, validationsSubmitState, expVariation, registerPageStep,
+) => {
+ if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep !== THIRD_STEP) {
+ return validationsSubmitState;
+ }
+ return registerSubmitState;
+};
+
+export const getMultiStepRegistrationPreviousStep = (currentStep) => {
+ if (currentStep === THIRD_STEP) {
+ return SECOND_STEP;
+ }
+ if (currentStep === SECOND_STEP) {
+ return FIRST_STEP;
+ }
+ return currentStep;
+};
+
+export const getMultiStepRegistrationNextStep = (currentStep) => {
+ if (currentStep === FIRST_STEP) {
+ return SECOND_STEP;
+ }
+ if (currentStep === SECOND_STEP) {
+ return THIRD_STEP;
+ }
+ return currentStep;
+};
diff --git a/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx b/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
new file mode 100644
index 00000000..692101b7
--- /dev/null
+++ b/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
@@ -0,0 +1,54 @@
+import { useEffect, useState } from 'react';
+
+import {
+ activateMultiStepRegistrationExperiment,
+ getMultiStepRegistrationExperimentVariation,
+ NOT_INITIALIZED,
+} from './helper';
+import { COMPLETE_STATE } from '../../../data/constants';
+
+/**
+ * This hook returns activates multi step registration experiment and returns the experiment
+ * variation for the user.
+ */
+const useMultiStepRegistrationExperimentVariation = (
+ initExpVariation,
+ registrationEmbedded,
+ tpaHint,
+ currentProvider,
+ thirdPartyAuthApiStatus,
+) => {
+ const [variation, setVariation] = useState(initExpVariation);
+
+ useEffect(() => {
+ if (initExpVariation || registrationEmbedded || !!tpaHint || !!currentProvider
+ || thirdPartyAuthApiStatus !== COMPLETE_STATE) {
+ return variation;
+ }
+
+ const getVariation = () => {
+ const expVariation = getMultiStepRegistrationExperimentVariation();
+ if (expVariation) {
+ setVariation(expVariation);
+ } else {
+ // This is to handle the case when user dont get variation for some reason, the register page
+ // shows unlimited spinner.
+ setVariation(NOT_INITIALIZED);
+ }
+ };
+
+ activateMultiStepRegistrationExperiment();
+
+ const timer = setTimeout(getVariation, 300);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [ // eslint-disable-line react-hooks/exhaustive-deps
+ currentProvider, initExpVariation, registrationEmbedded, thirdPartyAuthApiStatus, tpaHint,
+ ]);
+
+ return variation;
+};
+
+export default useMultiStepRegistrationExperimentVariation;
diff --git a/src/register/data/reducers.js b/src/register/data/reducers.js
index 70c3a994..32438877 100644
--- a/src/register/data/reducers.js
+++ b/src/register/data/reducers.js
@@ -5,9 +5,11 @@ import {
REGISTER_NEW_USER,
REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS,
+ REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA,
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from './actions';
+import { FIRST_STEP } from './optimizelyExperiment/helper';
import {
DEFAULT_STATE,
PENDING_STATE,
@@ -35,10 +37,14 @@ export const defaultState = {
},
validations: null,
submitState: DEFAULT_STATE,
+ validationsSubmitState: DEFAULT_STATE,
userPipelineDataLoaded: false,
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
+ multiStepRegExpVariation: '',
+ multiStepRegistrationPageStep: FIRST_STEP,
+ isValidatingMultiStepRegistrationPage: false,
};
const reducer = (state = defaultState, action = {}) => {
@@ -85,12 +91,22 @@ const reducer = (state = defaultState, action = {}) => {
registrationError: { ...registrationErrorTemp },
};
}
+ case REGISTER_FORM_VALIDATIONS.BEGIN: {
+ return {
+ ...state,
+ validationsSubmitState: action.payload?.isValidatingMultiStepRegistrationPage
+ ? PENDING_STATE
+ : state.validationsSubmitState,
+ };
+ }
case REGISTER_FORM_VALIDATIONS.SUCCESS: {
const { usernameSuggestions, ...validationWithoutUsernameSuggestions } = action.payload.validations;
return {
...state,
validations: validationWithoutUsernameSuggestions,
+ isValidatingMultiStepRegistrationPage: !!action.payload?.isValidatingMultiStepRegistrationPage,
usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
+ validationsSubmitState: DEFAULT_STATE,
};
}
case REGISTER_FORM_VALIDATIONS.FAILURE:
@@ -98,6 +114,7 @@ const reducer = (state = defaultState, action = {}) => {
...state,
validationApiRateLimited: true,
validations: null,
+ validationsSubmitState: DEFAULT_STATE,
};
case REGISTER_CLEAR_USERNAME_SUGGESTIONS:
return {
@@ -129,6 +146,14 @@ const reducer = (state = defaultState, action = {}) => {
emailSuggestion: action.payload.emailSuggestion,
},
};
+ case REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA: {
+ return {
+ ...state,
+ multiStepRegExpVariation: action.payload.multiStepRegExpVariation,
+ multiStepRegistrationPageStep: action.payload.multiStepRegistrationPageStep,
+ isValidatingMultiStepRegistrationPage: !!action.payload?.isValidatingMultiStepRegistrationPage,
+ };
+ }
default:
return {
...state,
diff --git a/src/register/data/sagas.js b/src/register/data/sagas.js
index 0325cd4b..3edd395e 100644
--- a/src/register/data/sagas.js
+++ b/src/register/data/sagas.js
@@ -40,10 +40,13 @@ export function* handleNewUserRegistration(action) {
export function* fetchRealtimeValidations(action) {
try {
- yield put(fetchRealtimeValidationsBegin());
+ yield put(fetchRealtimeValidationsBegin(action.payload?.isValidatingMultiStepRegistrationPage));
const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
- yield put(fetchRealtimeValidationsSuccess(camelCaseObject(fieldValidations)));
+ yield put(fetchRealtimeValidationsSuccess(
+ camelCaseObject(fieldValidations),
+ action.payload?.isValidatingMultiStepRegistrationPage,
+ ));
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(fetchRealtimeValidationsFailure());
diff --git a/src/register/data/tests/reducers.test.js b/src/register/data/tests/reducers.test.js
index 3e2270ab..909704d2 100644
--- a/src/register/data/tests/reducers.test.js
+++ b/src/register/data/tests/reducers.test.js
@@ -11,6 +11,7 @@ import {
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from '../actions';
+import { FIRST_STEP } from '../optimizelyExperiment/helper';
import reducer from '../reducers';
describe('Registration Reducer Tests', () => {
@@ -34,10 +35,14 @@ describe('Registration Reducer Tests', () => {
},
validations: null,
submitState: DEFAULT_STATE,
+ validationsSubmitState: DEFAULT_STATE,
userPipelineDataLoaded: false,
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
+ multiStepRegExpVariation: '',
+ multiStepRegistrationPageStep: FIRST_STEP,
+ isValidatingMultiStepRegistrationPage: false,
};
it('should return the initial state', () => {
diff --git a/src/register/messages.jsx b/src/register/messages.jsx
index 39d9e7f5..cea5e5df 100644
--- a/src/register/messages.jsx
+++ b/src/register/messages.jsx
@@ -201,6 +201,29 @@ const messages = defineMessages({
defaultMessage: 'Did you mean',
description: 'Did you mean alert suggestion',
},
+ // MultiStep Registration experiment
+ 'multistep.registration.exp.continue.button': {
+ id: 'multistep.registration.exp.continue.button',
+ defaultMessage: 'Continue',
+ description: 'Label text for multistep registration page second step',
+ },
+ 'multistep.registration.username.second.step.guideline.content': {
+ id: 'multistep.registration.username.second.step.guideline.content',
+ defaultMessage: 'Finish Registration',
+ description: 'Guideline content for username field in multi-step registration experiment step 2',
+ },
+ 'multistep.registration.username.third.step.guideline.content': {
+ id: 'multistep.registration.username.third.step.guideline.content',
+ defaultMessage: 'To finalize your registration, please confirm your country of residence '
+ + 'and create a public username that will identify you in your course communication forums. '
+ + 'The username cannot be changed.',
+ description: 'Guideline content for username field in multi-step registration experiment step 2',
+ },
+ 'multistep.registration.form.submission.error': {
+ id: 'multistep.registration.form.submission.error',
+ defaultMessage: 'Please check your responses for this and the previous step and try again.',
+ description: 'Error message that appears on top of the form when invalid form is submitted',
+ },
});
export default messages;
diff --git a/src/sass/_registration.scss b/src/sass/_registration.scss
index 75889601..4126da9a 100644
--- a/src/sass/_registration.scss
+++ b/src/sass/_registration.scss
@@ -1,7 +1,3 @@
-.register-button {
- min-width: 14.4rem;
-}
-
.pgn__form-autosuggest__wrapper > .pgn__form-group {
margin-bottom: 0 !important;
}
From 99850574fbdabd1dfcd920f1dc53d1022f6916b4 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Thu, 18 Apr 2024 11:04:28 +0500
Subject: [PATCH 31/82] feat: add multi step registration eventing (#1226)
* feat: implement multi step registration experiment
* feat: add multi step registration eventing
---
src/common-components/SocialAuthProviders.jsx | 14 ++++-
src/common-components/ThirdPartyAuth.jsx | 4 ++
.../data/optimizelyExperiment/track.js | 56 +++++++++++++++++++
...ltiStepRegistrationExperimentVariation.jsx | 2 +
4 files changed, 75 insertions(+), 1 deletion(-)
create mode 100644 src/register/data/optimizelyExperiment/track.js
diff --git a/src/common-components/SocialAuthProviders.jsx b/src/common-components/SocialAuthProviders.jsx
index abe06da7..5cdb04e0 100644
--- a/src/common-components/SocialAuthProviders.jsx
+++ b/src/common-components/SocialAuthProviders.jsx
@@ -9,14 +9,24 @@ import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
+import { CONTROL, MULTI_STEP_REGISTRATION_EXP_VARIATION } from '../register/data/optimizelyExperiment/helper';
+import { trackMultiStepRegistrationSSOBtnClicked } from '../register/data/optimizelyExperiment/track';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
- const { referrer, socialAuthProviders } = props;
+ const {
+ referrer,
+ socialAuthProviders,
+ multiStepRegistrationExpVariation,
+ } = props;
function handleSubmit(e) {
e.preventDefault();
+ if (multiStepRegistrationExpVariation === CONTROL
+ || multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION) {
+ trackMultiStepRegistrationSSOBtnClicked(multiStepRegistrationExpVariation);
+ }
const url = e.currentTarget.dataset.providerUrl;
window.location.href = getConfig().LMS_BASE_URL + url;
}
@@ -60,6 +70,7 @@ const SocialAuthProviders = (props) => {
SocialAuthProviders.defaultProps = {
referrer: LOGIN_PAGE,
socialAuthProviders: [],
+ multiStepRegistrationExpVariation: '',
};
SocialAuthProviders.propTypes = {
@@ -73,6 +84,7 @@ SocialAuthProviders.propTypes = {
registerUrl: PropTypes.string,
skipRegistrationForm: PropTypes.bool,
})),
+ multiStepRegistrationExpVariation: PropTypes.string,
};
export default SocialAuthProviders;
diff --git a/src/common-components/ThirdPartyAuth.jsx b/src/common-components/ThirdPartyAuth.jsx
index 7dcef3e1..406c8cf8 100644
--- a/src/common-components/ThirdPartyAuth.jsx
+++ b/src/common-components/ThirdPartyAuth.jsx
@@ -32,6 +32,7 @@ const ThirdPartyAuth = (props) => {
handleInstitutionLogin,
thirdPartyAuthApiStatus,
isLoginPage,
+ multiStepRegistrationExpVariation,
} = props;
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
const isSocialAuthActive = !!providers.length && !currentProvider;
@@ -79,6 +80,7 @@ const ThirdPartyAuth = (props) => {
)}
@@ -94,6 +96,7 @@ ThirdPartyAuth.defaultProps = {
secondaryProviders: [],
thirdPartyAuthApiStatus: PENDING_STATE,
isLoginPage: false,
+ multiStepRegistrationExpVariation: '',
};
ThirdPartyAuth.propTypes = {
@@ -121,6 +124,7 @@ ThirdPartyAuth.propTypes = {
),
thirdPartyAuthApiStatus: PropTypes.string,
isLoginPage: PropTypes.bool,
+ multiStepRegistrationExpVariation: PropTypes.string,
};
export default ThirdPartyAuth;
diff --git a/src/register/data/optimizelyExperiment/track.js b/src/register/data/optimizelyExperiment/track.js
new file mode 100644
index 00000000..8db71809
--- /dev/null
+++ b/src/register/data/optimizelyExperiment/track.js
@@ -0,0 +1,56 @@
+import { sendTrackEvent } from '@edx/frontend-platform/analytics';
+
+export const eventNames = {
+ multiStepRegistrationStep1Viewed: 'edx.bi.user.multistepregistration.step1.viewed',
+ multiStepRegistrationStep2Viewed: 'edx.bi.user.multistepregistration.step2.viewed',
+ multiStepRegistrationStep3Viewed: 'edx.bi.user.multistepregistration.step3.viewed',
+ multiStepRegistrationStep1SubmitBtnClicked: 'edx.bi.user.registration.step1.submit.click',
+ multiStepRegistrationStep2SubmitBtnClicked: 'edx.bi.user.registration.step2.submit.click',
+ multiStepRegistrationStep3SubmitBtnClicked: 'edx.bi.user.registration.step3.submit.click',
+ multiStepRegistrationFormSubmitBtnClicked: 'edx.bi.user.registration.form.submit.click',
+ multiStepRegistrationSSOBtnClicked: 'edx.bi.user.registration.sso.btn.click',
+};
+
+export const trackMultiStepRegistrationStep1Viewed = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep1Viewed, {
+ variation: expVariation,
+ });
+};
+
+export const trackMultiStepRegistrationStep2Viewed = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep2Viewed, {
+ variation: expVariation,
+ });
+};
+
+export const trackMultiStepRegistrationStep3Viewed = () => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep3Viewed, {});
+};
+
+export const trackMultiStepRegistrationStep1SubmitBtnClicked = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep1SubmitBtnClicked, {
+ variation: expVariation,
+ });
+};
+
+export const trackMultiStepRegistrationStep2SubmitBtnClicked = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep2SubmitBtnClicked, {
+ variation: expVariation,
+ });
+};
+
+export const trackMultiStepRegistrationStep3SubmitBtnClicked = () => {
+ sendTrackEvent(eventNames.multiStepRegistrationStep3SubmitBtnClicked, {});
+};
+
+export const trackMultiStepRegistrationFormSubmitBtnClicked = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationFormSubmitBtnClicked, {
+ variation: expVariation,
+ });
+};
+
+export const trackMultiStepRegistrationSSOBtnClicked = (expVariation) => {
+ sendTrackEvent(eventNames.multiStepRegistrationSSOBtnClicked, {
+ variation: expVariation,
+ });
+};
diff --git a/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx b/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
index 692101b7..97d0a233 100644
--- a/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
+++ b/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
@@ -5,6 +5,7 @@ import {
getMultiStepRegistrationExperimentVariation,
NOT_INITIALIZED,
} from './helper';
+import { trackMultiStepRegistrationStep1Viewed } from './track';
import { COMPLETE_STATE } from '../../../data/constants';
/**
@@ -30,6 +31,7 @@ const useMultiStepRegistrationExperimentVariation = (
const expVariation = getMultiStepRegistrationExperimentVariation();
if (expVariation) {
setVariation(expVariation);
+ trackMultiStepRegistrationStep1Viewed(expVariation);
} else {
// This is to handle the case when user dont get variation for some reason, the register page
// shows unlimited spinner.
From 4f48e82959186d145b6b67ddcbd545d9008441a7 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
Date: Mon, 22 Apr 2024 11:34:47 +0500
Subject: [PATCH 32/82] fix: fix register button width
---
src/register/data/optimizelyExperiment/helper.js | 7 +++++++
src/sass/_registration.scss | 8 ++++++++
2 files changed, 15 insertions(+)
diff --git a/src/register/data/optimizelyExperiment/helper.js b/src/register/data/optimizelyExperiment/helper.js
index e59ec718..7140d5ec 100644
--- a/src/register/data/optimizelyExperiment/helper.js
+++ b/src/register/data/optimizelyExperiment/helper.js
@@ -75,6 +75,13 @@ export const getRegisterButtonLabelInExperiment = (
return existingButtonLabel;
};
+export const getRegisterButtonClassInExperiment = (expVariation, registerPageStep) => {
+ if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep === FIRST_STEP) {
+ return 'continue-button';
+ }
+ return 'register-button';
+};
+
export const getRegisterButtonSubmitStateInExperiment = (
registerSubmitState, validationsSubmitState, expVariation, registerPageStep,
) => {
diff --git a/src/sass/_registration.scss b/src/sass/_registration.scss
index 4126da9a..884af10d 100644
--- a/src/sass/_registration.scss
+++ b/src/sass/_registration.scss
@@ -1,3 +1,11 @@
+.register-button {
+ min-width: 14.4rem;
+}
+
+.continue-button {
+ min-width: 7rem;
+}
+
.pgn__form-autosuggest__wrapper > .pgn__form-group {
margin-bottom: 0 !important;
}
From 788a42b341f9a80c2fc0c3f51b6c01e6b3921170 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
Date: Mon, 22 Apr 2024 15:17:08 +0500
Subject: [PATCH 33/82] fix: fix register button loader for control
---
src/register/data/optimizelyExperiment/helper.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/register/data/optimizelyExperiment/helper.js b/src/register/data/optimizelyExperiment/helper.js
index 7140d5ec..f3d21381 100644
--- a/src/register/data/optimizelyExperiment/helper.js
+++ b/src/register/data/optimizelyExperiment/helper.js
@@ -85,7 +85,8 @@ export const getRegisterButtonClassInExperiment = (expVariation, registerPageSte
export const getRegisterButtonSubmitStateInExperiment = (
registerSubmitState, validationsSubmitState, expVariation, registerPageStep,
) => {
- if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep !== THIRD_STEP) {
+ if ((expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep !== THIRD_STEP)
+ || (expVariation === CONTROL && registerPageStep !== SECOND_STEP)) {
return validationsSubmitState;
}
return registerSubmitState;
From f8290adab516f0883d9691c8a12752fadde328e6 Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Thu, 25 Apr 2024 15:28:05 +0500
Subject: [PATCH 34/82] feat: capture marketing lead in experiment events
(#1243)
---
src/register/data/optimizelyExperiment/track.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/register/data/optimizelyExperiment/track.js b/src/register/data/optimizelyExperiment/track.js
index 8db71809..68a6347a 100644
--- a/src/register/data/optimizelyExperiment/track.js
+++ b/src/register/data/optimizelyExperiment/track.js
@@ -17,9 +17,10 @@ export const trackMultiStepRegistrationStep1Viewed = (expVariation) => {
});
};
-export const trackMultiStepRegistrationStep2Viewed = (expVariation) => {
+export const trackMultiStepRegistrationStep2Viewed = (expVariation, isMarketingLead) => {
sendTrackEvent(eventNames.multiStepRegistrationStep2Viewed, {
variation: expVariation,
+ is_marketing_lead: isMarketingLead,
});
};
From a90ebb7d4d7ce6dd886ce6f97db1cba31ea6c626 Mon Sep 17 00:00:00 2001
From: mubbsharanwar
Date: Mon, 6 May 2024 15:57:54 +0500
Subject: [PATCH 35/82] revert: multistep registration experiment revert
multistep registration experiment changes
VAN-1930
---
src/common-components/SocialAuthProviders.jsx | 14 +--
src/common-components/ThirdPartyAuth.jsx | 4 -
src/common-components/messages.jsx | 6 -
src/config/index.js | 2 -
src/logistration/Logistration.jsx | 55 ++-------
src/logistration/Logistration.test.jsx | 7 --
src/register/RegistrationPage.jsx | 2 +-
src/register/RegistrationPage.test.jsx | 5 -
.../ConfigurableRegistrationForm.jsx | 24 +---
.../components/RegistrationFailure.jsx | 11 +-
.../ConfigurableRegistrationForm.test.jsx | 8 --
.../tests/RegistrationFailure.test.jsx | 5 -
.../components/tests/ThirdPartyAuth.test.jsx | 5 -
src/register/data/actions.js | 21 +---
.../data/optimizelyExperiment/helper.js | 113 ------------------
.../data/optimizelyExperiment/track.js | 57 ---------
...ltiStepRegistrationExperimentVariation.jsx | 56 ---------
src/register/data/reducers.js | 25 ----
src/register/data/sagas.js | 7 +-
src/register/data/tests/reducers.test.js | 5 -
src/register/data/utils.js | 36 +++---
src/register/messages.jsx | 23 ----
src/sass/_registration.scss | 4 -
23 files changed, 38 insertions(+), 457 deletions(-)
delete mode 100644 src/register/data/optimizelyExperiment/helper.js
delete mode 100644 src/register/data/optimizelyExperiment/track.js
delete mode 100644 src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
diff --git a/src/common-components/SocialAuthProviders.jsx b/src/common-components/SocialAuthProviders.jsx
index 5cdb04e0..abe06da7 100644
--- a/src/common-components/SocialAuthProviders.jsx
+++ b/src/common-components/SocialAuthProviders.jsx
@@ -9,24 +9,14 @@ import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
-import { CONTROL, MULTI_STEP_REGISTRATION_EXP_VARIATION } from '../register/data/optimizelyExperiment/helper';
-import { trackMultiStepRegistrationSSOBtnClicked } from '../register/data/optimizelyExperiment/track';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
- const {
- referrer,
- socialAuthProviders,
- multiStepRegistrationExpVariation,
- } = props;
+ const { referrer, socialAuthProviders } = props;
function handleSubmit(e) {
e.preventDefault();
- if (multiStepRegistrationExpVariation === CONTROL
- || multiStepRegistrationExpVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION) {
- trackMultiStepRegistrationSSOBtnClicked(multiStepRegistrationExpVariation);
- }
const url = e.currentTarget.dataset.providerUrl;
window.location.href = getConfig().LMS_BASE_URL + url;
}
@@ -70,7 +60,6 @@ const SocialAuthProviders = (props) => {
SocialAuthProviders.defaultProps = {
referrer: LOGIN_PAGE,
socialAuthProviders: [],
- multiStepRegistrationExpVariation: '',
};
SocialAuthProviders.propTypes = {
@@ -84,7 +73,6 @@ SocialAuthProviders.propTypes = {
registerUrl: PropTypes.string,
skipRegistrationForm: PropTypes.bool,
})),
- multiStepRegistrationExpVariation: PropTypes.string,
};
export default SocialAuthProviders;
diff --git a/src/common-components/ThirdPartyAuth.jsx b/src/common-components/ThirdPartyAuth.jsx
index 406c8cf8..7dcef3e1 100644
--- a/src/common-components/ThirdPartyAuth.jsx
+++ b/src/common-components/ThirdPartyAuth.jsx
@@ -32,7 +32,6 @@ const ThirdPartyAuth = (props) => {
handleInstitutionLogin,
thirdPartyAuthApiStatus,
isLoginPage,
- multiStepRegistrationExpVariation,
} = props;
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
const isSocialAuthActive = !!providers.length && !currentProvider;
@@ -80,7 +79,6 @@ const ThirdPartyAuth = (props) => {
)}
@@ -96,7 +94,6 @@ ThirdPartyAuth.defaultProps = {
secondaryProviders: [],
thirdPartyAuthApiStatus: PENDING_STATE,
isLoginPage: false,
- multiStepRegistrationExpVariation: '',
};
ThirdPartyAuth.propTypes = {
@@ -124,7 +121,6 @@ ThirdPartyAuth.propTypes = {
),
thirdPartyAuthApiStatus: PropTypes.string,
isLoginPage: PropTypes.bool,
- multiStepRegistrationExpVariation: PropTypes.string,
};
export default ThirdPartyAuth;
diff --git a/src/common-components/messages.jsx b/src/common-components/messages.jsx
index 6f0e69ca..08e88b8f 100644
--- a/src/common-components/messages.jsx
+++ b/src/common-components/messages.jsx
@@ -132,12 +132,6 @@ const messages = defineMessages({
defaultMessage: 'Company or school credentials',
description: 'Company or school login link text.',
},
- // multi step registration experiment messages
- 'tab.back.btn.text': {
- id: 'tab.back.btn.text',
- defaultMessage: 'Back',
- description: 'Tab back button text',
- },
});
export default messages;
diff --git a/src/config/index.js b/src/config/index.js
index 781db13c..badb6fe7 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -35,8 +35,6 @@ const configuration = {
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
- // Multi Step Registration Experiment
- MULTI_STEP_REGISTRATION_EXPERIMENT_ID: process.env.MULTI_STEP_REGISTRATION_EXPERIMENT_ID || '',
};
export default configuration;
diff --git a/src/logistration/Logistration.jsx b/src/logistration/Logistration.jsx
index 5199e1b3..9451aa27 100644
--- a/src/logistration/Logistration.jsx
+++ b/src/logistration/Logistration.jsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import { connect, useDispatch, useSelector } from 'react-redux';
+import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -7,11 +7,10 @@ import { getAuthService } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Icon,
- IconButton,
Tab,
Tabs,
} from '@openedx/paragon';
-import { ArrowBackIos, ChevronLeft } from '@openedx/paragon/icons';
+import { ChevronLeft } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { Navigate, useNavigate } from 'react-router-dom';
@@ -28,11 +27,7 @@ import {
import { LoginPage } from '../login';
import { backupLoginForm } from '../login/data/actions';
import { RegistrationPage } from '../register';
-import { backupRegistrationForm, setMultiStepRegistrationExpData } from '../register/data/actions';
-import {
- FIRST_STEP,
- getMultiStepRegistrationPreviousStep,
-} from '../register/data/optimizelyExperiment/helper';
+import { backupRegistrationForm } from '../register/data/actions';
const Logistration = (props) => {
const { selectedPage, tpaProviders } = props;
@@ -47,10 +42,6 @@ const Logistration = (props) => {
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
const hideRegistrationLink = getConfig().SHOW_REGISTRATION_LINKS === false;
- const dispatch = useDispatch();
- const multiStepRegExpVariation = useSelector(state => state.register.multiStepRegExpVariation);
- const multiStepRegistrationPageStep = useSelector(state => state.register.multiStepRegistrationPageStep);
-
useEffect(() => {
const authService = getAuthService();
if (authService) {
@@ -100,39 +91,6 @@ const Logistration = (props) => {
);
- /**
- * Temporary function created to resolve the complexity in tabs conditioning for multi-step
- * registration experiment
- */
- const getTabs = () => {
- if (multiStepRegistrationPageStep !== FIRST_STEP) {
- const prevStep = getMultiStepRegistrationPreviousStep(multiStepRegistrationPageStep);
- return (
-
- {
- dispatch(setMultiStepRegistrationExpData(multiStepRegExpVariation, prevStep));
- }}
- variant="primary"
- size="inline"
- className="mr-1"
- />
- {formatMessage(messages['tab.back.btn.text'])}
-
- );
- }
- return (
- handleOnSelect(tabKey, selectedPage)}>
-
-
-
- );
- };
-
const isValidTpaHint = () => {
const { provider } = getTpaProvider(tpaHint, providers, secondaryProviders);
return !!provider;
@@ -165,7 +123,12 @@ const Logistration = (props) => {
)
- : (!isValidTpaHint() && !hideRegistrationLink && getTabs())}
+ : (!isValidTpaHint() && !hideRegistrationLink && (
+ handleOnSelect(tabKey, selectedPage)}>
+
+
+
+ ))}
{ key && (
)}
diff --git a/src/logistration/Logistration.test.jsx b/src/logistration/Logistration.test.jsx
index ce7af88f..87bf3e70 100644
--- a/src/logistration/Logistration.test.jsx
+++ b/src/logistration/Logistration.test.jsx
@@ -15,16 +15,12 @@ import {
} from '../data/constants';
import { backupLoginForm } from '../login/data/actions';
import { backupRegistrationForm } from '../register/data/actions';
-import { FIRST_STEP, NOT_INITIALIZED } from '../register/data/optimizelyExperiment/helper';
-import useMultiStepRegistrationExperimentVariation
- from '../register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
-jest.mock('../register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
@@ -67,8 +63,6 @@ describe('Logistration', () => {
registrationError: {},
usernameSuggestions: [],
validationApiRateLimited: false,
- multiStepRegExpVariation: '',
- multiStepRegistrationPageStep: FIRST_STEP,
},
commonComponents: {
thirdPartyAuthContext: {
@@ -89,7 +83,6 @@ describe('Logistration', () => {
username: 'test-user',
})),
}));
- useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({
loggingService: { logError: jest.fn() },
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 145c5a38..8c5ea79f 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -176,7 +176,7 @@ const RegistrationPage = (props) => {
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
}
- }, [registrationResult]); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [registrationResult]);
const handleOnChange = (event) => {
const { name } = event.target;
diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx
index 59a4d8aa..18e4e513 100644
--- a/src/register/RegistrationPage.test.jsx
+++ b/src/register/RegistrationPage.test.jsx
@@ -17,9 +17,6 @@ import {
setUserPipelineDataLoaded,
} from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants';
-import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
-import useMultiStepRegistrationExperimentVariation
- from './data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
@@ -33,7 +30,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
-jest.mock('./data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -132,7 +128,6 @@ describe('RegistrationPage', () => {
institutionLogin: false,
};
window.location = { search: '' };
- useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx
index ce0fbea5..8c300b7e 100644
--- a/src/register/components/ConfigurableRegistrationForm.jsx
+++ b/src/register/components/ConfigurableRegistrationForm.jsx
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import { FormFieldRenderer } from '../../field-renderer';
import { FIELDS } from '../data/constants';
-import { FIRST_STEP, shouldDisplayFieldInExperiment } from '../data/optimizelyExperiment/helper';
import messages from '../messages';
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
@@ -32,8 +31,6 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors,
setFormFields,
autoSubmitRegistrationForm,
- multiStepRegistrationExpVariation,
- multiStepRegistrationPageStep,
} = props;
/** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
@@ -112,9 +109,7 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
};
- if (flags.showConfigurableRegistrationFields && shouldDisplayFieldInExperiment(
- 'other_fields', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
- )) {
+ if (flags.showConfigurableRegistrationFields) {
Object.keys(fieldDescriptions).forEach(fieldName => {
const fieldData = fieldDescriptions[fieldName];
switch (fieldData.name) {
@@ -166,9 +161,7 @@ const ConfigurableRegistrationForm = (props) => {
});
}
- if ((flags.showConfigurableEdxFields || showCountryField) && shouldDisplayFieldInExperiment(
- 'country', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
- )) {
+ if (flags.showConfigurableEdxFields || showCountryField) {
formFieldDescriptions.push(
{
);
}
- if (flags.showMarketingEmailOptInCheckbox && shouldDisplayFieldInExperiment(
- 'marketing_email_opt_in', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
- )) {
+ if (flags.showMarketingEmailOptInCheckbox) {
formFieldDescriptions.push(
{
);
}
- if ((flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode)
- && shouldDisplayFieldInExperiment(
- 'honor_code', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
- )) {
+ if (flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode) {
formFieldDescriptions.push(
@@ -243,15 +231,11 @@ ConfigurableRegistrationForm.propTypes = {
setFieldErrors: PropTypes.func.isRequired,
setFormFields: PropTypes.func.isRequired,
autoSubmitRegistrationForm: PropTypes.bool,
- multiStepRegistrationExpVariation: PropTypes.string,
- multiStepRegistrationPageStep: PropTypes.string,
};
ConfigurableRegistrationForm.defaultProps = {
fieldDescriptions: {},
autoSubmitRegistrationForm: false,
- multiStepRegistrationExpVariation: '',
- multiStepRegistrationPageStep: FIRST_STEP,
};
export default ConfigurableRegistrationForm;
diff --git a/src/register/components/RegistrationFailure.jsx b/src/register/components/RegistrationFailure.jsx
index f2a83585..c34c2af7 100644
--- a/src/register/components/RegistrationFailure.jsx
+++ b/src/register/components/RegistrationFailure.jsx
@@ -13,13 +13,12 @@ import {
TPA_AUTHENTICATION_FAILURE,
TPA_SESSION_EXPIRED,
} from '../data/constants';
-import { FIRST_STEP } from '../data/optimizelyExperiment/helper';
import messages from '../messages';
const RegistrationFailureMessage = (props) => {
const { formatMessage } = useIntl();
const {
- context, errorCode, failureCount, multiStepRegistrationPageStep,
+ context, errorCode, failureCount,
} = props;
useEffect(() => {
@@ -50,11 +49,7 @@ const RegistrationFailureMessage = (props) => {
errorMessage = formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider });
break;
default:
- if (multiStepRegistrationPageStep !== FIRST_STEP) {
- errorMessage = formatMessage(messages['multistep.registration.form.submission.error']);
- } else {
- errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
- }
+ errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
break;
}
@@ -70,7 +65,6 @@ RegistrationFailureMessage.defaultProps = {
context: {
errorMessage: null,
},
- multiStepRegistrationPageStep: FIRST_STEP,
};
RegistrationFailureMessage.propTypes = {
@@ -80,7 +74,6 @@ RegistrationFailureMessage.propTypes = {
}),
errorCode: PropTypes.string.isRequired,
failureCount: PropTypes.number.isRequired,
- multiStepRegistrationPageStep: PropTypes.string,
};
export default RegistrationFailureMessage;
diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
index 00fe5595..3d664260 100644
--- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
+++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
@@ -11,9 +11,6 @@ import configureStore from 'redux-mock-store';
import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants';
-import { FIRST_STEP, NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
-import useMultiStepRegistrationExperimentVariation
- from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
@@ -25,7 +22,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
-jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
@@ -97,9 +93,6 @@ describe('ConfigurableRegistrationForm', () => {
registrationError: {},
registrationFormData,
usernameSuggestions: [],
- multiStepRegistrationPageStep: FIRST_STEP,
- multiStepRegExpVariation: '',
- isValidatingMultiStepRegistrationPage: false,
},
commonComponents: {
thirdPartyAuthApiStatus: null,
@@ -128,7 +121,6 @@ describe('ConfigurableRegistrationForm', () => {
};
window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us'));
- useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/RegistrationFailure.test.jsx b/src/register/components/tests/RegistrationFailure.test.jsx
index b9667c9c..003cc966 100644
--- a/src/register/components/tests/RegistrationFailure.test.jsx
+++ b/src/register/components/tests/RegistrationFailure.test.jsx
@@ -12,9 +12,6 @@ import configureStore from 'redux-mock-store';
import {
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
} from '../../data/constants';
-import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
-import useMultiStepRegistrationExperimentVariation
- from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import RegistrationFailureMessage from '../RegistrationFailure';
@@ -26,7 +23,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
-jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
@@ -125,7 +121,6 @@ describe('RegistrationFailure', () => {
institutionLogin: false,
};
window.location = { search: '' };
- useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/ThirdPartyAuth.test.jsx b/src/register/components/tests/ThirdPartyAuth.test.jsx
index 45672ad5..917f10f9 100644
--- a/src/register/components/tests/ThirdPartyAuth.test.jsx
+++ b/src/register/components/tests/ThirdPartyAuth.test.jsx
@@ -12,9 +12,6 @@ import configureStore from 'redux-mock-store';
import {
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants';
-import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
-import useMultiStepRegistrationExperimentVariation
- from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -25,7 +22,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
-jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -124,7 +120,6 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false,
};
window.location = { search: '' };
- useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/data/actions.js b/src/register/data/actions.js
index 530f3bc8..68bfe9e6 100644
--- a/src/register/data/actions.js
+++ b/src/register/data/actions.js
@@ -8,8 +8,6 @@ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERRO
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
-export const REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA = 'REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA';
-
// Backup registration form
export const backupRegistrationForm = () => ({
type: BACKUP_REGISTRATION_DATA.BASE,
@@ -21,19 +19,18 @@ export const backupRegistrationFormBegin = (data) => ({
});
// Validate fields from the backend
-export const fetchRealtimeValidations = (formPayload, isValidatingMultiStepRegistrationPage) => ({
+export const fetchRealtimeValidations = (formPayload) => ({
type: REGISTER_FORM_VALIDATIONS.BASE,
- payload: { formPayload, isValidatingMultiStepRegistrationPage },
+ payload: { formPayload },
});
-export const fetchRealtimeValidationsBegin = (isValidatingMultiStepRegistrationPage) => ({
+export const fetchRealtimeValidationsBegin = () => ({
type: REGISTER_FORM_VALIDATIONS.BEGIN,
- payload: { isValidatingMultiStepRegistrationPage },
});
-export const fetchRealtimeValidationsSuccess = (validations, isValidatingMultiStepRegistrationPage) => ({
+export const fetchRealtimeValidationsSuccess = (validations) => ({
type: REGISTER_FORM_VALIDATIONS.SUCCESS,
- payload: { validations, isValidatingMultiStepRegistrationPage },
+ payload: { validations },
});
export const fetchRealtimeValidationsFailure = () => ({
@@ -85,11 +82,3 @@ export const setUserPipelineDataLoaded = (value) => ({
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
payload: { value },
});
-
-// Multi Step Registration Experiment Actions
-export const setMultiStepRegistrationExpData = (
- multiStepRegExpVariation, multiStepRegistrationPageStep, isValidatingMultiStepRegistrationPage,
-) => ({
- type: REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA,
- payload: { multiStepRegExpVariation, multiStepRegistrationPageStep, isValidatingMultiStepRegistrationPage },
-});
diff --git a/src/register/data/optimizelyExperiment/helper.js b/src/register/data/optimizelyExperiment/helper.js
deleted file mode 100644
index f3d21381..00000000
--- a/src/register/data/optimizelyExperiment/helper.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * This file contains data for Multi Step Registration Optimizely experiment
- */
-import { getConfig } from '@edx/frontend-platform';
-
-import messages from '../../messages';
-
-export const NOT_INITIALIZED = 'experiment-not-initialized';
-export const CONTROL = 'control-registration-page';
-export const MULTI_STEP_REGISTRATION_EXP_VARIATION = 'multi-step-registration-page';
-
-export const FIRST_STEP = 'first-step';
-export const SECOND_STEP = 'second-step';
-export const THIRD_STEP = 'third-step';
-
-export const CONTROL_REGISTER_PAGE_FIRST_STEP_FIELDS = ['name', 'email', 'password', 'marketing_email_opt_in', 'ThirdPartyAuth'];
-export const CONTROL_REGISTER_PAGE_SECOND_STEP_FIELDS = ['username', 'country'];
-export const CONTROL_REGISTER_PAGE_COMMON_FIELDS = ['tos_and_honor_code', 'honor_code'];
-
-export const MULTI_STEP_REGISTER_PAGE_FIRST_STEP_FIELDS = ['email', 'marketing_email_opt_in', 'ThirdPartyAuth'];
-export const MULTI_STEP_REGISTER_PAGE_SECOND_STEP_FIELDS = ['name', 'password'];
-export const MULTI_STEP_REGISTER_PAGE_THIRD_STEP_FIELDS = ['username', 'country'];
-export const MULTI_STEP_REGISTER_PAGE_COMMON_FIELDS = ['tos_and_honor_code', 'honor_code'];
-
-const MULTI_STEP_REGISTRATION_EXP_PAGE = 'authn_register_page';
-
-export function getMultiStepRegistrationExperimentVariation() {
- try {
- if (window.optimizely
- && window.optimizely.get('data').experiments[getConfig().MULTI_STEP_REGISTRATION_EXPERIMENT_ID]) {
- const selectedVariant = window.optimizely.get('state').getVariationMap()[
- getConfig().MULTI_STEP_REGISTRATION_EXPERIMENT_ID
- ];
- return selectedVariant?.name;
- }
- } catch (e) { /* empty */ }
- return '';
-}
-
-export function activateMultiStepRegistrationExperiment() {
- window.optimizely = window.optimizely || [];
- window.optimizely.push({
- type: 'page',
- pageName: MULTI_STEP_REGISTRATION_EXP_PAGE,
- });
-}
-
-/**
- * We want to display username and honor_code fields in second page if user is in multi-step
- * registration page experiment
- */
-export const shouldDisplayFieldInExperiment = (fieldName, expVariation, registerPageStep) => (
- !expVariation || expVariation === NOT_INITIALIZED
- || (expVariation === CONTROL
- && (
- CONTROL_REGISTER_PAGE_COMMON_FIELDS.includes(fieldName)
- || (registerPageStep === FIRST_STEP && CONTROL_REGISTER_PAGE_FIRST_STEP_FIELDS.includes(fieldName))
- || (registerPageStep === SECOND_STEP && CONTROL_REGISTER_PAGE_SECOND_STEP_FIELDS.includes(fieldName))
- ))
- || (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION
- && (
- MULTI_STEP_REGISTER_PAGE_COMMON_FIELDS.includes(fieldName)
- || (registerPageStep === FIRST_STEP && MULTI_STEP_REGISTER_PAGE_FIRST_STEP_FIELDS.includes(fieldName))
- || (registerPageStep === SECOND_STEP && MULTI_STEP_REGISTER_PAGE_SECOND_STEP_FIELDS.includes(fieldName))
- || (registerPageStep === THIRD_STEP && MULTI_STEP_REGISTER_PAGE_THIRD_STEP_FIELDS.includes(fieldName))
- ))
-);
-
-export const getRegisterButtonLabelInExperiment = (
- existingButtonLabel, expVariation, registerPageStep, formatMessage,
-) => {
- if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep === FIRST_STEP) {
- return formatMessage(messages['multistep.registration.exp.continue.button']);
- }
- return existingButtonLabel;
-};
-
-export const getRegisterButtonClassInExperiment = (expVariation, registerPageStep) => {
- if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep === FIRST_STEP) {
- return 'continue-button';
- }
- return 'register-button';
-};
-
-export const getRegisterButtonSubmitStateInExperiment = (
- registerSubmitState, validationsSubmitState, expVariation, registerPageStep,
-) => {
- if ((expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep !== THIRD_STEP)
- || (expVariation === CONTROL && registerPageStep !== SECOND_STEP)) {
- return validationsSubmitState;
- }
- return registerSubmitState;
-};
-
-export const getMultiStepRegistrationPreviousStep = (currentStep) => {
- if (currentStep === THIRD_STEP) {
- return SECOND_STEP;
- }
- if (currentStep === SECOND_STEP) {
- return FIRST_STEP;
- }
- return currentStep;
-};
-
-export const getMultiStepRegistrationNextStep = (currentStep) => {
- if (currentStep === FIRST_STEP) {
- return SECOND_STEP;
- }
- if (currentStep === SECOND_STEP) {
- return THIRD_STEP;
- }
- return currentStep;
-};
diff --git a/src/register/data/optimizelyExperiment/track.js b/src/register/data/optimizelyExperiment/track.js
deleted file mode 100644
index 68a6347a..00000000
--- a/src/register/data/optimizelyExperiment/track.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { sendTrackEvent } from '@edx/frontend-platform/analytics';
-
-export const eventNames = {
- multiStepRegistrationStep1Viewed: 'edx.bi.user.multistepregistration.step1.viewed',
- multiStepRegistrationStep2Viewed: 'edx.bi.user.multistepregistration.step2.viewed',
- multiStepRegistrationStep3Viewed: 'edx.bi.user.multistepregistration.step3.viewed',
- multiStepRegistrationStep1SubmitBtnClicked: 'edx.bi.user.registration.step1.submit.click',
- multiStepRegistrationStep2SubmitBtnClicked: 'edx.bi.user.registration.step2.submit.click',
- multiStepRegistrationStep3SubmitBtnClicked: 'edx.bi.user.registration.step3.submit.click',
- multiStepRegistrationFormSubmitBtnClicked: 'edx.bi.user.registration.form.submit.click',
- multiStepRegistrationSSOBtnClicked: 'edx.bi.user.registration.sso.btn.click',
-};
-
-export const trackMultiStepRegistrationStep1Viewed = (expVariation) => {
- sendTrackEvent(eventNames.multiStepRegistrationStep1Viewed, {
- variation: expVariation,
- });
-};
-
-export const trackMultiStepRegistrationStep2Viewed = (expVariation, isMarketingLead) => {
- sendTrackEvent(eventNames.multiStepRegistrationStep2Viewed, {
- variation: expVariation,
- is_marketing_lead: isMarketingLead,
- });
-};
-
-export const trackMultiStepRegistrationStep3Viewed = () => {
- sendTrackEvent(eventNames.multiStepRegistrationStep3Viewed, {});
-};
-
-export const trackMultiStepRegistrationStep1SubmitBtnClicked = (expVariation) => {
- sendTrackEvent(eventNames.multiStepRegistrationStep1SubmitBtnClicked, {
- variation: expVariation,
- });
-};
-
-export const trackMultiStepRegistrationStep2SubmitBtnClicked = (expVariation) => {
- sendTrackEvent(eventNames.multiStepRegistrationStep2SubmitBtnClicked, {
- variation: expVariation,
- });
-};
-
-export const trackMultiStepRegistrationStep3SubmitBtnClicked = () => {
- sendTrackEvent(eventNames.multiStepRegistrationStep3SubmitBtnClicked, {});
-};
-
-export const trackMultiStepRegistrationFormSubmitBtnClicked = (expVariation) => {
- sendTrackEvent(eventNames.multiStepRegistrationFormSubmitBtnClicked, {
- variation: expVariation,
- });
-};
-
-export const trackMultiStepRegistrationSSOBtnClicked = (expVariation) => {
- sendTrackEvent(eventNames.multiStepRegistrationSSOBtnClicked, {
- variation: expVariation,
- });
-};
diff --git a/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx b/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
deleted file mode 100644
index 97d0a233..00000000
--- a/src/register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation.jsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import {
- activateMultiStepRegistrationExperiment,
- getMultiStepRegistrationExperimentVariation,
- NOT_INITIALIZED,
-} from './helper';
-import { trackMultiStepRegistrationStep1Viewed } from './track';
-import { COMPLETE_STATE } from '../../../data/constants';
-
-/**
- * This hook returns activates multi step registration experiment and returns the experiment
- * variation for the user.
- */
-const useMultiStepRegistrationExperimentVariation = (
- initExpVariation,
- registrationEmbedded,
- tpaHint,
- currentProvider,
- thirdPartyAuthApiStatus,
-) => {
- const [variation, setVariation] = useState(initExpVariation);
-
- useEffect(() => {
- if (initExpVariation || registrationEmbedded || !!tpaHint || !!currentProvider
- || thirdPartyAuthApiStatus !== COMPLETE_STATE) {
- return variation;
- }
-
- const getVariation = () => {
- const expVariation = getMultiStepRegistrationExperimentVariation();
- if (expVariation) {
- setVariation(expVariation);
- trackMultiStepRegistrationStep1Viewed(expVariation);
- } else {
- // This is to handle the case when user dont get variation for some reason, the register page
- // shows unlimited spinner.
- setVariation(NOT_INITIALIZED);
- }
- };
-
- activateMultiStepRegistrationExperiment();
-
- const timer = setTimeout(getVariation, 300);
-
- return () => {
- clearTimeout(timer);
- };
- }, [ // eslint-disable-line react-hooks/exhaustive-deps
- currentProvider, initExpVariation, registrationEmbedded, thirdPartyAuthApiStatus, tpaHint,
- ]);
-
- return variation;
-};
-
-export default useMultiStepRegistrationExperimentVariation;
diff --git a/src/register/data/reducers.js b/src/register/data/reducers.js
index 32438877..70c3a994 100644
--- a/src/register/data/reducers.js
+++ b/src/register/data/reducers.js
@@ -5,11 +5,9 @@ import {
REGISTER_NEW_USER,
REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS,
- REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA,
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from './actions';
-import { FIRST_STEP } from './optimizelyExperiment/helper';
import {
DEFAULT_STATE,
PENDING_STATE,
@@ -37,14 +35,10 @@ export const defaultState = {
},
validations: null,
submitState: DEFAULT_STATE,
- validationsSubmitState: DEFAULT_STATE,
userPipelineDataLoaded: false,
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
- multiStepRegExpVariation: '',
- multiStepRegistrationPageStep: FIRST_STEP,
- isValidatingMultiStepRegistrationPage: false,
};
const reducer = (state = defaultState, action = {}) => {
@@ -91,22 +85,12 @@ const reducer = (state = defaultState, action = {}) => {
registrationError: { ...registrationErrorTemp },
};
}
- case REGISTER_FORM_VALIDATIONS.BEGIN: {
- return {
- ...state,
- validationsSubmitState: action.payload?.isValidatingMultiStepRegistrationPage
- ? PENDING_STATE
- : state.validationsSubmitState,
- };
- }
case REGISTER_FORM_VALIDATIONS.SUCCESS: {
const { usernameSuggestions, ...validationWithoutUsernameSuggestions } = action.payload.validations;
return {
...state,
validations: validationWithoutUsernameSuggestions,
- isValidatingMultiStepRegistrationPage: !!action.payload?.isValidatingMultiStepRegistrationPage,
usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
- validationsSubmitState: DEFAULT_STATE,
};
}
case REGISTER_FORM_VALIDATIONS.FAILURE:
@@ -114,7 +98,6 @@ const reducer = (state = defaultState, action = {}) => {
...state,
validationApiRateLimited: true,
validations: null,
- validationsSubmitState: DEFAULT_STATE,
};
case REGISTER_CLEAR_USERNAME_SUGGESTIONS:
return {
@@ -146,14 +129,6 @@ const reducer = (state = defaultState, action = {}) => {
emailSuggestion: action.payload.emailSuggestion,
},
};
- case REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA: {
- return {
- ...state,
- multiStepRegExpVariation: action.payload.multiStepRegExpVariation,
- multiStepRegistrationPageStep: action.payload.multiStepRegistrationPageStep,
- isValidatingMultiStepRegistrationPage: !!action.payload?.isValidatingMultiStepRegistrationPage,
- };
- }
default:
return {
...state,
diff --git a/src/register/data/sagas.js b/src/register/data/sagas.js
index 3edd395e..0325cd4b 100644
--- a/src/register/data/sagas.js
+++ b/src/register/data/sagas.js
@@ -40,13 +40,10 @@ export function* handleNewUserRegistration(action) {
export function* fetchRealtimeValidations(action) {
try {
- yield put(fetchRealtimeValidationsBegin(action.payload?.isValidatingMultiStepRegistrationPage));
+ yield put(fetchRealtimeValidationsBegin());
const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
- yield put(fetchRealtimeValidationsSuccess(
- camelCaseObject(fieldValidations),
- action.payload?.isValidatingMultiStepRegistrationPage,
- ));
+ yield put(fetchRealtimeValidationsSuccess(camelCaseObject(fieldValidations)));
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(fetchRealtimeValidationsFailure());
diff --git a/src/register/data/tests/reducers.test.js b/src/register/data/tests/reducers.test.js
index 909704d2..3e2270ab 100644
--- a/src/register/data/tests/reducers.test.js
+++ b/src/register/data/tests/reducers.test.js
@@ -11,7 +11,6 @@ import {
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from '../actions';
-import { FIRST_STEP } from '../optimizelyExperiment/helper';
import reducer from '../reducers';
describe('Registration Reducer Tests', () => {
@@ -35,14 +34,10 @@ describe('Registration Reducer Tests', () => {
},
validations: null,
submitState: DEFAULT_STATE,
- validationsSubmitState: DEFAULT_STATE,
userPipelineDataLoaded: false,
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
- multiStepRegExpVariation: '',
- multiStepRegistrationPageStep: FIRST_STEP,
- isValidatingMultiStepRegistrationPage: false,
};
it('should return the initial state', () => {
diff --git a/src/register/data/utils.js b/src/register/data/utils.js
index 25694527..e7773e2b 100644
--- a/src/register/data/utils.js
+++ b/src/register/data/utils.js
@@ -43,39 +43,31 @@ export const isFormValid = (
Object.keys(payload).forEach(key => {
switch (key) {
case 'name':
- if (!fieldErrors.name) {
- fieldErrors.name = validateName(payload.name, formatMessage);
- }
+ fieldErrors.name = validateName(payload.name, formatMessage);
if (fieldErrors.name) { isValid = false; }
break;
case 'email': {
- if (!fieldErrors.email) {
- const {
- fieldError, confirmEmailError, suggestion,
- } = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
- if (fieldError) {
- fieldErrors.email = fieldError;
- isValid = false;
- }
- if (confirmEmailError) {
- fieldErrors.confirm_email = confirmEmailError;
- isValid = false;
- }
- emailSuggestion = suggestion;
+ const {
+ fieldError, confirmEmailError, suggestion,
+ } = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
+ if (fieldError) {
+ fieldErrors.email = fieldError;
+ isValid = false;
}
+ if (confirmEmailError) {
+ fieldErrors.confirm_email = confirmEmailError;
+ isValid = false;
+ }
+ emailSuggestion = suggestion;
if (fieldErrors.email) { isValid = false; }
break;
}
case 'username':
- if (!fieldErrors.username) {
- fieldErrors.username = validateUsername(payload.username, formatMessage);
- }
+ fieldErrors.username = validateUsername(payload.username, formatMessage);
if (fieldErrors.username) { isValid = false; }
break;
case 'password':
- if (!fieldErrors.password) {
- fieldErrors.password = validatePasswordField(payload.password, formatMessage);
- }
+ fieldErrors.password = validatePasswordField(payload.password, formatMessage);
if (fieldErrors.password) { isValid = false; }
break;
default:
diff --git a/src/register/messages.jsx b/src/register/messages.jsx
index cea5e5df..39d9e7f5 100644
--- a/src/register/messages.jsx
+++ b/src/register/messages.jsx
@@ -201,29 +201,6 @@ const messages = defineMessages({
defaultMessage: 'Did you mean',
description: 'Did you mean alert suggestion',
},
- // MultiStep Registration experiment
- 'multistep.registration.exp.continue.button': {
- id: 'multistep.registration.exp.continue.button',
- defaultMessage: 'Continue',
- description: 'Label text for multistep registration page second step',
- },
- 'multistep.registration.username.second.step.guideline.content': {
- id: 'multistep.registration.username.second.step.guideline.content',
- defaultMessage: 'Finish Registration',
- description: 'Guideline content for username field in multi-step registration experiment step 2',
- },
- 'multistep.registration.username.third.step.guideline.content': {
- id: 'multistep.registration.username.third.step.guideline.content',
- defaultMessage: 'To finalize your registration, please confirm your country of residence '
- + 'and create a public username that will identify you in your course communication forums. '
- + 'The username cannot be changed.',
- description: 'Guideline content for username field in multi-step registration experiment step 2',
- },
- 'multistep.registration.form.submission.error': {
- id: 'multistep.registration.form.submission.error',
- defaultMessage: 'Please check your responses for this and the previous step and try again.',
- description: 'Error message that appears on top of the form when invalid form is submitted',
- },
});
export default messages;
diff --git a/src/sass/_registration.scss b/src/sass/_registration.scss
index 884af10d..75889601 100644
--- a/src/sass/_registration.scss
+++ b/src/sass/_registration.scss
@@ -2,10 +2,6 @@
min-width: 14.4rem;
}
-.continue-button {
- min-width: 7rem;
-}
-
.pgn__form-autosuggest__wrapper > .pgn__form-group {
margin-bottom: 0 !important;
}
From ad0d75ab0df747e11973a72246e8808c2d85b916 Mon Sep 17 00:00:00 2001
From: Blue
Date: Mon, 13 May 2024 14:11:03 +0500
Subject: [PATCH 36/82] feat: implement auto generated username experiment
(#1248)
* feat: implement auto generated username registration exp
---
src/config/index.js | 1 +
src/logistration/Logistration.test.jsx | 5 +
src/register/RegistrationPage.jsx | 203 ++++++++++--------
src/register/RegistrationPage.test.jsx | 5 +
.../ConfigurableRegistrationForm.test.jsx | 5 +
.../tests/RegistrationFailure.test.jsx | 5 +
.../components/tests/ThirdPartyAuth.test.jsx | 5 +
src/register/data/actions.js | 7 +
.../data/optimizelyExperiment/helper.js | 30 +++
...utoGeneratedUsernameExperimentVariation.js | 53 +++++
src/register/data/reducers.js | 8 +
src/register/data/tests/reducers.test.js | 1 +
12 files changed, 234 insertions(+), 94 deletions(-)
create mode 100644 src/register/data/optimizelyExperiment/helper.js
create mode 100644 src/register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation.js
diff --git a/src/config/index.js b/src/config/index.js
index badb6fe7..78cea882 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -35,6 +35,7 @@ const configuration = {
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
+ AUTO_GENERATED_USERNAME_EXPERIMENT_ID: process.env.AUTO_GENERATED_USERNAME_EXPERIMENT_ID || '',
};
export default configuration;
diff --git a/src/logistration/Logistration.test.jsx b/src/logistration/Logistration.test.jsx
index 87bf3e70..b16ae216 100644
--- a/src/logistration/Logistration.test.jsx
+++ b/src/logistration/Logistration.test.jsx
@@ -15,12 +15,16 @@ import {
} from '../data/constants';
import { backupLoginForm } from '../login/data/actions';
import { backupRegistrationForm } from '../register/data/actions';
+import { NOT_INITIALIZED } from '../register/data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation
+ from '../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
+jest.mock('../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
@@ -84,6 +88,7 @@ describe('Logistration', () => {
})),
}));
+ useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({
loggingService: { logError: jest.fn() },
config: {
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 8c5ea79f..22f12e14 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -18,6 +18,7 @@ import {
backupRegistrationFormBegin,
clearRegistrationBackendError,
registerNewUser,
+ setAutoGeneratedUsernameExperimentData,
setEmailSuggestionInStore,
setUserPipelineDataLoaded,
} from './data/actions';
@@ -25,6 +26,8 @@ import {
FORM_SUBMISSION_ERROR,
TPA_AUTHENTICATION_FAILURE,
} from './data/constants';
+import { AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION, NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import getBackendValidations from './data/selectors';
import {
isFormValid, prepareRegistrationPayload,
@@ -68,6 +71,7 @@ const RegistrationPage = (props) => {
} = props;
const backedUpFormData = useSelector(state => state.register.registrationFormData);
+ const initExpVariation = useSelector(state => state.register.autoGeneratedUsernameExperimentVariation);
const registrationError = useSelector(state => state.register.registrationError);
const registrationErrorCode = registrationError?.errorCode;
const registrationResult = useSelector(state => state.register.registrationResult);
@@ -103,6 +107,12 @@ const RegistrationPage = (props) => {
? formatMessage(messages['create.account.cta.button'], { label: cta })
: formatMessage(messages['create.account.for.free.button']);
+ const autoGeneratedUsernameExpVariation = useAutoGeneratedUsernameExperimentVariation(
+ initExpVariation, registrationEmbedded, tpaHint, currentProvider, thirdPartyAuthApiStatus,
+ );
+
+ const hideUsernameField = flags.autoGeneratedUsernameEnabled
+ || autoGeneratedUsernameExpVariation === AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION;
/**
* Set the userPipelineDetails data in formFields for only first time
*/
@@ -149,8 +159,10 @@ const RegistrationPage = (props) => {
formFields: { ...formFields },
errors: { ...errors },
}));
+ dispatch(setAutoGeneratedUsernameExperimentData(autoGeneratedUsernameExpVariation));
}
- }, [shouldBackupState, configurableFormFields, formFields, errors, dispatch, backedUpFormData]);
+ }, [shouldBackupState, configurableFormFields, // eslint-disable-line react-hooks/exhaustive-deps
+ formFields, errors, dispatch, backedUpFormData]);
useEffect(() => {
if (backendValidations) {
@@ -216,7 +228,7 @@ const RegistrationPage = (props) => {
delete payload.password;
payload.social_auth_provider = currentProvider;
}
- if (flags.autoGeneratedUsernameEnabled) {
+ if (hideUsernameField) {
delete payload.username;
}
@@ -286,106 +298,109 @@ const RegistrationPage = (props) => {
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
}
/>
- {autoSubmitRegForm && !errorCode.type ? (
-
-
-
- ) : (
-
-
-
-
+ ) : (
+
+
-
- {!flags.autoGeneratedUsernameEnabled && (
-
+
- )}
- {!currentProvider && (
-
- )}
-
- e.preventDefault()}
- />
- {!registrationEmbedded && (
-
+ )}
+ {!currentProvider && (
+
+ )}
+
- )}
-
-
- )}
-
+ e.preventDefault()}
+ />
+ {!registrationEmbedded && (
+
+ )}
+
+
+ )}
>
);
};
diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx
index 18e4e513..bec3ec60 100644
--- a/src/register/RegistrationPage.test.jsx
+++ b/src/register/RegistrationPage.test.jsx
@@ -17,6 +17,9 @@ import {
setUserPipelineDataLoaded,
} from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants';
+import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation
+ from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
@@ -30,6 +33,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('./data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -128,6 +132,7 @@ describe('RegistrationPage', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
index 3d664260..88d64253 100644
--- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
+++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
@@ -11,6 +11,9 @@ import configureStore from 'redux-mock-store';
import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants';
+import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation
+ from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
@@ -22,6 +25,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
@@ -121,6 +125,7 @@ describe('ConfigurableRegistrationForm', () => {
};
window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us'));
+ useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/RegistrationFailure.test.jsx b/src/register/components/tests/RegistrationFailure.test.jsx
index 003cc966..da8867cd 100644
--- a/src/register/components/tests/RegistrationFailure.test.jsx
+++ b/src/register/components/tests/RegistrationFailure.test.jsx
@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
} from '../../data/constants';
+import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation
+ from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import RegistrationFailureMessage from '../RegistrationFailure';
@@ -23,6 +26,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
@@ -121,6 +125,7 @@ describe('RegistrationFailure', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/components/tests/ThirdPartyAuth.test.jsx b/src/register/components/tests/ThirdPartyAuth.test.jsx
index 917f10f9..7706d185 100644
--- a/src/register/components/tests/ThirdPartyAuth.test.jsx
+++ b/src/register/components/tests/ThirdPartyAuth.test.jsx
@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants';
+import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
+import useAutoGeneratedUsernameExperimentVariation
+ from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -22,6 +25,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
+jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -120,6 +124,7 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false,
};
window.location = { search: '' };
+ useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
diff --git a/src/register/data/actions.js b/src/register/data/actions.js
index 68bfe9e6..2c122515 100644
--- a/src/register/data/actions.js
+++ b/src/register/data/actions.js
@@ -8,6 +8,7 @@ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERRO
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
+export const REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA = 'REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA';
// Backup registration form
export const backupRegistrationForm = () => ({
type: BACKUP_REGISTRATION_DATA.BASE,
@@ -82,3 +83,9 @@ export const setUserPipelineDataLoaded = (value) => ({
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
payload: { value },
});
+
+// Auto Generated Username Registration Experiment Actions
+export const setAutoGeneratedUsernameExperimentData = (autoGeneratedRegExpVariation) => ({
+ type: REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
+ payload: { autoGeneratedRegExpVariation },
+});
diff --git a/src/register/data/optimizelyExperiment/helper.js b/src/register/data/optimizelyExperiment/helper.js
new file mode 100644
index 00000000..0cc54183
--- /dev/null
+++ b/src/register/data/optimizelyExperiment/helper.js
@@ -0,0 +1,30 @@
+/**
+ * This file contains data for auto generated username Optimizely experiment
+ */
+import { getConfig } from '@edx/frontend-platform';
+
+export const NOT_INITIALIZED = 'experiment-not-initialized';
+export const CONTROL = 'control-registration-page';
+export const AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION = 'auto-generated-username-register-page';
+const AUTO_GENERATED_USERNAME_EXP_PAGE = 'targeting_for_auto_generated_username_page';
+
+export function getAutoGeneratedUsernameExperimentVariation() {
+ try {
+ if (window.optimizely
+ && window.optimizely.get('data').experiments[getConfig().AUTO_GENERATED_USERNAME_EXPERIMENT_ID]) {
+ const selectedVariant = window.optimizely.get('state').getVariationMap()[
+ getConfig().AUTO_GENERATED_USERNAME_EXPERIMENT_ID
+ ];
+ return selectedVariant?.name;
+ }
+ } catch (e) { /* empty */ }
+ return '';
+}
+
+export function activateAutoGeneratedUsernameExperiment() {
+ window.optimizely = window.optimizely || [];
+ window.optimizely.push({
+ type: 'page',
+ pageName: AUTO_GENERATED_USERNAME_EXP_PAGE,
+ });
+}
diff --git a/src/register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation.js b/src/register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation.js
new file mode 100644
index 00000000..5442a652
--- /dev/null
+++ b/src/register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation.js
@@ -0,0 +1,53 @@
+import { useEffect, useState } from 'react';
+
+import {
+ activateAutoGeneratedUsernameExperiment,
+ getAutoGeneratedUsernameExperimentVariation,
+ NOT_INITIALIZED,
+} from './helper';
+import { COMPLETE_STATE } from '../../../data/constants';
+
+/**
+ * This hook returns activates multi step registration experiment and returns the experiment
+ * variation for the user.
+ */
+const useAutoGeneratedUsernameExperimentVariation = (
+ initExpVariation,
+ registrationEmbedded,
+ tpaHint,
+ currentProvider,
+ thirdPartyAuthApiStatus,
+) => {
+ const [variation, setVariation] = useState(initExpVariation);
+ useEffect(() => {
+ if (initExpVariation || registrationEmbedded || !!tpaHint || !!currentProvider
+ || thirdPartyAuthApiStatus !== COMPLETE_STATE) {
+ return variation;
+ }
+
+ const getVariation = () => {
+ const expVariation = getAutoGeneratedUsernameExperimentVariation();
+ if (expVariation) {
+ setVariation(expVariation);
+ } else {
+ // This is to handle the case when user dont get variation for some reason, the register page
+ // shows unlimited spinner.
+ setVariation(NOT_INITIALIZED);
+ }
+ };
+
+ activateAutoGeneratedUsernameExperiment();
+
+ const timer = setTimeout(getVariation, 300);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [ // eslint-disable-line react-hooks/exhaustive-deps
+ initExpVariation, currentProvider, registrationEmbedded, thirdPartyAuthApiStatus, tpaHint,
+ ]);
+
+ return variation;
+};
+
+export default useAutoGeneratedUsernameExperimentVariation;
diff --git a/src/register/data/reducers.js b/src/register/data/reducers.js
index 70c3a994..d8a822df 100644
--- a/src/register/data/reducers.js
+++ b/src/register/data/reducers.js
@@ -3,6 +3,7 @@ import {
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTER_FORM_VALIDATIONS,
REGISTER_NEW_USER,
+ REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS,
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
@@ -39,6 +40,7 @@ export const defaultState = {
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
+ autoGeneratedUsernameExperimentVariation: '',
};
const reducer = (state = defaultState, action = {}) => {
@@ -55,6 +57,12 @@ const reducer = (state = defaultState, action = {}) => {
registrationFormData: { ...action.payload },
userPipelineDataLoaded: state.userPipelineDataLoaded,
};
+ case REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA: {
+ return {
+ ...state,
+ autoGeneratedUsernameExperimentVariation: action.payload.autoGeneratedRegExpVariation,
+ };
+ }
case REGISTER_NEW_USER.BEGIN:
return {
...state,
diff --git a/src/register/data/tests/reducers.test.js b/src/register/data/tests/reducers.test.js
index 3e2270ab..39fa4cee 100644
--- a/src/register/data/tests/reducers.test.js
+++ b/src/register/data/tests/reducers.test.js
@@ -38,6 +38,7 @@ describe('Registration Reducer Tests', () => {
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
+ autoGeneratedUsernameExperimentVariation: '',
};
it('should return the initial state', () => {
From 1819edc9b793fa8725d1a154d27c33faf25a7bd1 Mon Sep 17 00:00:00 2001
From: Blue
Date: Tue, 4 Jun 2024 16:27:14 +0500
Subject: [PATCH 37/82] feat: add page event for reset password (#1253)
Description: Add page event for reset password page
VAN-1929
---
src/forgot-password/ForgotPasswordPage.jsx | 2 +-
src/reset-password/ResetPasswordPage.jsx | 5 +++++
src/reset-password/tests/ResetPasswordPage.test.jsx | 4 ++++
3 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/forgot-password/ForgotPasswordPage.jsx
index ff381340..f4c7b314 100644
--- a/src/forgot-password/ForgotPasswordPage.jsx
+++ b/src/forgot-password/ForgotPasswordPage.jsx
@@ -41,7 +41,7 @@ const ForgotPasswordPage = (props) => {
const navigate = useNavigate();
useEffect(() => {
- sendPageEvent('login_and_registration', 'reset');
+ sendPageEvent('login_and_registration', 'forgot-password');
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
}, []);
diff --git a/src/reset-password/ResetPasswordPage.jsx b/src/reset-password/ResetPasswordPage.jsx
index 9b4b6758..0393d279 100644
--- a/src/reset-password/ResetPasswordPage.jsx
+++ b/src/reset-password/ResetPasswordPage.jsx
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
+import { sendPageEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form,
@@ -42,6 +43,10 @@ const ResetPasswordPage = (props) => {
const { token } = useParams();
const navigate = useNavigate();
+ useEffect(() => {
+ sendPageEvent('login_and_registration', 'reset-password');
+ }, []);
+
useEffect(() => {
if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) {
setErrorCode(props.status);
diff --git a/src/reset-password/tests/ResetPasswordPage.test.jsx b/src/reset-password/tests/ResetPasswordPage.test.jsx
index 421edc9e..0c9ba1a2 100644
--- a/src/reset-password/tests/ResetPasswordPage.test.jsx
+++ b/src/reset-password/tests/ResetPasswordPage.test.jsx
@@ -19,6 +19,10 @@ import ResetPasswordPage from '../ResetPasswordPage';
const mockedNavigator = jest.fn();
const token = '1c-bmjdkc-5e60e084cf8113048ca7';
+jest.mock('@edx/frontend-platform/analytics', () => ({
+ sendPageEvent: jest.fn(),
+}));
+
jest.mock('@edx/frontend-platform/auth');
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')),
From 739f94d624e22c359453f2b79500a84c1df320d1 Mon Sep 17 00:00:00 2001
From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com>
Date: Fri, 7 Jun 2024 07:57:42 +0500
Subject: [PATCH 38/82] Update 2u-main with master (#1254)
* feat: Hide preloaders for third party auth providers if they are disabled
* feat: remove username from the registration from (#1201) (#1241)
Co-authored-by: Attiya Ishaque
* fix: add new entry for another US label (#1244)
Add new entry for for another US label which is United States
* feat: implement multi step registration experiment
Rebase 2u main with master (#1228)
* chore(deps): update dependency babel-plugin-formatjs to v10.5.14
* fix(deps): update dependency @edx/frontend-platform to v7.1.3
* fix(deps): update font awesome to v6.5.2
* chore(deps): update dependency @openedx/frontend-build to v13.1.4
* fix(deps): update dependency @openedx/paragon to v22.2.1
* fix(deps): update dependency algoliasearch to v4.23.3
* fix(deps): update dependency algoliasearch-helper to v3.17.0
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* feat: add multi step registration eventing (#1226)
* feat: implement multi step registration experiment
* feat: add multi step registration eventing
* fix: fix register button width
* fix: fix register button loader for control
* feat: capture marketing lead in experiment events (#1243)
* revert: multistep registration experiment
revert multistep registration experiment changes
VAN-1930
* feat: implement auto generated username experiment (#1248)
* feat: implement auto generated username registration exp
* feat: add page event for reset password (#1253)
Description: Add page event for reset password page
VAN-1929
---------
Co-authored-by: Stanislav Lunyachek
Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
Co-authored-by: Attiya Ishaque
Co-authored-by: Blue
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syed Sajjad Hussain Shah
From 4898864416b0fdeb97f79a9907f1b76ab862e89b Mon Sep 17 00:00:00 2001
From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com>
Date: Tue, 11 Jun 2024 12:01:30 +0500
Subject: [PATCH 39/82] feat: hard code fields on frontend (#1256)
* feat: hard code fields
hard code configurable fields on frontend which includes country field on register page & level of education & gender field on progressive profiling
VAN-1971
* fix: fix secondary provider null name issue
---
.../InstitutionLogistration.jsx | 2 +-
src/common-components/data/constants.js | 79 +++++++++++++++++++
src/common-components/data/sagas.js | 13 ++-
src/config/index.js | 1 +
4 files changed, 93 insertions(+), 2 deletions(-)
create mode 100644 src/common-components/data/constants.js
diff --git a/src/common-components/InstitutionLogistration.jsx b/src/common-components/InstitutionLogistration.jsx
index b773bc6e..ba26cf3c 100644
--- a/src/common-components/InstitutionLogistration.jsx
+++ b/src/common-components/InstitutionLogistration.jsx
@@ -60,7 +60,7 @@ const InstitutionLogistration = props => {
className="btn nav-item p-0 mb-1 institutions--provider-link"
destination={lmsBaseUrl + provider.loginUrl}
>
- {provider.name}
+ {provider?.name}
diff --git a/src/common-components/data/constants.js b/src/common-components/data/constants.js
new file mode 100644
index 00000000..4f9ec72c
--- /dev/null
+++ b/src/common-components/data/constants.js
@@ -0,0 +1,79 @@
+export const registerFields = {
+ fields: {
+ country: {
+ name: 'country',
+ error_message: 'Select your country or region of residence',
+ },
+ honor_code: {
+ name: 'honor_code',
+ type: 'tos_and_honor_code',
+ error_message: '',
+ },
+ },
+};
+
+export const progressiveProfilingFields = {
+ extended_profile: [],
+ fields: {
+ level_of_education: {
+ name: 'level_of_education',
+ type: 'select',
+ label: 'Highest level of education completed',
+ error_message: '',
+ options: [
+ [
+ 'p',
+ 'Doctorate',
+ ],
+ [
+ 'm',
+ "Master's or professional degree",
+ ],
+ [
+ 'b',
+ "Bachelor's degree",
+ ],
+ [
+ 'a',
+ 'Associate degree',
+ ],
+ [
+ 'hs',
+ 'Secondary/high school',
+ ],
+ [
+ 'jhs',
+ 'Junior secondary/junior high/middle school',
+ ],
+ [
+ 'none',
+ 'No formal education',
+ ],
+ [
+ 'other',
+ 'Other education',
+ ],
+ ],
+ },
+ gender: {
+ name: 'gender',
+ type: 'select',
+ label: 'Gender',
+ error_message: '',
+ options: [
+ [
+ 'm',
+ 'Male',
+ ],
+ [
+ 'f',
+ 'Female',
+ ],
+ [
+ 'o',
+ 'Other/Prefer Not to Say',
+ ],
+ ],
+ },
+ },
+};
diff --git a/src/common-components/data/sagas.js b/src/common-components/data/sagas.js
index ffe0be37..65105866 100644
--- a/src/common-components/data/sagas.js
+++ b/src/common-components/data/sagas.js
@@ -1,3 +1,4 @@
+import { getConfig } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
@@ -7,6 +8,7 @@ import {
getThirdPartyAuthContextSuccess,
THIRD_PARTY_AUTH_CONTEXT,
} from './actions';
+import { progressiveProfilingFields, registerFields } from './constants';
import {
getThirdPartyAuthContext,
} from './service';
@@ -20,7 +22,16 @@ export function* fetchThirdPartyAuthContext(action) {
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
- yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
+ // hard code country field, level of education and gender fields
+ if (getConfig().ENABLE_HARD_CODE_OPTIONAL_FIELDS) {
+ yield put(getThirdPartyAuthContextSuccess(
+ registerFields,
+ progressiveProfilingFields,
+ thirdPartyAuthContext,
+ ));
+ } else {
+ yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
+ }
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);
diff --git a/src/config/index.js b/src/config/index.js
index 78cea882..6399b549 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -11,6 +11,7 @@ const configuration = {
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
SHOW_REGISTRATION_LINKS: process.env.SHOW_REGISTRATION_LINKS !== 'false',
+ ENABLE_HARD_CODE_OPTIONAL_FIELDS: process.env.ENABLE_HARD_CODE_OPTIONAL_FIELDS || false,
ENABLE_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false,
// Links
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
From afd4d243604b20676c7e2935ffe69973e1bcbb70 Mon Sep 17 00:00:00 2001
From: Muhammad Abdullah Waheed
<42172960+abdullahwaheed@users.noreply.github.com>
Date: Wed, 3 Jul 2024 17:08:44 +0500
Subject: [PATCH 40/82] feat: added app name identifier in segment events
(#1277)
* feat: added app name identifier in registration call
* feat: added utils for tracking events
* refactor: mapped login events
* refactor: mapped forgot password events
* refactor: mapped reset password events
* refactor: mapped register events
* fix: fixed unit tests
* refactor: mapped progressive prifiling events
* fix: fixed unit tests
* refactor: added app name in logistration events
* refactor: resolved PR reviews and fixed tests
---
src/data/constants.js | 1 +
src/data/segment/utils.js | 37 ++++++++++++++++
src/forgot-password/ForgotPasswordPage.jsx | 9 ++--
src/login/LoginPage.jsx | 15 ++++---
src/login/tests/LoginPage.test.jsx | 8 ++--
src/logistration/Logistration.jsx | 10 ++---
src/logistration/Logistration.test.jsx | 5 ++-
.../ProgressiveProfiling.jsx | 44 +++++++++----------
.../tests/ProgressiveProfiling.test.jsx | 10 +++--
src/register/RegistrationPage.jsx | 10 ++---
src/register/RegistrationPage.test.jsx | 10 +++--
.../ConfigurableRegistrationForm.test.jsx | 3 +-
src/reset-password/ResetPasswordPage.jsx | 15 ++++---
.../tests/ResetPasswordPage.test.jsx | 1 +
src/tracking/trackers/forgotpassword.js | 22 ++++++++++
src/tracking/trackers/login.js | 29 ++++++++++++
.../trackers/progressive-profiling.js | 37 ++++++++++++++++
src/tracking/trackers/register.js | 22 ++++++++++
src/tracking/trackers/reset-password.js | 14 ++++++
.../trackers/tests/forgot-password.test.jsx | 37 ++++++++++++++++
src/tracking/trackers/tests/login.test.jsx | 37 ++++++++++++++++
.../tests/progressive-profiling.test.jsx | 37 ++++++++++++++++
src/tracking/trackers/tests/register.test.jsx | 36 +++++++++++++++
.../trackers/tests/reset-password.test.jsx | 26 +++++++++++
24 files changed, 418 insertions(+), 57 deletions(-)
create mode 100644 src/data/segment/utils.js
create mode 100644 src/tracking/trackers/forgotpassword.js
create mode 100644 src/tracking/trackers/login.js
create mode 100644 src/tracking/trackers/progressive-profiling.js
create mode 100644 src/tracking/trackers/register.js
create mode 100644 src/tracking/trackers/reset-password.js
create mode 100644 src/tracking/trackers/tests/forgot-password.test.jsx
create mode 100644 src/tracking/trackers/tests/login.test.jsx
create mode 100644 src/tracking/trackers/tests/progressive-profiling.test.jsx
create mode 100644 src/tracking/trackers/tests/register.test.jsx
create mode 100644 src/tracking/trackers/tests/reset-password.test.jsx
diff --git a/src/data/constants.js b/src/data/constants.js
index 90fdf75f..5adf4382 100644
--- a/src/data/constants.js
+++ b/src/data/constants.js
@@ -37,3 +37,4 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
// things like auto-enrollment upon login and registration.
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta'];
export const REDIRECT = 'redirect';
+export const APP_NAME = 'authn_mfe';
diff --git a/src/data/segment/utils.js b/src/data/segment/utils.js
new file mode 100644
index 00000000..fa84443d
--- /dev/null
+++ b/src/data/segment/utils.js
@@ -0,0 +1,37 @@
+/* eslint-disable import/prefer-default-export */
+import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
+
+import { APP_NAME } from '../constants';
+
+export const LINK_TIMEOUT = 300;
+
+/**
+ * Creates an event tracker function that sends a tracking event with the given name and options.
+ *
+ * @param {string} name - The name of the event to be tracked.
+ * @param {object} [options={}] - Additional options to be included with the event.
+ * @returns {function} - A function that, when called, sends the tracking event.
+ */
+export const createEventTracker = (name, options = {}) => () => sendTrackEvent(
+ name,
+ { ...options, app_name: APP_NAME },
+);
+
+/**
+ * Creates an event tracker function that sends a tracking event with the given name and options.
+ *
+ * @param {string} name - The name of the event to be tracked.
+ * @param {object} [options={}] - Additional options to be included with the event.
+ * @returns {function} - A function that, when called, sends the tracking event.
+ */
+export const createPageEventTracker = (name, options = null) => () => sendPageEvent(
+ name,
+ options,
+ { app_name: APP_NAME },
+);
+
+export const createLinkTracker = (tracker, href) => (e) => {
+ e.preventDefault();
+ tracker();
+ return setTimeout(() => { window.location.href = href; }, LINK_TIMEOUT);
+};
diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/forgot-password/ForgotPasswordPage.jsx
index f4c7b314..fad31300 100644
--- a/src/forgot-password/ForgotPasswordPage.jsx
+++ b/src/forgot-password/ForgotPasswordPage.jsx
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
-import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form,
@@ -25,6 +24,10 @@ import BaseContainer from '../base-container';
import { FormGroup } from '../common-components';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
+import {
+ trackForgotPasswordPageEvent,
+ trackForgotPasswordPageViewed,
+} from '../tracking/trackers/forgotpassword';
const ForgotPasswordPage = (props) => {
const platformName = getConfig().SITE_NAME;
@@ -41,8 +44,8 @@ const ForgotPasswordPage = (props) => {
const navigate = useNavigate();
useEffect(() => {
- sendPageEvent('login_and_registration', 'forgot-password');
- sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
+ trackForgotPasswordPageEvent();
+ trackForgotPasswordPageViewed();
}, []);
useEffect(() => {
diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx
index 62815726..0ff25258 100644
--- a/src/login/LoginPage.jsx
+++ b/src/login/LoginPage.jsx
@@ -2,7 +2,6 @@ import React, { useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
-import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import {
Form, StatefulButton,
@@ -43,6 +42,9 @@ import {
updatePathWithQueryParams,
} from '../data/utils';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
+import {
+ trackForgotPasswordLinkClick, trackLoginPageViewed, trackLoginSuccess,
+} from '../tracking/trackers/login';
const LoginPage = (props) => {
const {
@@ -78,9 +80,15 @@ const LoginPage = (props) => {
const tpaHint = getTpaHint();
useEffect(() => {
- sendPageEvent('login_and_registration', 'login');
+ trackLoginPageViewed();
}, []);
+ useEffect(() => {
+ if (loginResult.success) {
+ trackLoginSuccess();
+ }
+ }, [loginResult]);
+
useEffect(() => {
const payload = { ...queryParams };
if (tpaHint) {
@@ -170,9 +178,6 @@ const LoginPage = (props) => {
const { name } = event.target;
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
};
- const trackForgotPasswordLinkClick = () => {
- sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
- };
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
diff --git a/src/login/tests/LoginPage.test.jsx b/src/login/tests/LoginPage.test.jsx
index 9c337bf2..38778b8c 100644
--- a/src/login/tests/LoginPage.test.jsx
+++ b/src/login/tests/LoginPage.test.jsx
@@ -11,7 +11,9 @@ import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
-import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants';
+import {
+ APP_NAME, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE,
+} from '../../data/constants';
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions';
import { INTERNAL_SERVER_ERROR } from '../data/constants';
import LoginPage from '../LoginPage';
@@ -751,7 +753,7 @@ describe('LoginPage', () => {
it('should send page event when login page is rendered', () => {
render(reduxWrapper());
- expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
+ expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login', { app_name: APP_NAME });
});
it('tests that form is in invalid state when it is submitted', () => {
@@ -784,7 +786,7 @@ describe('LoginPage', () => {
{ selector: '#forgot-password' },
));
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
});
it('should backup the login form state when shouldBackupState is true', () => {
diff --git a/src/logistration/Logistration.jsx b/src/logistration/Logistration.jsx
index 9451aa27..1a65b259 100644
--- a/src/logistration/Logistration.jsx
+++ b/src/logistration/Logistration.jsx
@@ -20,7 +20,7 @@ import {
tpaProvidersSelector,
} from '../common-components/data/selectors';
import messages from '../common-components/messages';
-import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
+import { APP_NAME, LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import {
getTpaHint, getTpaProvider, updatePathWithQueryParams,
} from '../data/utils';
@@ -56,11 +56,11 @@ const Logistration = (props) => {
}, [navigate, disablePublicAccountCreation]);
const handleInstitutionLogin = (e) => {
- sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
+ sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
if (typeof e === 'string') {
- sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register');
+ sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register', { app_name: APP_NAME });
} else {
- sendPageEvent('login_and_registration', e.target.dataset.eventName);
+ sendPageEvent('login_and_registration', e.target.dataset.eventName, { app_name: APP_NAME });
}
setInstitutionLogin(!institutionLogin);
@@ -70,7 +70,7 @@ const Logistration = (props) => {
if (tabKey === currentTab) {
return;
}
- sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
+ sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement', app_name: APP_NAME });
props.clearThirdPartyAuthContextErrorMessage();
if (tabKey === LOGIN_PAGE) {
props.backupRegistrationForm();
diff --git a/src/logistration/Logistration.test.jsx b/src/logistration/Logistration.test.jsx
index b16ae216..b4b0d1e9 100644
--- a/src/logistration/Logistration.test.jsx
+++ b/src/logistration/Logistration.test.jsx
@@ -11,6 +11,7 @@ import configureStore from 'redux-mock-store';
import Logistration from './Logistration';
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import {
+ APP_NAME,
COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE,
} from '../data/constants';
import { backupLoginForm } from '../login/data/actions';
@@ -229,8 +230,8 @@ describe('Logistration', () => {
render(reduxWrapper());
fireEvent.click(screen.getByText('Institution/campus credentials'));
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
- expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
+ expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login', { app_name: APP_NAME });
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
diff --git a/src/progressive-profiling/ProgressiveProfiling.jsx b/src/progressive-profiling/ProgressiveProfiling.jsx
index d42e3d52..55b96462 100644
--- a/src/progressive-profiling/ProgressiveProfiling.jsx
+++ b/src/progressive-profiling/ProgressiveProfiling.jsx
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
-import { identifyAuthenticatedUser, sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
+import { identifyAuthenticatedUser } from '@edx/frontend-platform/analytics';
import {
AxiosJwtAuthService,
configure as configureAuth,
@@ -39,6 +39,13 @@ import {
import isOneTrustFunctionalCookieEnabled from '../data/oneTrust';
import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils';
import { FormFieldRenderer } from '../field-renderer';
+import {
+ trackDisablePostRegistrationRecommendations,
+ trackProgressiveProfilingPageViewed,
+ trackProgressiveProfilingSkipLinkClick,
+ trackProgressiveProfilingSubmitClick,
+ trackProgressiveProfilingSupportLinkCLick,
+} from '../tracking/trackers/progressive-profiling';
const ProgressiveProfiling = (props) => {
const { formatMessage } = useIntl();
@@ -98,14 +105,13 @@ const ProgressiveProfiling = (props) => {
useEffect(() => {
if (authenticatedUser?.userId) {
identifyAuthenticatedUser(authenticatedUser.userId);
- sendPageEvent('login_and_registration', 'welcome');
+ trackProgressiveProfilingPageViewed();
}
}, [authenticatedUser]);
useEffect(() => {
if (!enablePostRegistrationRecommendations) {
- sendTrackEvent(
- 'edx.bi.user.recommendations.not.enabled',
+ trackDisablePostRegistrationRecommendations(
{ functionalCookiesConsent, page: 'authn_recommendations' },
);
return;
@@ -149,29 +155,23 @@ const ProgressiveProfiling = (props) => {
});
}
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
-
- sendTrackEvent(
- 'edx.bi.welcome.page.submit.clicked',
- {
- isGenderSelected: !!values.gender,
- isYearOfBirthSelected: !!values.year_of_birth,
- isLevelOfEducationSelected: !!values.level_of_education,
- isWorkExperienceSelected: !!values.work_experience,
- host: queryParams?.host || '',
- },
- );
+ const eventProperties = {
+ isGenderSelected: !!values.gender,
+ isYearOfBirthSelected: !!values.year_of_birth,
+ isLevelOfEducationSelected: !!values.level_of_education,
+ isWorkExperienceSelected: !!values.work_experience,
+ host: queryParams?.host || '',
+ };
+ trackProgressiveProfilingSubmitClick(eventProperties);
};
const handleSkip = (e) => {
e.preventDefault();
window.history.replaceState(location.state, null, '');
setShowModal(true);
- sendTrackEvent(
- 'edx.bi.welcome.page.skip.link.clicked',
- {
- host: queryParams?.host || '',
- },
- );
+ trackProgressiveProfilingSkipLinkClick({
+ host: queryParams?.host || '',
+ });
};
const onChangeHandler = (e) => {
@@ -242,7 +242,7 @@ const ProgressiveProfiling = (props) => {
destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
target="_blank"
showLaunchIcon={false}
- onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
+ onClick={() => (trackProgressiveProfilingSupportLinkCLick())}
>
{formatMessage(messages['optional.fields.information.link'])}
diff --git a/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx b/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx
index 9bc4439c..c5786b2e 100644
--- a/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx
+++ b/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx
@@ -12,6 +12,7 @@ import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
+ APP_NAME,
AUTHN_PROGRESSIVE_PROFILING,
COMPLETE_STATE, DEFAULT_REDIRECT_URL,
EMBEDDED,
@@ -143,8 +144,9 @@ describe('ProgressiveProfilingTests', () => {
const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container');
expect(modalContentContainer).toBeTruthy();
+ const payload = { host: '', app_name: APP_NAME };
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host: '' });
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', payload);
});
// ******** test event functionality ********
@@ -165,7 +167,7 @@ describe('ProgressiveProfilingTests', () => {
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
fireEvent.click(supportLink);
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked');
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked', { app_name: APP_NAME });
});
it('should set empty host property value for non-embedded experience', () => {
@@ -175,6 +177,7 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false,
isWorkExperienceSelected: false,
host: '',
+ app_name: APP_NAME,
};
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
@@ -316,7 +319,7 @@ describe('ProgressiveProfilingTests', () => {
const skipLinkButton = screen.getByText('Skip for now');
fireEvent.click(skipLinkButton);
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host });
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host, app_name: APP_NAME });
});
it('should show spinner while fetching the optional fields', () => {
@@ -349,6 +352,7 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false,
isWorkExperienceSelected: false,
host: 'http://example.com',
+ app_name: APP_NAME,
};
delete window.location;
window.location = {
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 22f12e14..0a0a5b53 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -4,7 +4,6 @@ import React, {
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
-import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form, Spinner, StatefulButton } from '@openedx/paragon';
import classNames from 'classnames';
@@ -44,11 +43,12 @@ import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../c
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import {
- COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
+ APP_NAME, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
import {
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
} from '../data/utils';
+import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
/**
* Main Registration Page component
@@ -138,7 +138,7 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (!formStartTime) {
- sendPageEvent('login_and_registration', 'register');
+ trackRegistrationPageViewed();
const payload = { ...queryParams, is_register_page: true };
if (tpaHint) {
payload.tpa_hint = tpaHint;
@@ -183,7 +183,7 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (registrationResult.success) {
// This event is used by GTM
- sendTrackEvent('edx.bi.user.account.registered.client', {});
+ trackRegistrationSuccess();
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
@@ -222,7 +222,7 @@ const RegistrationPage = (props) => {
const registerUser = () => {
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
- let payload = { ...formFields };
+ let payload = { ...formFields, app_name: APP_NAME };
if (currentProvider) {
delete payload.password;
diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx
index bec3ec60..a98d55d5 100644
--- a/src/register/RegistrationPage.test.jsx
+++ b/src/register/RegistrationPage.test.jsx
@@ -22,7 +22,7 @@ import useAutoGeneratedUsernameExperimentVariation
from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
- AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
+ APP_NAME, AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -190,6 +190,7 @@ describe('RegistrationPage', () => {
honor_code: true,
total_registration_time: 0,
next: '/course/demo-course-url',
+ app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -212,6 +213,7 @@ describe('RegistrationPage', () => {
honor_code: true,
social_auth_provider: 'Apple',
total_registration_time: 0,
+ app_name: APP_NAME,
};
store = mockStore({
@@ -297,6 +299,7 @@ describe('RegistrationPage', () => {
honor_code: true,
total_registration_time: 0,
marketing_emails_opt_in: true,
+ app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -323,6 +326,7 @@ describe('RegistrationPage', () => {
country: 'Pakistan',
honor_code: true,
total_registration_time: 0,
+ app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -597,7 +601,7 @@ describe('RegistrationPage', () => {
it('should send page event when register page is rendered', () => {
render(routerWrapper(reduxWrapper()));
- expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
+ expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register', { app_name: APP_NAME });
});
it('should send track event when user has successfully registered', () => {
@@ -615,7 +619,7 @@ describe('RegistrationPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper()));
- expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {});
+ expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', { app_name: APP_NAME });
});
it('should populate form with pipeline user details', () => {
diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
index 88d64253..61a25848 100644
--- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
+++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
@@ -9,6 +9,7 @@ import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
+import { APP_NAME } from '../../../data/constants';
import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
@@ -265,7 +266,7 @@ describe('ConfigurableRegistrationForm', () => {
fireEvent.click(submitButton);
- expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
+ expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK', app_name: APP_NAME }));
});
it('should show error messages for required fields on empty form submission', () => {
diff --git a/src/reset-password/ResetPasswordPage.jsx b/src/reset-password/ResetPasswordPage.jsx
index 0393d279..ceb37e8c 100644
--- a/src/reset-password/ResetPasswordPage.jsx
+++ b/src/reset-password/ResetPasswordPage.jsx
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
-import { sendPageEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form,
@@ -19,7 +18,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { resetPassword, validateToken } from './data/actions';
import {
- FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, TOKEN_STATE,
+ FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, SUCCESS, TOKEN_STATE,
} from './data/constants';
import { resetPasswordResultSelector } from './data/selectors';
import { validatePassword } from './data/service';
@@ -31,6 +30,7 @@ import {
LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE,
} from '../data/constants';
import { getAllPossibleQueryParams, updatePathWithQueryParams, windowScrollTo } from '../data/utils';
+import { trackPasswordResetSuccess, trackResetPasswordPageViewed } from '../tracking/trackers/reset-password';
const ResetPasswordPage = (props) => {
const { formatMessage } = useIntl();
@@ -44,8 +44,13 @@ const ResetPasswordPage = (props) => {
const navigate = useNavigate();
useEffect(() => {
- sendPageEvent('login_and_registration', 'reset-password');
- }, []);
+ if (props.status === TOKEN_STATE.VALID) {
+ trackResetPasswordPageViewed();
+ }
+ if (props.status === SUCCESS) {
+ trackPasswordResetSuccess();
+ }
+ }, [props.status]);
useEffect(() => {
if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) {
@@ -144,7 +149,7 @@ const ResetPasswordPage = (props) => {
}
} else if (props.status === PASSWORD_RESET_ERROR) {
navigate(updatePathWithQueryParams(RESET_PAGE));
- } else if (props.status === 'success') {
+ } else if (props.status === SUCCESS) {
navigate(updatePathWithQueryParams(LOGIN_PAGE));
} else {
return (
diff --git a/src/reset-password/tests/ResetPasswordPage.test.jsx b/src/reset-password/tests/ResetPasswordPage.test.jsx
index 0c9ba1a2..f3619be7 100644
--- a/src/reset-password/tests/ResetPasswordPage.test.jsx
+++ b/src/reset-password/tests/ResetPasswordPage.test.jsx
@@ -21,6 +21,7 @@ const token = '1c-bmjdkc-5e60e084cf8113048ca7';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
+ sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
diff --git a/src/tracking/trackers/forgotpassword.js b/src/tracking/trackers/forgotpassword.js
new file mode 100644
index 00000000..3918478d
--- /dev/null
+++ b/src/tracking/trackers/forgotpassword.js
@@ -0,0 +1,22 @@
+import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
+
+export const eventNames = {
+ loginAndRegistration: 'login_and_registration',
+ forgotPasswordPageViewed: 'edx.bi.password_reset_form.viewed',
+};
+
+export const categories = {
+ userEngagement: 'user-engagement',
+};
+
+// Event tracker for forgot password page viewed
+export const trackForgotPasswordPageViewed = () => createEventTracker(
+ eventNames.forgotPasswordPageViewed,
+ {
+ category: categories.userEngagement,
+ },
+)();
+
+export const trackForgotPasswordPageEvent = () => {
+ createPageEventTracker(eventNames.loginAndRegistration, 'forgot-password')();
+};
diff --git a/src/tracking/trackers/login.js b/src/tracking/trackers/login.js
new file mode 100644
index 00000000..3c6a4a2a
--- /dev/null
+++ b/src/tracking/trackers/login.js
@@ -0,0 +1,29 @@
+import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
+
+export const eventNames = {
+ forgotPasswordLinkClicked: 'edx.bi.password-reset_form.toggled',
+ loginAndRegistration: 'login_and_registration',
+ registerFormToggled: 'edx.bi.register_form.toggled',
+ loginSuccess: 'edx.bi.user.account.authenticated.client',
+};
+
+export const categories = {
+ userEngagement: 'user-engagement',
+};
+
+// Event tracker for Forgot Password link click
+export const trackForgotPasswordLinkClick = () => createEventTracker(
+ eventNames.forgotPasswordLinkClicked,
+ { category: categories.userEngagement },
+)();
+
+// Tracks the login page event.
+export const trackLoginPageViewed = () => {
+ createPageEventTracker(eventNames.loginAndRegistration, 'login')();
+};
+
+// Tracks the login sucess event.
+export const trackLoginSuccess = () => createEventTracker(
+ eventNames.loginSuccess,
+ {},
+)();
diff --git a/src/tracking/trackers/progressive-profiling.js b/src/tracking/trackers/progressive-profiling.js
new file mode 100644
index 00000000..11a6f53e
--- /dev/null
+++ b/src/tracking/trackers/progressive-profiling.js
@@ -0,0 +1,37 @@
+import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
+
+export const eventNames = {
+ progressiveProfilingSubmitClick: 'edx.bi.welcome.page.submit.clicked',
+ progressiveProfilingSkipLinkClick: 'edx.bi.welcome.page.skip.link.clicked',
+ disablePostRegistrationRecommendations: 'edx.bi.user.recommendations.not.enabled',
+ progressiveProfilingSupportLinkCLick: 'edx.bi.welcome.page.support.link.clicked',
+ loginAndRegistration: 'login_and_registration',
+};
+
+// Event link tracker for Progressive profiling skip button click
+export const trackProgressiveProfilingSkipLinkClick = evenProperties => createEventTracker(
+ eventNames.progressiveProfilingSkipLinkClick, { ...evenProperties },
+)();
+
+// Event tracker for progressive profiling submit button click
+export const trackProgressiveProfilingSubmitClick = (evenProperties) => createEventTracker(
+ eventNames.progressiveProfilingSubmitClick,
+ { ...evenProperties },
+)();
+
+// Event tracker for progressive profiling submit button click
+export const trackDisablePostRegistrationRecommendations = (evenProperties) => createEventTracker(
+ eventNames.disablePostRegistrationRecommendations,
+ { ...evenProperties },
+)();
+
+// Tracks the progressive profiling page event.
+export const trackProgressiveProfilingPageViewed = () => {
+ createPageEventTracker(eventNames.loginAndRegistration, 'welcome')();
+};
+
+// Tracks the progressive profiling spport link click.
+export const trackProgressiveProfilingSupportLinkCLick = () => createEventTracker(
+ eventNames.progressiveProfilingSupportLinkCLick,
+ {},
+)();
diff --git a/src/tracking/trackers/register.js b/src/tracking/trackers/register.js
new file mode 100644
index 00000000..3a860856
--- /dev/null
+++ b/src/tracking/trackers/register.js
@@ -0,0 +1,22 @@
+import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
+
+export const eventNames = {
+ loginAndRegistration: 'login_and_registration',
+ registrationSuccess: 'edx.bi.user.account.registered.client',
+ loginFormToggled: 'edx.bi.login_form.toggled',
+};
+
+export const categories = {
+ userEngagement: 'user-engagement',
+};
+
+// Event tracker for successful registration
+export const trackRegistrationSuccess = () => createEventTracker(
+ eventNames.registrationSuccess,
+ {},
+)();
+
+// Tracks the progressive profiling page event.
+export const trackRegistrationPageViewed = () => {
+ createPageEventTracker(eventNames.loginAndRegistration, 'register')();
+};
diff --git a/src/tracking/trackers/reset-password.js b/src/tracking/trackers/reset-password.js
new file mode 100644
index 00000000..cb79340f
--- /dev/null
+++ b/src/tracking/trackers/reset-password.js
@@ -0,0 +1,14 @@
+import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
+
+export const eventNames = {
+ loginAndRegistration: 'login_and_registration',
+ resetPasswordSuccess: 'edx.bi.user.password.reset.success',
+};
+
+export const trackResetPasswordPageViewed = () => {
+ createPageEventTracker(eventNames.loginAndRegistration, 'reset-password')();
+};
+
+export const trackPasswordResetSuccess = () => {
+ createEventTracker(eventNames.resetPasswordSuccess, {})();
+};
diff --git a/src/tracking/trackers/tests/forgot-password.test.jsx b/src/tracking/trackers/tests/forgot-password.test.jsx
new file mode 100644
index 00000000..6a9b87c4
--- /dev/null
+++ b/src/tracking/trackers/tests/forgot-password.test.jsx
@@ -0,0 +1,37 @@
+import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
+import {
+ categories,
+ eventNames,
+ trackForgotPasswordPageEvent,
+ trackForgotPasswordPageViewed,
+} from '../forgotpassword';
+
+// Mock createEventTracker function
+jest.mock('../../../data/segment/utils', () => ({
+ createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+}));
+
+describe('Tracking Functions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should fire trackForgotPasswordPageEvent', () => {
+ trackForgotPasswordPageEvent();
+
+ expect(createPageEventTracker).toHaveBeenCalledWith(
+ eventNames.loginAndRegistration,
+ 'forgot-password',
+ );
+ });
+
+ it('should fire forgotPasswordPageViewedEvent', () => {
+ trackForgotPasswordPageViewed();
+
+ expect(createEventTracker).toHaveBeenCalledWith(
+ eventNames.forgotPasswordPageViewed,
+ { category: categories.userEngagement },
+ );
+ });
+});
diff --git a/src/tracking/trackers/tests/login.test.jsx b/src/tracking/trackers/tests/login.test.jsx
new file mode 100644
index 00000000..fac7d082
--- /dev/null
+++ b/src/tracking/trackers/tests/login.test.jsx
@@ -0,0 +1,37 @@
+import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
+import {
+ categories,
+ eventNames,
+ trackForgotPasswordLinkClick,
+ trackLoginPageViewed,
+} from '../login';
+
+// Mock createEventTracker function
+jest.mock('../../../data/segment/utils', () => ({
+ createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+}));
+
+describe('Tracking Functions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('trackForgotPasswordLinkClick function', () => {
+ trackForgotPasswordLinkClick();
+
+ expect(createEventTracker).toHaveBeenCalledWith(
+ eventNames.forgotPasswordLinkClicked,
+ { category: categories.userEngagement },
+ );
+ });
+
+ it('trackLoginPageEvent function', () => {
+ trackLoginPageViewed();
+
+ expect(createPageEventTracker).toHaveBeenCalledWith(
+ eventNames.loginAndRegistration,
+ 'login',
+ );
+ });
+});
diff --git a/src/tracking/trackers/tests/progressive-profiling.test.jsx b/src/tracking/trackers/tests/progressive-profiling.test.jsx
new file mode 100644
index 00000000..4cda593d
--- /dev/null
+++ b/src/tracking/trackers/tests/progressive-profiling.test.jsx
@@ -0,0 +1,37 @@
+import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
+import {
+ eventNames,
+ trackProgressiveProfilingPageViewed,
+ trackProgressiveProfilingSkipLinkClick,
+} from '../progressive-profiling';
+
+// Mock createEventTracker function
+jest.mock('../../../data/segment/utils', () => ({
+ createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createLinkTracker: jest.fn().mockImplementation(() => jest.fn()),
+}));
+
+describe('Tracking Functions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should fire trackProgressiveProfilingSkipLinkClickEvent', () => {
+ trackProgressiveProfilingSkipLinkClick();
+
+ expect(createEventTracker).toHaveBeenCalledWith(
+ eventNames.progressiveProfilingSkipLinkClick,
+ {},
+ );
+ });
+
+ it('should fire trackProgressiveProfilingPageEvent', () => {
+ trackProgressiveProfilingPageViewed();
+
+ expect(createPageEventTracker).toHaveBeenCalledWith(
+ eventNames.loginAndRegistration,
+ 'welcome',
+ );
+ });
+});
diff --git a/src/tracking/trackers/tests/register.test.jsx b/src/tracking/trackers/tests/register.test.jsx
new file mode 100644
index 00000000..d058908f
--- /dev/null
+++ b/src/tracking/trackers/tests/register.test.jsx
@@ -0,0 +1,36 @@
+import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
+import {
+ eventNames,
+ trackRegistrationPageViewed,
+ trackRegistrationSuccess,
+} from '../register';
+
+// Mock createEventTracker function
+jest.mock('../../../data/segment/utils', () => ({
+ createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+}));
+
+describe('Tracking Functions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should fire registrationSuccessEvent', () => {
+ trackRegistrationSuccess();
+
+ expect(createEventTracker).toHaveBeenCalledWith(
+ eventNames.registrationSuccess,
+ {},
+ );
+ });
+
+ it('should fire trackRegistrationPageEvent', () => {
+ trackRegistrationPageViewed();
+
+ expect(createPageEventTracker).toHaveBeenCalledWith(
+ eventNames.loginAndRegistration,
+ 'register',
+ );
+ });
+});
diff --git a/src/tracking/trackers/tests/reset-password.test.jsx b/src/tracking/trackers/tests/reset-password.test.jsx
new file mode 100644
index 00000000..15d6ed79
--- /dev/null
+++ b/src/tracking/trackers/tests/reset-password.test.jsx
@@ -0,0 +1,26 @@
+import { createPageEventTracker } from '../../../data/segment/utils';
+import {
+ eventNames,
+ trackResetPasswordPageViewed,
+} from '../reset-password';
+
+// Mock createEventTracker function
+jest.mock('../../../data/segment/utils', () => ({
+ createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+ createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
+}));
+
+describe('Tracking Functions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should fire trackResettPasswordPageEvent', () => {
+ trackResetPasswordPageViewed();
+
+ expect(createPageEventTracker).toHaveBeenCalledWith(
+ eventNames.loginAndRegistration,
+ 'reset-password',
+ );
+ });
+});
From 56bd6d835e910e3f330ce29d78b48092bcf24b1b Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Fri, 12 Jul 2024 13:18:42 +0500
Subject: [PATCH 41/82] fix: set marketing opt in in cookie for sso (#1285)
---
src/common-components/SocialAuthProviders.jsx | 8 +++++++-
.../tests/SocialAuthProviders.test.jsx | 7 ++++---
.../components/ConfigurableRegistrationForm.jsx | 15 +++++++++++++++
3 files changed, 26 insertions(+), 4 deletions(-)
diff --git a/src/common-components/SocialAuthProviders.jsx b/src/common-components/SocialAuthProviders.jsx
index abe06da7..f76e4968 100644
--- a/src/common-components/SocialAuthProviders.jsx
+++ b/src/common-components/SocialAuthProviders.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -8,15 +9,20 @@ import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';
-import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
+import { LOGIN_PAGE, REGISTER_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
+import { setCookie } from '../data/utils';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
const { referrer, socialAuthProviders } = props;
+ const registrationFields = useSelector(state => state.register.registrationFormData);
function handleSubmit(e) {
e.preventDefault();
+ if (referrer === REGISTER_PAGE) {
+ setCookie('marketingEmailsOptIn', registrationFields?.configurableFormFields?.marketingEmailsOptIn);
+ }
const url = e.currentTarget.dataset.providerUrl;
window.location.href = getConfig().LMS_BASE_URL + url;
}
diff --git a/src/common-components/tests/SocialAuthProviders.test.jsx b/src/common-components/tests/SocialAuthProviders.test.jsx
index 850708ec..ccc96157 100644
--- a/src/common-components/tests/SocialAuthProviders.test.jsx
+++ b/src/common-components/tests/SocialAuthProviders.test.jsx
@@ -27,7 +27,8 @@ describe('SocialAuthProviders', () => {
loginUrl: '/auth/login/facebook/?auth_entry=login&next=/dashboard',
};
- it('should match social auth provider with iconImage snapshot', () => {
+ // Skipped tests will be fixed later.
+ it.skip('should match social auth provider with iconImage snapshot', () => {
props = { socialAuthProviders: [appleProvider, facebookProvider] };
const tree = renderer.create(
@@ -39,7 +40,7 @@ describe('SocialAuthProviders', () => {
expect(tree).toMatchSnapshot();
});
- it('should match social auth provider with iconClass snapshot', () => {
+ it.skip('should match social auth provider with iconClass snapshot', () => {
props = {
socialAuthProviders: [{
...appleProvider,
@@ -57,7 +58,7 @@ describe('SocialAuthProviders', () => {
expect(tree).toMatchSnapshot();
});
- it('should match social auth provider with default icon snapshot', () => {
+ it.skip('should match social auth provider with default icon snapshot', () => {
props = {
socialAuthProviders: [{
...appleProvider,
diff --git a/src/register/components/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx
index 8c300b7e..971bdd4a 100644
--- a/src/register/components/ConfigurableRegistrationForm.jsx
+++ b/src/register/components/ConfigurableRegistrationForm.jsx
@@ -1,10 +1,12 @@
import React, { useEffect, useMemo } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { FormFieldRenderer } from '../../field-renderer';
+import { backupRegistrationFormBegin } from '../data/actions';
import { FIELDS } from '../data/constants';
import messages from '../messages';
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
@@ -32,6 +34,7 @@ const ConfigurableRegistrationForm = (props) => {
setFormFields,
autoSubmitRegistrationForm,
} = props;
+ const dispatch = useDispatch();
/** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
States' instead of 'United States of America' which does not exist in country dropdown list and gets the user
@@ -50,6 +53,8 @@ const ConfigurableRegistrationForm = (props) => {
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
};
+ const backedUpFormData = useSelector(state => state.register.registrationFormData);
+
/**
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
*/
@@ -90,6 +95,16 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
}
}
+ // setting marketingEmailsOptIn state for SSO authentication flow for register API call
+ if (name === 'marketingEmailsOptIn') {
+ dispatch(backupRegistrationFormBegin({
+ ...backedUpFormData,
+ configurableFormFields: {
+ ...backedUpFormData.configurableFormFields,
+ [name]: value,
+ },
+ }));
+ }
setFormFields(prevState => ({ ...prevState, [name]: value }));
};
From 05aa85a5fbad144b4ed9af615a37354308790b19 Mon Sep 17 00:00:00 2001
From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com>
Date: Fri, 12 Jul 2024 17:05:50 +0500
Subject: [PATCH 42/82] fix: remove cookie (#1286)
-remove marketingEmailsOptIn cookie on successful registration
- fix tests
---
.../RedirectLogistration.jsx | 2 +-
.../tests/SocialAuthProviders.test.jsx | 38 ++++++++++++++-----
src/data/utils/cookies.js | 8 ++++
src/data/utils/index.js | 2 +-
src/register/RegistrationPage.jsx | 5 ++-
5 files changed, 42 insertions(+), 13 deletions(-)
diff --git a/src/common-components/RedirectLogistration.jsx b/src/common-components/RedirectLogistration.jsx
index f6c30b50..24603f6a 100644
--- a/src/common-components/RedirectLogistration.jsx
+++ b/src/common-components/RedirectLogistration.jsx
@@ -5,7 +5,7 @@ import { Navigate } from 'react-router-dom';
import {
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
} from '../data/constants';
-import { setCookie } from '../data/utils';
+import setCookie from '../data/utils/cookies';
const RedirectLogistration = (props) => {
const {
diff --git a/src/common-components/tests/SocialAuthProviders.test.jsx b/src/common-components/tests/SocialAuthProviders.test.jsx
index ccc96157..b1066a19 100644
--- a/src/common-components/tests/SocialAuthProviders.test.jsx
+++ b/src/common-components/tests/SocialAuthProviders.test.jsx
@@ -1,16 +1,35 @@
import React from 'react';
+import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
+import configureStore from 'redux-mock-store';
import registerIcons from '../RegisterFaIcons';
import SocialAuthProviders from '../SocialAuthProviders';
registerIcons();
+const mockStore = configureStore();
describe('SocialAuthProviders', () => {
let props = {};
+ const initialState = {
+ register: {
+ registrationFormData: {
+ configurableFormFields: {
+ marketingEmailsOptIn: true,
+ },
+ },
+ },
+ };
+ const store = mockStore(initialState);
+ const reduxWrapper = children => (
+
+ {children}
+
+ );
+
const appleProvider = {
id: 'oa2-apple-id',
name: 'Apple',
@@ -27,20 +46,19 @@ describe('SocialAuthProviders', () => {
loginUrl: '/auth/login/facebook/?auth_entry=login&next=/dashboard',
};
- // Skipped tests will be fixed later.
- it.skip('should match social auth provider with iconImage snapshot', () => {
+ it('should match social auth provider with iconImage snapshot', () => {
props = { socialAuthProviders: [appleProvider, facebookProvider] };
- const tree = renderer.create(
+ const tree = renderer.create(reduxWrapper(
,
- ).toJSON();
+ )).toJSON();
expect(tree).toMatchSnapshot();
});
- it.skip('should match social auth provider with iconClass snapshot', () => {
+ it('should match social auth provider with iconClass snapshot', () => {
props = {
socialAuthProviders: [{
...appleProvider,
@@ -49,16 +67,16 @@ describe('SocialAuthProviders', () => {
}],
};
- const tree = renderer.create(
+ const tree = renderer.create(reduxWrapper(
,
- ).toJSON();
+ )).toJSON();
expect(tree).toMatchSnapshot();
});
- it.skip('should match social auth provider with default icon snapshot', () => {
+ it('should match social auth provider with default icon snapshot', () => {
props = {
socialAuthProviders: [{
...appleProvider,
@@ -67,11 +85,11 @@ describe('SocialAuthProviders', () => {
}],
};
- const tree = renderer.create(
+ const tree = renderer.create(reduxWrapper(
,
- ).toJSON();
+ )).toJSON();
expect(tree).toMatchSnapshot();
});
diff --git a/src/data/utils/cookies.js b/src/data/utils/cookies.js
index 1aad2858..cfddf5ec 100644
--- a/src/data/utils/cookies.js
+++ b/src/data/utils/cookies.js
@@ -11,3 +11,11 @@ export default function setCookie(cookieName, cookieValue, cookieExpiry) {
cookies.set(cookieName, cookieValue, options);
}
}
+
+export function removeCookie(cookieName) {
+ if (cookieName) {
+ const cookies = new Cookies();
+ const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
+ cookies.remove(cookieName, options);
+ }
+}
diff --git a/src/data/utils/index.js b/src/data/utils/index.js
index 05532111..1c9eebbb 100644
--- a/src/data/utils/index.js
+++ b/src/data/utils/index.js
@@ -8,4 +8,4 @@ export {
windowScrollTo,
} from './dataUtils';
export { default as AsyncActionType } from './reduxUtils';
-export { default as setCookie } from './cookies';
+export { default as setCookie, removeCookie } from './cookies';
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 0a0a5b53..50f689bf 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -46,7 +46,7 @@ import {
APP_NAME, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
import {
- getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
+ getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, removeCookie, setCookie,
} from '../data/utils';
import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
@@ -187,6 +187,9 @@ const RegistrationPage = (props) => {
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
+
+ // remove marketingEmailsOptIn cookie that was set on SSO registration flow
+ removeCookie('marketingEmailsOptIn');
}
}, [registrationResult]);
From d10f9b932bd87b7e2ffc54393bf7e7a3719daff6 Mon Sep 17 00:00:00 2001
From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com>
Date: Fri, 26 Jul 2024 15:57:18 +0500
Subject: [PATCH 43/82] fix: fix marketingEmailsOptIn null value (#1294)
Fix marketingEmailsOptIn null value issue for SSO flow on onboarding component
VAN-2013
---
src/common-components/ThirdPartyAuthAlert.jsx | 6 +++++-
src/login/LoginPage.jsx | 4 ++++
src/register/RegistrationPage.jsx | 4 +++-
3 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/src/common-components/ThirdPartyAuthAlert.jsx b/src/common-components/ThirdPartyAuthAlert.jsx
index fb79a6a6..fa2c6ee6 100644
--- a/src/common-components/ThirdPartyAuthAlert.jsx
+++ b/src/common-components/ThirdPartyAuthAlert.jsx
@@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
+import setCookie from '../data/utils/cookies';
const ThirdPartyAuthAlert = (props) => {
const { formatMessage } = useIntl();
@@ -20,7 +21,10 @@ const ThirdPartyAuthAlert = (props) => {
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
}
- if (!currentProvider) {
+ if (currentProvider) {
+ // Setting this cookie to capture marketingEmailsOptIn for SSO flow on the onboarding component
+ setCookie('ssoPipelineRedirectionDone', true);
+ } else {
return null;
}
diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx
index 0ff25258..6a935935 100644
--- a/src/login/LoginPage.jsx
+++ b/src/login/LoginPage.jsx
@@ -41,6 +41,7 @@ import {
getTpaProvider,
updatePathWithQueryParams,
} from '../data/utils';
+import { removeCookie } from '../data/utils/cookies';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
import {
trackForgotPasswordLinkClick, trackLoginPageViewed, trackLoginSuccess,
@@ -86,6 +87,9 @@ const LoginPage = (props) => {
useEffect(() => {
if (loginResult.success) {
trackLoginSuccess();
+
+ // Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
+ removeCookie('ssoPipelineRedirectionDone');
}
}, [loginResult]);
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 50f689bf..8b6dc9fd 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -188,8 +188,10 @@ const RegistrationPage = (props) => {
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
- // remove marketingEmailsOptIn cookie that was set on SSO registration flow
+ // Remove marketingEmailsOptIn cookie that was set on SSO registration flow
removeCookie('marketingEmailsOptIn');
+ // Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
+ removeCookie('ssoPipelineRedirectionDone');
}
}, [registrationResult]);
From 2d50ed224fbdc88aff14ba05aec3bcf473c2a46b Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Mon, 29 Jul 2024 11:23:48 +0500
Subject: [PATCH 44/82] fix: retain query params in authenticated user
redirection (#1288)
---
src/common-components/UnAuthOnlyRoute.jsx | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/common-components/UnAuthOnlyRoute.jsx b/src/common-components/UnAuthOnlyRoute.jsx
index 7be62aae..3e05fe0b 100644
--- a/src/common-components/UnAuthOnlyRoute.jsx
+++ b/src/common-components/UnAuthOnlyRoute.jsx
@@ -4,9 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import PropTypes from 'prop-types';
-import {
- DEFAULT_REDIRECT_URL,
-} from '../data/constants';
+import { updatePathWithQueryParams } from '../data/utils';
/**
* This wrapper redirects the requester to our default redirect url if they are
@@ -25,7 +23,8 @@ const UnAuthOnlyRoute = ({ children }) => {
if (isReady) {
if (authUser && authUser.username) {
- global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
+ const updatedPath = updatePathWithQueryParams(window.location.pathname);
+ global.location.href = getConfig().LMS_BASE_URL.concat(updatedPath);
return null;
}
From efc07aac67da32278c68e14ad374a24eef69852e Mon Sep 17 00:00:00 2001
From: Syed Sajjad Hussain Shah
<52817156+syedsajjadkazmii@users.noreply.github.com>
Date: Thu, 1 Aug 2024 16:06:20 +0500
Subject: [PATCH 45/82] fix: fix datadog js errors (#1296)
---
src/recommendations/track.js | 4 ++--
src/register/RegistrationFields/NameField/validator.js | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/recommendations/track.js b/src/recommendations/track.js
index 5d826a3e..9a18b974 100644
--- a/src/recommendations/track.js
+++ b/src/recommendations/track.js
@@ -15,7 +15,7 @@ const generateProductKey = (product) => (
export const getProductMapping = (recommendedProducts) => recommendedProducts.map((product) => ({
product_key: generateProductKey(product),
product_line: product.cardType,
- product_source: product.productSource.name,
+ product_source: product?.productSource?.name,
}));
export const trackRecommendationClick = (product, position, userId) => {
@@ -25,7 +25,7 @@ export const trackRecommendationClick = (product, position, userId) => {
recommendation_type: product.recommendationType,
product_key: generateProductKey(product),
product_line: product.cardType,
- product_source: product.productSource.name,
+ product_source: product?.productSource?.name,
user_id: userId,
});
diff --git a/src/register/RegistrationFields/NameField/validator.js b/src/register/RegistrationFields/NameField/validator.js
index aefaedfb..c8e1d09c 100644
--- a/src/register/RegistrationFields/NameField/validator.js
+++ b/src/register/RegistrationFields/NameField/validator.js
@@ -11,7 +11,7 @@ export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g;
const validateName = (value, formatMessage) => {
let fieldError = '';
- if (!value.trim()) {
+ if (!value || (value && !value.trim())) {
fieldError = formatMessage(messages['empty.name.field.error']);
} else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) {
fieldError = formatMessage(messages['name.validation.message']);
From cd9b3bd08470d126e2136738abbe2306b5c772ae Mon Sep 17 00:00:00 2001
From: Blue
Date: Wed, 28 Aug 2024 14:58:40 +0500
Subject: [PATCH 46/82] fix: covert totalRegistrationTime to snake case (#1302)
Description:
Convert totalRegistrationTime to snake case
VAN-1816
Co-authored-by: Ahtesham Quraish
---
src/register/RegistrationPage.test.jsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx
index a98d55d5..5f23e2c2 100644
--- a/src/register/RegistrationPage.test.jsx
+++ b/src/register/RegistrationPage.test.jsx
@@ -892,6 +892,7 @@ describe('RegistrationPage', () => {
country: 'PK',
social_auth_provider: 'Apple',
total_registration_time: 0,
+ app_name: APP_NAME,
}));
});
});
From ac2548913f8f5d2e29baa5b078104b83fcfa358e Mon Sep 17 00:00:00 2001
From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com>
Date: Thu, 29 Aug 2024 09:48:21 +0500
Subject: [PATCH 47/82] fix: password reset redirection (#1300)
fix authenticated user redirects to 404 if token is invalide for password reset
VAN-2052
---
src/common-components/UnAuthOnlyRoute.jsx | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/common-components/UnAuthOnlyRoute.jsx b/src/common-components/UnAuthOnlyRoute.jsx
index 3e05fe0b..2e9df557 100644
--- a/src/common-components/UnAuthOnlyRoute.jsx
+++ b/src/common-components/UnAuthOnlyRoute.jsx
@@ -4,6 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import PropTypes from 'prop-types';
+import { RESET_PAGE } from '../data/constants';
import { updatePathWithQueryParams } from '../data/utils';
/**
@@ -24,6 +25,10 @@ const UnAuthOnlyRoute = ({ children }) => {
if (isReady) {
if (authUser && authUser.username) {
const updatedPath = updatePathWithQueryParams(window.location.pathname);
+ if (updatedPath.startsWith(RESET_PAGE)) {
+ global.location.href = getConfig().LMS_BASE_URL;
+ return null;
+ }
global.location.href = getConfig().LMS_BASE_URL.concat(updatedPath);
return null;
}
From b41fca36056cccb11a8aa895a0512df5d71cc2e1 Mon Sep 17 00:00:00 2001
From: Awais Ansari <79941147+awais-ansari@users.noreply.github.com>
Date: Tue, 10 Sep 2024 21:11:44 +0500
Subject: [PATCH 48/82] feat: removed Russian Federation from country list
(#1315)
---
src/register/components/ConfigurableRegistrationForm.jsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/register/components/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx
index 971bdd4a..3013be5f 100644
--- a/src/register/components/ConfigurableRegistrationForm.jsx
+++ b/src/register/components/ConfigurableRegistrationForm.jsx
@@ -40,7 +40,10 @@ const ConfigurableRegistrationForm = (props) => {
States' instead of 'United States of America' which does not exist in country dropdown list and gets the user
confused and unable to create an account. So we added the United States entry in the dropdown list.
*/
- const countryList = useMemo(() => getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []);
+
+ const countryList = useMemo(() => (
+ getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]).filter(country => country.code !== 'RU')
+ ), []);
let showTermsOfServiceAndHonorCode = false;
let showCountryField = false;
From 47b0501e1cf4ff1c9de25659ceff85549e25f1fa Mon Sep 17 00:00:00 2001
From: Awais Ansari <79941147+awais-ansari@users.noreply.github.com>
Date: Wed, 25 Sep 2024 13:58:01 +0500
Subject: [PATCH 49/82] feat: add MainAppSlot for chatbot plugin (#1320)
* feat: add MainAppSlot for chatbot plugin
* test: added test for MainAppSlot
* chore: add read me for plugin-slot
---
package-lock.json | 57 ++++++++++++++++--
package.json | 2 +
src/MainApp.jsx | 5 +-
.../MainAppSlot/MainAppSlot.test.jsx | 29 +++++++++
src/plugin-slots/MainAppSlot/README.md | 41 +++++++++++++
.../MainAppSlot/images/main_app_slot.png | Bin 0 -> 275766 bytes
src/plugin-slots/MainAppSlot/index.jsx | 7 +++
src/plugin-slots/README.md | 3 +
8 files changed, 136 insertions(+), 8 deletions(-)
create mode 100644 src/plugin-slots/MainAppSlot/MainAppSlot.test.jsx
create mode 100644 src/plugin-slots/MainAppSlot/README.md
create mode 100644 src/plugin-slots/MainAppSlot/images/main_app_slot.png
create mode 100644 src/plugin-slots/MainAppSlot/index.jsx
create mode 100644 src/plugin-slots/README.md
diff --git a/package-lock.json b/package-lock.json
index 1b356ecf..b5a8a6af 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@fortawesome/free-brands-svg-icons": "6.6.0",
"@fortawesome/free-solid-svg-icons": "6.6.0",
"@fortawesome/react-fontawesome": "0.2.2",
+ "@openedx/frontend-plugin-framework": "^1.3.0",
"@openedx/paragon": "^22.1.1",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.3.0",
@@ -31,6 +32,7 @@
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
+ "react-error-boundary": "^4.0.13",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.4.0",
"react-redux": "7.2.9",
@@ -3395,6 +3397,37 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@openedx/frontend-plugin-framework": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.3.0.tgz",
+ "integrity": "sha512-qLtX/4HIuWXiIhBdtBuL6mPVbV2un0rsFYx3I5+3tIUf7+T7WRq81a6JHU5QGyAmZy9dfiv7QwbqwiEQOVXVuQ==",
+ "dependencies": {
+ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
+ "classnames": "^2.3.2",
+ "core-js": "3.37.1",
+ "react-redux": "7.2.9",
+ "redux": "4.2.1",
+ "regenerator-runtime": "0.14.1"
+ },
+ "peerDependencies": {
+ "@edx/frontend-platform": "^7.0.0 || ^8.0.0",
+ "@openedx/paragon": "^21.0.0 || ^22.0.0",
+ "prop-types": "^15.8.0",
+ "react": "^17.0.0",
+ "react-dom": "^17.0.0",
+ "react-error-boundary": "^4.0.11"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/core-js": {
+ "version": "3.37.1",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz",
+ "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==",
+ "hasInstallScript": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/@openedx/paragon": {
"version": "22.7.0",
"license": "Apache-2.0",
@@ -4053,6 +4086,21 @@
}
}
},
+ "node_modules/@testing-library/react-hooks/node_modules/react-error-boundary": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
+ "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"license": "MIT",
@@ -13216,15 +13264,12 @@
}
},
"node_modules/react-error-boundary": {
- "version": "3.1.4",
- "license": "MIT",
+ "version": "4.0.13",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz",
+ "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
- "engines": {
- "node": ">=10",
- "npm": ">=6"
- },
"peerDependencies": {
"react": ">=16.13.1"
}
diff --git a/package.json b/package.json
index 7d3966bb..f344c8d8 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"@fortawesome/free-brands-svg-icons": "6.6.0",
"@fortawesome/free-solid-svg-icons": "6.6.0",
"@fortawesome/react-fontawesome": "0.2.2",
+ "@openedx/frontend-plugin-framework": "^1.3.0",
"@openedx/paragon": "^22.1.1",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.3.0",
@@ -54,6 +55,7 @@
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
+ "react-error-boundary": "^4.0.13",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.4.0",
"react-redux": "7.2.9",
diff --git a/src/MainApp.jsx b/src/MainApp.jsx
index 26c2cf59..b107e365 100755
--- a/src/MainApp.jsx
+++ b/src/MainApp.jsx
@@ -6,7 +6,7 @@ import { Helmet } from 'react-helmet';
import { Navigate, Route, Routes } from 'react-router-dom';
import {
- EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
+ EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute,
} from './common-components';
import configureStore from './data/configureStore';
import {
@@ -22,6 +22,7 @@ import {
import { updatePathWithQueryParams } from './data/utils';
import { ForgotPasswordPage } from './forgot-password';
import Logistration from './logistration/Logistration';
+import MainAppSlot from './plugin-slots/MainAppSlot';
import { ProgressiveProfiling } from './progressive-profiling';
import { RecommendationsPage } from './recommendations';
import { RegistrationPage } from './register';
@@ -36,7 +37,6 @@ const MainApp = () => (
- {getConfig().ZENDESK_KEY && }
} />
(
} />
} />
+
);
diff --git a/src/plugin-slots/MainAppSlot/MainAppSlot.test.jsx b/src/plugin-slots/MainAppSlot/MainAppSlot.test.jsx
new file mode 100644
index 00000000..f5262d81
--- /dev/null
+++ b/src/plugin-slots/MainAppSlot/MainAppSlot.test.jsx
@@ -0,0 +1,29 @@
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import { render } from '@testing-library/react';
+
+import MainAppSlot from './index';
+
+jest.mock('@openedx/frontend-plugin-framework', () => ({
+ PluginSlot: jest.fn(() => null),
+}));
+
+describe('MainAppSlot', () => {
+ it('renders without crashing', () => {
+ render();
+ });
+
+ it('renders a PluginSlot component', () => {
+ render();
+ expect(PluginSlot).toHaveBeenCalled();
+ });
+
+ it('passes the correct id prop to PluginSlot', () => {
+ render();
+ expect(PluginSlot).toHaveBeenCalledWith({ id: 'main_app_slot' }, {});
+ });
+
+ it('does not render any children', () => {
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+});
diff --git a/src/plugin-slots/MainAppSlot/README.md b/src/plugin-slots/MainAppSlot/README.md
new file mode 100644
index 00000000..ff755fc0
--- /dev/null
+++ b/src/plugin-slots/MainAppSlot/README.md
@@ -0,0 +1,41 @@
+# Main App Slot
+
+### Slot ID: `main_app_slot`
+
+## Description
+
+This slot is used for adding content at the root level.
+
+## Example
+
+The following `env.config.jsx` will render a component at the MFE root level.
+
+
+
+```js
+import {
+ DIRECT_PLUGIN,
+ PLUGIN_OPERATIONS,
+} from "@openedx/frontend-plugin-framework";
+import { ExampleComponent } from "@openedx/frontend-plugin-example";
+
+const config = {
+ pluginSlots: {
+ main_app_slot: {
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: "example-component",
+ type: DIRECT_PLUGIN,
+ priority: 60,
+ RenderWidget: ExampleComponent,
+ },
+ },
+ ],
+ },
+ },
+};
+
+export default config;
+```
diff --git a/src/plugin-slots/MainAppSlot/images/main_app_slot.png b/src/plugin-slots/MainAppSlot/images/main_app_slot.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc842adec1295bc5ab87d0bf4ea693cf86dad8e7
GIT binary patch
literal 275766
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