From b8801584b72dc15fd892d8e352648025d03dfaea Mon Sep 17 00:00:00 2001 From: Maks Turtiainen Date: Mon, 27 Nov 2023 16:39:14 +0200 Subject: [PATCH] feat: add Askem feedback buttons (HL-922) --- .env.benefit-applicant.example | 3 + frontend/Dockerfile | 1 + .../applications/Applications.sc.ts | 20 ++++++ .../applications/pageContent/PageContent.tsx | 43 +++++++++---- .../cookieConsent/CookieConsent.tsx | 61 +++++++++++++++---- .../notificationView/NotificatinsView.sc.ts | 5 +- frontend/benefit/applicant/src/constants.ts | 3 + .../applicant/src/hooks/useAnalytics.ts | 34 +++++++++++ frontend/benefit/applicant/src/pages/_app.tsx | 4 +- .../benefit/applicant/src/types/common.d.ts | 19 ++++++ .../benefit/applicant/src/utils/cookie.ts | 19 ++++++ 11 files changed, 186 insertions(+), 26 deletions(-) create mode 100644 frontend/benefit/applicant/src/hooks/useAnalytics.ts create mode 100644 frontend/benefit/applicant/src/utils/cookie.ts diff --git a/.env.benefit-applicant.example b/.env.benefit-applicant.example index 7c1e45befc..d59f63c4f2 100644 --- a/.env.benefit-applicant.example +++ b/.env.benefit-applicant.example @@ -37,3 +37,6 @@ NEXTJS_SENTRY_DEBUG=true NEXTJS_SENTRY_TRACING=true NEXT_PUBLIC_SENTRY_ATTACH_STACKTRACE= NEXT_PUBLIC_SENTRY_MAX_BREADCRUMBS= + +NEXT_PUBLIC_ASKEM_API_KEY= +NEXT_PUBLIC_SHOW_COOKIE_BANNER=0 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 7628968ef2..dea8ddffa3 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -83,6 +83,7 @@ ARG SENTRY_PROJECT ARG SENTRY_AUTH_TOKEN ARG SENTRY_URL ARG SENTRY_RELEASE +ARG NEXT_PUBLIC_ASKEM_API_KEY # Use non-root user USER appuser diff --git a/frontend/benefit/applicant/src/components/applications/Applications.sc.ts b/frontend/benefit/applicant/src/components/applications/Applications.sc.ts index 896b402afc..af1cda1a35 100644 --- a/frontend/benefit/applicant/src/components/applications/Applications.sc.ts +++ b/frontend/benefit/applicant/src/components/applications/Applications.sc.ts @@ -49,3 +49,23 @@ export const $SpinnerContainer = styled.div` `; export const $Empty = styled.div``; + +export const $AskemContainer = styled.div` + display: flex; + flex-direction: column; + min-height: 129px; + margin: 0; + padding-left: ${(props) => props.theme.spacing.l}; + padding-right: ${(props) => props.theme.spacing.l}; + ${respondAbove('sm')` + flex-direction: row; + `} +`; + +export const $AskemItem = styled.div` + display: flex; + flex-flow: column; + ${respondAbove('sm')` + min-width: 13%; + `} +`; diff --git a/frontend/benefit/applicant/src/components/applications/pageContent/PageContent.tsx b/frontend/benefit/applicant/src/components/applications/pageContent/PageContent.tsx index ce05e0661a..88c363ebe0 100644 --- a/frontend/benefit/applicant/src/components/applications/pageContent/PageContent.tsx +++ b/frontend/benefit/applicant/src/components/applications/pageContent/PageContent.tsx @@ -12,7 +12,9 @@ import ApplicationFormStep3 from 'benefit/applicant/components/applications/form import ApplicationFormStep4 from 'benefit/applicant/components/applications/forms/application/step4/ApplicationFormStep4'; import ApplicationFormStep5 from 'benefit/applicant/components/applications/forms/application/step5/ApplicationFormStep5'; import ApplicationFormStep6 from 'benefit/applicant/components/applications/forms/application/step6/ApplicationFormStep6'; +import { $Hr } from 'benefit/applicant/components/pages/Pages.sc'; import { SUBMITTED_STATUSES } from 'benefit/applicant/constants'; +import { useAskem } from 'benefit/applicant/hooks/useAnalytics'; import { IconInfoCircleFill, LoadingSpinner, Stepper } from 'hds-react'; import { useRouter } from 'next/router'; import React, { useEffect } from 'react'; @@ -24,6 +26,7 @@ import { useTheme } from 'styled-components'; import ErrorPage from '../../errorPage/ErrorPage'; import { $Notification } from '../../Notification/Notification.sc'; import NotificationView from '../../notificationView/NotificationView'; +import { $AskemContainer, $AskemItem } from '../Applications.sc'; import { usePageContent } from './usePageContent'; const stepperCss = { @@ -49,8 +52,12 @@ const PageContent: React.FC = () => { } = usePageContent(); const theme = useTheme(); - const router = useRouter(); + const canShowAskem = useAskem( + router.locale, + isSubmittedApplication, + isLoading + ); useEffect(() => { if (isReadOnly) document.title = t('common:pageTitles.viewApplication'); @@ -83,16 +90,30 @@ const PageContent: React.FC = () => { if (isSubmittedApplication) { return ( - + <> + + {canShowAskem && ( + + <$Hr /> + <$AskemContainer> + <$AskemItem /> + <$AskemItem> +
+ + + <$Hr /> + + )} + ); } diff --git a/frontend/benefit/applicant/src/components/cookieConsent/CookieConsent.tsx b/frontend/benefit/applicant/src/components/cookieConsent/CookieConsent.tsx index d3959ca4c5..180aef3344 100644 --- a/frontend/benefit/applicant/src/components/cookieConsent/CookieConsent.tsx +++ b/frontend/benefit/applicant/src/components/cookieConsent/CookieConsent.tsx @@ -1,19 +1,21 @@ +import { + ASKEM_HOSTNAME, + SUPPORTED_LANGUAGES, +} from 'benefit/applicant/constants'; import useLocale from 'benefit/applicant/hooks/useLocale'; import { BackendEndpoint } from 'benefit-shared/backend-api/backend-api'; import { CookieModal } from 'hds-react'; import { useRouter } from 'next/router'; -import { useTranslation } from 'next-i18next'; import React from 'react'; import useBackendAPI from 'shared/hooks/useBackendAPI'; const CookieConsent: React.FC = () => { const locale = useLocale(); const router = useRouter(); - const { t } = useTranslation(); const { axios } = useBackendAPI(); const { pathname, asPath, query } = router; - const onLanguageChange = (newLanguage: 'fi' | 'sv' | 'en'): void => { + const onLanguageChange = (newLanguage: SUPPORTED_LANGUAGES): void => { void axios.get(BackendEndpoint.USER_OPTIONS, { params: { lang: newLanguage }, }); @@ -23,14 +25,56 @@ const CookieConsent: React.FC = () => { }); }; + type CookieTexts = { + title: { fi: string; sv: string; en: string }; + askemDescription: { fi: string; sv: string; en: string }; + }; + + const text: CookieTexts = { + title: { + fi: 'Helsinki-lisä', + sv: 'Helsingforstillägg', + en: 'Helsinki benefit', + }, + askemDescription: { + fi: 'Askem-reaktionappien toimintaan liittyvä tietue.', + sv: 'En post relaterad till driften av reaktionsknappen Askem.', + en: 'A record related to the operation of the Askem react buttons.', + }, + }; + + const askemDescription = text.askemDescription[locale]; + const contentSource = { - siteName: t('common:appName'), + siteName: text.title[locale], currentLanguage: locale, optionalCookies: { - cookies: [ + groups: [ { commonGroup: 'statistics', - commonCookie: 'matomo', + cookies: [ + { + id: 'rns', + name: 'rnsbid', + hostName: ASKEM_HOSTNAME, + description: askemDescription, + expiration: '-', + }, + { + id: 'rnsbid_ts', + name: 'rnsbid_ts', + hostName: ASKEM_HOSTNAME, + description: askemDescription, + expiration: '-', + }, + { + id: 'rns_reaction', + name: 'rns_reaction_*', + hostName: ASKEM_HOSTNAME, + description: askemDescription, + expiration: '-', + }, + ], }, ], }, @@ -38,11 +82,6 @@ const CookieConsent: React.FC = () => { onLanguageChange, }, focusTargetSelector: '#focused-element-after-cookie-consent-closed', - onAllConsentsGiven: (consents: { matomo: boolean }) => { - if (!consents.matomo) { - console.log('stop matomo'); - } - }, }; return ; diff --git a/frontend/benefit/applicant/src/components/notificationView/NotificatinsView.sc.ts b/frontend/benefit/applicant/src/components/notificationView/NotificatinsView.sc.ts index 5689ede132..fb46df5c7e 100644 --- a/frontend/benefit/applicant/src/components/notificationView/NotificatinsView.sc.ts +++ b/frontend/benefit/applicant/src/components/notificationView/NotificatinsView.sc.ts @@ -5,14 +5,13 @@ import styled from 'styled-components'; export const $Notification = styled.div` display: flex; flex-direction: column; - margin: ${(props) => props.theme.spacing.xl} 0 - ${(props) => props.theme.spacing.xl} 0; + margin-top: ${(props) => props.theme.spacing.xl}; padding: ${(props) => props.theme.spacing.l}; background: ${(props) => props.theme.colors.fogLight}; ${respondAbove('sm')` flex-direction: row; - + `} `; diff --git a/frontend/benefit/applicant/src/constants.ts b/frontend/benefit/applicant/src/constants.ts index eac634f0b7..45098760a8 100644 --- a/frontend/benefit/applicant/src/constants.ts +++ b/frontend/benefit/applicant/src/constants.ts @@ -119,3 +119,6 @@ export enum LOCAL_STORAGE_KEYS { IS_TERMS_OF_SERVICE_APPROVED = 'isTermsOfServiceApproved', CSRF_TOKEN = 'csrfToken', } + +export const ASKEM_SCRIPT_URL = 'https://cdn.reactandshare.com/plugin/rns.js'; +export const ASKEM_HOSTNAME = 'reactandshare.com'; diff --git a/frontend/benefit/applicant/src/hooks/useAnalytics.ts b/frontend/benefit/applicant/src/hooks/useAnalytics.ts new file mode 100644 index 0000000000..01e87045da --- /dev/null +++ b/frontend/benefit/applicant/src/hooks/useAnalytics.ts @@ -0,0 +1,34 @@ +import { useEffect } from 'react'; + +import { ASKEM_SCRIPT_URL } from '../constants'; +import { canShowAskem } from '../utils/cookie'; + +export const useAskem = ( + lang: string | undefined, + isSubmittedApplication: boolean, + isLoading: boolean +): boolean => { + const showAskem = canShowAskem(lang); + useEffect(() => { + if (!canShowAskem || isLoading) { + return () => {}; + } + + const script = document.createElement('script'); + // eslint-disable-next-line scanjs-rules/assign_to_src + script.src = ASKEM_SCRIPT_URL; + script.type = 'text/javascript'; + + window.rnsData = { + apiKey: process.env.NEXT_PUBLIC_ASKEM_API_KEY, + title: 'Helsinki-lisä', + canonicalUrl: window.location.href, + }; + + document.body.append(script); + return () => { + script.remove(); + }; + }, [lang, isSubmittedApplication, isLoading, showAskem]); + return showAskem; +}; diff --git a/frontend/benefit/applicant/src/pages/_app.tsx b/frontend/benefit/applicant/src/pages/_app.tsx index 9a3beaa172..9622d51a33 100644 --- a/frontend/benefit/applicant/src/pages/_app.tsx +++ b/frontend/benefit/applicant/src/pages/_app.tsx @@ -33,6 +33,8 @@ const App: React.FC = (appProps) => { const { t } = useTranslation(); + const showCookieBanner = process.env.NEXT_PUBLIC_SHOW_COOKIE_BANNER === '1'; + useEffect(() => { setAppLoaded(); switch (router.route) { @@ -62,7 +64,7 @@ const App: React.FC = (appProps) => { > - + {showCookieBanner && } ; } + +type RNSData = { + apiKey: string; + title: string; + canonicalUrl: string; +}; + +export type ConsentsCookie = { + 'city-of-helsinki-cookie-consents': boolean; + rns: boolean; + rnsbid_ts: boolean; + rns_reaction: boolean; +}; + +declare global { + interface Window { + rnsData: RNSData; + } +} diff --git a/frontend/benefit/applicant/src/utils/cookie.ts b/frontend/benefit/applicant/src/utils/cookie.ts new file mode 100644 index 0000000000..0acf31900a --- /dev/null +++ b/frontend/benefit/applicant/src/utils/cookie.ts @@ -0,0 +1,19 @@ +import { getLastCookieValue } from 'shared/cookies/get-last-cookie-value'; + +import type { ConsentsCookie } from '../types/common'; + +const parseConsentsCookie = (): ConsentsCookie | undefined => { + const consentsCookieEncoded = getLastCookieValue( + 'city-of-helsinki-cookie-consents' + ); + if (!consentsCookieEncoded) { + return undefined; + } + const consentsCookieString = decodeURIComponent(consentsCookieEncoded); + return JSON.parse(consentsCookieString) as ConsentsCookie; +}; + +export const canShowAskem = (lang: string): boolean => { + const consentsCookie = parseConsentsCookie(); + return consentsCookie && consentsCookie.rns && lang === 'fi'; +};