From c1be24c2144a23287c03b6d52e4b73202ce9d6c1 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon <44397098+microbit-matt-hillsdon@users.noreply.github.com> Date: Tue, 1 Aug 2023 10:32:36 +0100 Subject: [PATCH] Cookie consent integration (#1124) This integrates the new microbit.org-wide cookie consent approach. Mostly this is delegated to the deployment code, but the line between the two changes. New action added to the help menu so cookie settings can be revisited. The welcome dialog is delayed until after a cookie decision is made to avoid a clash of modals. --- .github/workflows/build.yml | 3 ++- lang/ui.ca.json | 4 ++++ lang/ui.en.json | 4 ++++ lang/ui.es-es.json | 4 ++++ lang/ui.fr.json | 4 ++++ lang/ui.ja.json | 4 ++++ lang/ui.ko.json | 4 ++++ lang/ui.nl.json | 4 ++++ lang/ui.zh-cn.json | 4 ++++ lang/ui.zh-tw.json | 4 ++++ package-lock.json | 14 ++++++------ package.json | 2 +- public/index.html | 20 +++++------------ src/App.tsx | 13 ++++++----- src/deployment/default/index.tsx | 20 ++++++++++++++++- src/deployment/index.ts | 35 ++++++++++++++++++++++-------- src/e2e/app.ts | 10 +++++++-- src/messages/ui.ca.json | 6 +++++ src/messages/ui.en.json | 6 +++++ src/messages/ui.es-es.json | 6 +++++ src/messages/ui.fr.json | 6 +++++ src/messages/ui.ja.json | 6 +++++ src/messages/ui.ko.json | 6 +++++ src/messages/ui.nl.json | 6 +++++ src/messages/ui.zh-cn.json | 6 +++++ src/messages/ui.zh-tw.json | 6 +++++ src/workbench/HelpMenu.tsx | 16 ++++++++++++-- src/workbench/PreReleaseNotice.tsx | 12 +++++++--- src/workbench/Workbench.tsx | 19 +--------------- 29 files changed, 190 insertions(+), 64 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 86119b95a..70ad78cfc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,7 @@ jobs: PRODUCTION_CLOUDFRONT_DISTRIBUTION_ID: E2ELTBTA2OFPY2 STAGING_CLOUDFRONT_DISTRIBUTION_ID: E2ELTBTA2OFPY2 REVIEW_CLOUDFRONT_DISTRIBUTION_ID: E3267W09ZJHQG9 + REACT_APP_FOUNDATION_BUILD: ${{ github.repository_owner == 'microbit-foundation' }} steps: # Note: This workflow disables deployment steps and micro:bit branding installation on forks. @@ -35,7 +36,7 @@ jobs: - run: npm ci env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: npm install --no-save @microbit-foundation/python-editor-v3-microbit@0.1.0-dev.198 @microbit-foundation/website-deploy-aws@0.3.0 @microbit-foundation/website-deploy-aws-config@0.7.1 @microbit-foundation/circleci-npm-package-versioner@1 + - run: npm install --no-save @microbit-foundation/python-editor-v3-microbit@0.2.0-dev.18 @microbit-foundation/website-deploy-aws@0.3.0 @microbit-foundation/website-deploy-aws-config@0.7.1 @microbit-foundation/circleci-npm-package-versioner@1 if: github.repository_owner == 'microbit-foundation' env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/lang/ui.ca.json b/lang/ui.ca.json index a94acbb42..d53287236 100644 --- a/lang/ui.ca.json +++ b/lang/ui.ca.json @@ -195,6 +195,10 @@ "defaultMessage": "Alguna cosa ha anat malament. Baixa el teu fitxer hexadecimal per mantenir-lo segur i, a continuació, actualitza la pàgina per tornar-la a carregar.", "description": "Text displayed when content fails to load" }, + "cookies-action": { + "defaultMessage": "Cookies", + "description": "Action to show dialog to choose website cookie preferences" + }, "copied": { "defaultMessage": "Copiat", "description": "Text shown after copy to clipboard" diff --git a/lang/ui.en.json b/lang/ui.en.json index be2d13265..248c9d3b6 100644 --- a/lang/ui.en.json +++ b/lang/ui.en.json @@ -195,6 +195,10 @@ "defaultMessage": "Something went wrong. Download your hex file for safe keeping, then refresh the page to reload.", "description": "Text displayed when content fails to load" }, + "cookies-action": { + "defaultMessage": "Cookies", + "description": "Action to show dialog to choose website cookie preferences" + }, "copied": { "defaultMessage": "Copied", "description": "Text shown after copy to clipboard" diff --git a/lang/ui.es-es.json b/lang/ui.es-es.json index e2f8c2907..0c702f70a 100644 --- a/lang/ui.es-es.json +++ b/lang/ui.es-es.json @@ -195,6 +195,10 @@ "defaultMessage": "Algo salió mal. Por seguridad, descarga tu archivo HEX y actualiza la página para recargar.", "description": "Text displayed when content fails to load" }, + "cookies-action": { + "defaultMessage": "Cookies", + "description": "Action to show dialog to choose website cookie preferences" + }, "copied": { "defaultMessage": "Copiado", "description": "Text shown after copy to clipboard" diff --git a/lang/ui.fr.json b/lang/ui.fr.json index c0eb1d903..543c7abd9 100644 --- a/lang/ui.fr.json +++ b/lang/ui.fr.json @@ -195,6 +195,10 @@ "defaultMessage": "Une erreur est survenue. Téléchargez votre fichier hex pour ne pas le perdre, puis actualisez la page.", "description": "Text displayed when content fails to load" }, + "cookies-action": { + "defaultMessage": "Cookies", + "description": "Action to show dialog to choose website cookie preferences" + }, "copied": { "defaultMessage": "Copié", "description": "Text shown after copy to clipboard" diff --git a/lang/ui.ja.json b/lang/ui.ja.json index acb27089f..774f88ef4 100644 --- a/lang/ui.ja.json +++ b/lang/ui.ja.json @@ -195,6 +195,10 @@ "defaultMessage": "問題が発生しました。hex ファイルをダウンロードして、ページをリロードしてください。", "description": "Text displayed when content fails to load" }, + "cookies-action": { + "defaultMessage": "Cookies", + "description": "Action to show dialog to choose website cookie preferences" + }, "copied": { "defaultMessage": "コピーしました", "description": "Text shown after copy to clipboard" diff --git a/lang/ui.ko.json b/lang/ui.ko.json index cd45f9838..0f148421f 100644 --- a/lang/ui.ko.json +++ b/lang/ui.ko.json @@ -195,6 +195,10 @@ "defaultMessage": "오류가 발생했습니다. hex 파일을 다운로드해 작업물을 보호하고 새로 고침으로 페이지를 다시 불러오세요.", "description": "Text displayed when content fails to load" }, + "cookies-action": { + "defaultMessage": "Cookies", + "description": "Action to show dialog to choose website cookie preferences" + }, "copied": { "defaultMessage": "복사됨", "description": "Text shown after copy to clipboard" diff --git a/lang/ui.nl.json b/lang/ui.nl.json index 61f866d70..52555630e 100644 --- a/lang/ui.nl.json +++ b/lang/ui.nl.json @@ -195,6 +195,10 @@ "defaultMessage": "Er is iets fout gegaan. Download je hexadecimale bestand voor een veilige bewaring, ververs daarna de pagina om te herladen.", "description": "Text displayed when content fails to load" }, + "cookies-action": { + "defaultMessage": "Cookies", + "description": "Action to show dialog to choose website cookie preferences" + }, "copied": { "defaultMessage": "Gekopieerd", "description": "Text shown after copy to clipboard" diff --git a/lang/ui.zh-cn.json b/lang/ui.zh-cn.json index 43cc76358..8bd9b2c88 100644 --- a/lang/ui.zh-cn.json +++ b/lang/ui.zh-cn.json @@ -195,6 +195,10 @@ "defaultMessage": "出错了。下载您的 hex 文件以便安全保存,然后刷新页面来重新加载。", "description": "Text displayed when content fails to load" }, + "cookies-action": { + "defaultMessage": "Cookies", + "description": "Action to show dialog to choose website cookie preferences" + }, "copied": { "defaultMessage": "已复制", "description": "Text shown after copy to clipboard" diff --git a/lang/ui.zh-tw.json b/lang/ui.zh-tw.json index 840ab3e6d..8cb18f409 100644 --- a/lang/ui.zh-tw.json +++ b/lang/ui.zh-tw.json @@ -195,6 +195,10 @@ "defaultMessage": "發生錯誤。下載您的 HEX 檔案以便安全儲存,然後重新整理頁面來重新載入。", "description": "Text displayed when content fails to load" }, + "cookies-action": { + "defaultMessage": "Cookies", + "description": "Action to show dialog to choose website cookie preferences" + }, "copied": { "defaultMessage": "複製的", "description": "Text shown after copy to clipboard" diff --git a/package-lock.json b/package-lock.json index 7fc54aff8..c0d33676b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "mobile-drag-drop": "^2.3.0-rc.2", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-icons": "^4.2.0", + "react-icons": "^4.8.0", "react-intl": "^5.20.10", "vscode-jsonrpc": "^6.0.0", "vscode-languageserver-protocol": "^3.16.0", @@ -19743,9 +19743,9 @@ } }, "node_modules/react-icons": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.2.0.tgz", - "integrity": "sha512-rmzEDFt+AVXRzD7zDE21gcxyBizD/3NqjbX6cmViAgdqfJ2UiLer8927/QhhrXQV7dEj/1EGuOTPp7JnLYVJKQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz", + "integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==", "peerDependencies": { "react": "*" } @@ -38516,9 +38516,9 @@ } }, "react-icons": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.2.0.tgz", - "integrity": "sha512-rmzEDFt+AVXRzD7zDE21gcxyBizD/3NqjbX6cmViAgdqfJ2UiLer8927/QhhrXQV7dEj/1EGuOTPp7JnLYVJKQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz", + "integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==", "requires": {} }, "react-intl": { diff --git a/package.json b/package.json index cf8450668..d1b8b5f3e 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "mobile-drag-drop": "^2.3.0-rc.2", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-icons": "^4.2.0", + "react-icons": "^4.8.0", "react-intl": "^5.20.10", "vscode-jsonrpc": "^6.0.0", "vscode-languageserver-protocol": "^3.16.0", diff --git a/public/index.html b/public/index.html index 24c52dba1..00ae47ae9 100644 --- a/public/index.html +++ b/public/index.html @@ -20,25 +20,15 @@ name="twitter:description" content="A Python Editor for the BBC micro:bit, built by the Micro:bit Educational Foundation and the global Python Community." /> - <% if (process.env.REACT_APP_GA_MEASUREMENT_ID && - (process.env.REACT_APP_STAGE === 'PRODUCTION' || process.env.REACT_APP_STAGE - === "STAGING")) { %> - + <% if (process.env.REACT_APP_FOUNDATION_BUILD === 'true') { %> + + <% } %> diff --git a/src/App.tsx b/src/App.tsx index a025e1d98..1e2187d5a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -62,6 +62,7 @@ const App = () => { }); const deployment = useDeployment(); + const { ConsentProvider } = deployment.compliance; return ( <> @@ -79,11 +80,13 @@ const App = () => { - - - - - + + + + + + + diff --git a/src/deployment/default/index.tsx b/src/deployment/default/index.tsx index 884a5df09..352736847 100644 --- a/src/deployment/default/index.tsx +++ b/src/deployment/default/index.tsx @@ -3,13 +3,31 @@ * * SPDX-License-Identifier: MIT */ -import { DeploymentConfig } from ".."; +import { createContext } from "react"; +import { CookieConsent, DeploymentConfig } from ".."; import { NullLogging } from "./logging"; import theme from "./theme"; +const stubConsentValue: CookieConsent = { + analytics: false, + functional: true, +}; +const stubConsentContext = createContext( + stubConsentValue +); + const defaultDeployment: DeploymentConfig = { chakraTheme: theme, logging: new NullLogging(), + compliance: { + ConsentProvider: ({ children }) => ( + + {children} + + ), + consentContext: stubConsentContext, + manageCookies: undefined, + }, }; export default defaultDeployment; diff --git a/src/deployment/index.ts b/src/deployment/index.ts index 61ba1edf0..03f8c7c06 100644 --- a/src/deployment/index.ts +++ b/src/deployment/index.ts @@ -3,24 +3,36 @@ * * SPDX-License-Identifier: MIT */ -import { ReactNode } from "react"; -import { IconType } from "react-icons/lib"; +import { ReactNode, useContext } from "react"; import { Logging } from "../logging/logging"; // This is configured via a webpack alias, defaulting to ./default import { default as d } from "theme-package"; export const deployment: DeploymentConfig = d; +export interface CookieConsent { + analytics: boolean; + functional: boolean; +} + export interface DeploymentConfig { squareLogo?: ReactNode; horizontalLogo?: ReactNode; - Compliance?: ({ - zIndex, - externalLinkIcon, - }: { - zIndex: number; - externalLinkIcon: IconType; - }) => JSX.Element; + compliance: { + /** + * A provider that will be used to wrap the app UI. + */ + ConsentProvider: (props: { children: ReactNode }) => JSX.Element; + /** + * Context that will be used to read the current consent value. + * The provider is not used directly. + */ + consentContext: React.Context; + /** + * Optional hook for the user to revisit cookie settings. + */ + manageCookies: (() => void) | undefined; + }; chakraTheme: any; @@ -35,3 +47,8 @@ export interface DeploymentConfig { export const useDeployment = (): DeploymentConfig => { return deployment; }; + +export const useCookieConsent = (): CookieConsent | undefined => { + const { compliance } = useDeployment(); + return useContext(compliance.consentContext); +}; diff --git a/src/e2e/app.ts b/src/e2e/app.ts index 5f0212b2b..02ba453df 100644 --- a/src/e2e/app.ts +++ b/src/e2e/app.ts @@ -126,10 +126,16 @@ export class App { value: "1", url: this.url, }); - // Don't show compliance notice. + // Don't show compliance notice for Foundation builds await page.setCookie({ name: "MBCC", - value: "1", + value: encodeURIComponent( + JSON.stringify({ + version: 1, + analytics: false, + functional: true, + }) + ), url: this.url, }); diff --git a/src/messages/ui.ca.json b/src/messages/ui.ca.json index 4144f03bd..6fcf67da5 100644 --- a/src/messages/ui.ca.json +++ b/src/messages/ui.ca.json @@ -411,6 +411,12 @@ "value": "Alguna cosa ha anat malament. Baixa el teu fitxer hexadecimal per mantenir-lo segur i, a continuació, actualitza la pàgina per tornar-la a carregar." } ], + "cookies-action": [ + { + "type": 0, + "value": "Cookies" + } + ], "copied": [ { "type": 0, diff --git a/src/messages/ui.en.json b/src/messages/ui.en.json index f855d5841..311634217 100644 --- a/src/messages/ui.en.json +++ b/src/messages/ui.en.json @@ -407,6 +407,12 @@ "value": "Something went wrong. Download your hex file for safe keeping, then refresh the page to reload." } ], + "cookies-action": [ + { + "type": 0, + "value": "Cookies" + } + ], "copied": [ { "type": 0, diff --git a/src/messages/ui.es-es.json b/src/messages/ui.es-es.json index 8eafbb0b0..1588fa397 100644 --- a/src/messages/ui.es-es.json +++ b/src/messages/ui.es-es.json @@ -411,6 +411,12 @@ "value": "Algo salió mal. Por seguridad, descarga tu archivo HEX y actualiza la página para recargar." } ], + "cookies-action": [ + { + "type": 0, + "value": "Cookies" + } + ], "copied": [ { "type": 0, diff --git a/src/messages/ui.fr.json b/src/messages/ui.fr.json index 9bd5b7145..df5a2e339 100644 --- a/src/messages/ui.fr.json +++ b/src/messages/ui.fr.json @@ -407,6 +407,12 @@ "value": "Une erreur est survenue. Téléchargez votre fichier hex pour ne pas le perdre, puis actualisez la page." } ], + "cookies-action": [ + { + "type": 0, + "value": "Cookies" + } + ], "copied": [ { "type": 0, diff --git a/src/messages/ui.ja.json b/src/messages/ui.ja.json index 1b2138d94..536b66b2a 100644 --- a/src/messages/ui.ja.json +++ b/src/messages/ui.ja.json @@ -431,6 +431,12 @@ "value": "問題が発生しました。hex ファイルをダウンロードして、ページをリロードしてください。" } ], + "cookies-action": [ + { + "type": 0, + "value": "Cookies" + } + ], "copied": [ { "type": 0, diff --git a/src/messages/ui.ko.json b/src/messages/ui.ko.json index 94077d95c..ade0f3fb2 100644 --- a/src/messages/ui.ko.json +++ b/src/messages/ui.ko.json @@ -415,6 +415,12 @@ "value": "오류가 발생했습니다. hex 파일을 다운로드해 작업물을 보호하고 새로 고침으로 페이지를 다시 불러오세요." } ], + "cookies-action": [ + { + "type": 0, + "value": "Cookies" + } + ], "copied": [ { "type": 0, diff --git a/src/messages/ui.nl.json b/src/messages/ui.nl.json index 559feb68f..934206e71 100644 --- a/src/messages/ui.nl.json +++ b/src/messages/ui.nl.json @@ -419,6 +419,12 @@ "value": "Er is iets fout gegaan. Download je hexadecimale bestand voor een veilige bewaring, ververs daarna de pagina om te herladen." } ], + "cookies-action": [ + { + "type": 0, + "value": "Cookies" + } + ], "copied": [ { "type": 0, diff --git a/src/messages/ui.zh-cn.json b/src/messages/ui.zh-cn.json index 82efc1329..b831daba9 100644 --- a/src/messages/ui.zh-cn.json +++ b/src/messages/ui.zh-cn.json @@ -407,6 +407,12 @@ "value": "出错了。下载您的 hex 文件以便安全保存,然后刷新页面来重新加载。" } ], + "cookies-action": [ + { + "type": 0, + "value": "Cookies" + } + ], "copied": [ { "type": 0, diff --git a/src/messages/ui.zh-tw.json b/src/messages/ui.zh-tw.json index 1bca34cca..d622df00c 100644 --- a/src/messages/ui.zh-tw.json +++ b/src/messages/ui.zh-tw.json @@ -403,6 +403,12 @@ "value": "發生錯誤。下載您的 HEX 檔案以便安全儲存,然後重新整理頁面來重新載入。" } ], + "cookies-action": [ + { + "type": 0, + "value": "Cookies" + } + ], "copied": [ { "type": 0, diff --git a/src/workbench/HelpMenu.tsx b/src/workbench/HelpMenu.tsx index d1ce33fc7..521809c88 100644 --- a/src/workbench/HelpMenu.tsx +++ b/src/workbench/HelpMenu.tsx @@ -16,6 +16,7 @@ import { useDisclosure, } from "@chakra-ui/react"; import { useCallback, useRef } from "react"; +import { MdOutlineCookie } from "react-icons/md"; import { RiExternalLinkLine, RiFeedbackLine, @@ -25,7 +26,7 @@ import { import { FormattedMessage, useIntl } from "react-intl"; import { useDialogs } from "../common/use-dialogs"; import { zIndexAboveTerminal } from "../common/zIndex"; -import { deployment } from "../deployment"; +import { deployment, useDeployment } from "../deployment"; import AboutDialog from "./AboutDialog/AboutDialog"; import FeedbackForm from "./FeedbackForm"; @@ -49,6 +50,11 @@ const HelpMenu = ({ size, ...props }: HelpMenuProps) => { /> )); }, [dialogs]); + const { compliance } = useDeployment(); + const handleCookies = useCallback(() => { + // Only called if defined: + compliance.manageCookies!(); + }, [compliance]); const menuButtonRef = useRef(null); return ( <> @@ -106,7 +112,13 @@ const HelpMenu = ({ size, ...props }: HelpMenuProps) => { )} - + {deployment.compliance.manageCookies && ( + } onClick={handleCookies}> + + + )} + {(deployment.termsOfUseLink || + deployment.compliance.manageCookies) && } } onClick={aboutDialogDisclosure.onOpen} diff --git a/src/workbench/PreReleaseNotice.tsx b/src/workbench/PreReleaseNotice.tsx index 61a49b767..116209132 100644 --- a/src/workbench/PreReleaseNotice.tsx +++ b/src/workbench/PreReleaseNotice.tsx @@ -8,6 +8,7 @@ import { Flex, HStack, Text } from "@chakra-ui/layout"; import { useCallback, useEffect, useState } from "react"; import { RiFeedbackFill, RiInformationFill } from "react-icons/ri"; import { useStorage } from "../common/use-storage"; +import { useCookieConsent } from "../deployment"; import { flags } from "../flags"; export type ReleaseNoticeState = "info" | "feedback" | "closed"; @@ -39,13 +40,18 @@ export const useReleaseDialogState = (): [ ); const [releaseDialog, setReleaseDialog] = useState("closed"); - // Show the dialog on start-up once per user. + // Show the dialog on start-up once per user once we have cookie consent. + const cookieConsent = useCookieConsent(); useEffect(() => { - if (!flags.noWelcome && storedNotice.version < currentVersion) { + if ( + cookieConsent && + !flags.noWelcome && + storedNotice.version < currentVersion + ) { setReleaseDialog("info"); setStoredNotice({ version: currentVersion }); } - }, [storedNotice, setStoredNotice, setReleaseDialog]); + }, [cookieConsent, storedNotice, setStoredNotice, setReleaseDialog]); return [releaseDialog, setReleaseDialog]; }; diff --git a/src/workbench/Workbench.tsx b/src/workbench/Workbench.tsx index e3cc425c8..bd797d132 100644 --- a/src/workbench/Workbench.tsx +++ b/src/workbench/Workbench.tsx @@ -6,7 +6,6 @@ import { Box, Flex } from "@chakra-ui/layout"; import { useMediaQuery } from "@chakra-ui/react"; import { ReactNode, useCallback, useEffect, useRef, useState } from "react"; -import { RiExternalLinkLine } from "react-icons/ri"; import { useIntl } from "react-intl"; import { hideSidebarMediaQuery, @@ -21,8 +20,6 @@ import { SplitViewSized, } from "../common/SplitView"; import { SizedMode } from "../common/SplitView/SplitView"; -import { zIndexAboveDialogs } from "../common/zIndex"; -import { useDeployment } from "../deployment"; import { ConnectionStatus } from "../device/device"; import { useConnectionStatus } from "../device/device-hooks"; import EditorArea from "../editor/EditorArea"; @@ -120,23 +117,9 @@ const Workbench = () => { )} ); - const inIframe = () => { - try { - return window.self !== window.top; - } catch (e) { - return true; - } - }; - const deployment = useDeployment(); - const Compliance = deployment.Compliance ?? (() => null); + return ( - {!inIframe() && ( - - )}