From 2b9bc8c3ce109cfe1299a5b90229a36594af9c70 Mon Sep 17 00:00:00 2001 From: "Chuma Mcphoy (We Ahead)" Date: Fri, 5 Dec 2025 05:47:11 +0000 Subject: [PATCH] Merged in feat/LOY-497-Flag-Profiling-Consent (pull request #3292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(LOY-497): hide profiling consent behind feature flag * refactor(LOY-497): hide profiling consent behind feature flag * chore(LOY-497): up to date consent readme Approved-by: Matilda Landström --- .../(live)/(protected)/my-pages/layout.tsx | 47 ++++- .../my-pages/profile/consent/page.tsx | 11 ++ .../SignupFormWrapper/index.tsx | 9 +- .../components/Forms/Signup/index.tsx | 81 +++++---- .../Profile/CommunicationSettings/index.tsx | 4 +- .../MyPages/ProfilingConsent/Banner/index.tsx | 3 + .../MyPages/ProfilingConsent/README.md | 163 +++++++++++------- apps/scandic-web/env/server.ts | 6 + 8 files changed, 216 insertions(+), 108 deletions(-) diff --git a/apps/scandic-web/app/[lang]/(live)/(protected)/my-pages/layout.tsx b/apps/scandic-web/app/[lang]/(live)/(protected)/my-pages/layout.tsx index e5ee6b8c6..85d1cd06f 100644 --- a/apps/scandic-web/app/[lang]/(live)/(protected)/my-pages/layout.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(protected)/my-pages/layout.tsx @@ -1,6 +1,7 @@ import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK" import { getEurobonusMembership } from "@scandic-hotels/trpc/routers/user/helpers" +import { env } from "@/env/server" import { getProfileSafely, getProfilingConsent, @@ -17,12 +18,22 @@ import { ModalTracking } from "@/utils/tracking/profilingConsent" import styles from "./layout.module.css" -export default async function MyPagesLayout({ +type MyPagesLayoutProps = React.PropsWithChildren<{ + breadcrumbs: React.ReactNode +}> + +export default async function MyPagesLayout(props: MyPagesLayoutProps) { + if (env.ENABLE_PROFILE_CONSENT) { + return + } + + return +} + +async function MyPagesLayoutWithConsent({ breadcrumbs, children, -}: React.PropsWithChildren<{ - breadcrumbs: React.ReactNode -}>) { +}: MyPagesLayoutProps) { const profile = await getProfileSafely() const eurobonusMembership = profile?.loyalty ? getEurobonusMembership(profile.loyalty) @@ -37,6 +48,8 @@ export default async function MyPagesLayout({ const lang = await getLang() + const showConsentModal = memberKey && profilingConsent && !hasConsent + return (
@@ -51,7 +64,7 @@ export default async function MyPagesLayout({ {eurobonusMembership && } - {memberKey && profilingConsent && !hasConsent ? ( + {showConsentModal && ( <> - ) : null} + )}
) } + +async function MyPagesLayoutBase({ + breadcrumbs, + children, +}: MyPagesLayoutProps) { + const profile = await getProfileSafely() + const eurobonusMembership = profile?.loyalty + ? getEurobonusMembership(profile.loyalty) + : null + + return ( +
+
+ {breadcrumbs} +
{children}
+
+ + {eurobonusMembership && } + +
+ ) +} diff --git a/apps/scandic-web/app/[lang]/(live)/(protected)/my-pages/profile/consent/page.tsx b/apps/scandic-web/app/[lang]/(live)/(protected)/my-pages/profile/consent/page.tsx index 2468ef75e..576b128a3 100644 --- a/apps/scandic-web/app/[lang]/(live)/(protected)/my-pages/profile/consent/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(protected)/my-pages/profile/consent/page.tsx @@ -1,13 +1,24 @@ +import { redirect } from "next/navigation" + +import { profile } from "@scandic-hotels/common/constants/routes/myPages" import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK" +import { env } from "@/env/server" import { getProfile } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import { ProfilingConsent } from "@/components/Forms/ProfilingConsent" +import { getLang } from "@/i18n/serverContext" import styles from "./page.module.css" export default async function ProfilingConsentSlot() { + const lang = await getLang() + + if (!env.ENABLE_PROFILE_CONSENT) { + redirect(profile[lang]) + } + const caller = await serverClient() const accountPage = await caller.contentstack.accountPage.get() const user = await getProfile() diff --git a/apps/scandic-web/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx index 089339bd1..2d20f9864 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx @@ -1,3 +1,5 @@ +import { env } from "@/env/server" + import SignupForm from "@/components/Forms/Signup" import type { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent" @@ -5,5 +7,10 @@ import type { SignupFormWrapperProps } from "@/types/components/blocks/dynamicCo export default async function SignupFormWrapper({ dynamic_content, }: SignupFormWrapperProps) { - return + return ( + + ) } diff --git a/apps/scandic-web/components/Forms/Signup/index.tsx b/apps/scandic-web/components/Forms/Signup/index.tsx index 7250f0ae6..cb03bf8e8 100644 --- a/apps/scandic-web/components/Forms/Signup/index.tsx +++ b/apps/scandic-web/components/Forms/Signup/index.tsx @@ -43,9 +43,14 @@ import styles from "./form.module.css" interface SignUpFormProps { title: string + enableProfileConsent?: boolean } -export default function SignupForm({ title }: SignUpFormProps) { +export default function SignupForm({ + title, + // Handled as a prop rather than a client env var due to limits in Netlify env var size. + enableProfileConsent = false, +}: SignUpFormProps) { const intl = useIntl() const router = useRouter() const lang = useLang() @@ -130,7 +135,7 @@ export default function SignupForm({ title }: SignUpFormProps) { return (
- + {enableProfileConsent && } {title ? (

{title}

@@ -278,41 +283,43 @@ export default function SignupForm({ title }: SignUpFormProps) { /> -
-
- -

- {intl.formatMessage({ - id: "signup.UnlockYourPersonalizedExperience", - defaultMessage: "Unlock your personalized experience!", - })} -

-
-
- - {intl.formatMessage({ - id: "signup.yesConsent", - defaultMessage: - "I consent to Scandic using my information to give me even more personalized travel inspiration and offers from Scandic and trusted Scandic Friends partners. This means Scandic may use information about my interactions with Scandic Friends partners, and share details of my interactions with Scandic with those partners, to make the experience even more relevant to me.", - })} - - - {intl.formatMessage({ - id: "signup.ReadMoreAboutPersonalization", - defaultMessage: "Read more about personalization at Scandic", - })} - -
+ {enableProfileConsent && ( +
+
+ +

+ {intl.formatMessage({ + id: "signup.UnlockYourPersonalizedExperience", + defaultMessage: "Unlock your personalized experience!", + })} +

+
+
+ + {intl.formatMessage({ + id: "signup.yesConsent", + defaultMessage: + "I consent to Scandic using my information to give me even more personalized travel inspiration and offers from Scandic and trusted Scandic Friends partners. This means Scandic may use information about my interactions with Scandic Friends partners, and share details of my interactions with Scandic with those partners, to make the experience even more relevant to me.", + })} + + + {intl.formatMessage({ + id: "signup.ReadMoreAboutPersonalization", + defaultMessage: "Read more about personalization at Scandic", + })} + +
+ )}
diff --git a/apps/scandic-web/components/MyPages/Profile/CommunicationSettings/index.tsx b/apps/scandic-web/components/MyPages/Profile/CommunicationSettings/index.tsx index 674241ac1..49a36849c 100644 --- a/apps/scandic-web/components/MyPages/Profile/CommunicationSettings/index.tsx +++ b/apps/scandic-web/components/MyPages/Profile/CommunicationSettings/index.tsx @@ -1,3 +1,5 @@ +import { env } from "@/env/server" + import { getIntl } from "@/i18n" import { Section } from "../Section" @@ -15,7 +17,7 @@ export async function CommunicationSettings() { })} > - + {env.ENABLE_PROFILE_CONSENT && }
) } diff --git a/apps/scandic-web/components/MyPages/ProfilingConsent/Banner/index.tsx b/apps/scandic-web/components/MyPages/ProfilingConsent/Banner/index.tsx index f6ffd1399..baa41c320 100644 --- a/apps/scandic-web/components/MyPages/ProfilingConsent/Banner/index.tsx +++ b/apps/scandic-web/components/MyPages/ProfilingConsent/Banner/index.tsx @@ -1,5 +1,6 @@ import { Typography } from "@scandic-hotels/design-system/Typography" +import { env } from "@/env/server" import { getProfile, getProfilingConsent } from "@/lib/trpc/memoizedRequests" import { GetMainIconByCSIdentifier, userHasConsent } from "../utils" @@ -8,6 +9,8 @@ import { BannerButton } from "./Button" import styles from "./profilingConsentBanner.module.css" export async function ProfilingConsentBanner() { + if (!env.ENABLE_PROFILE_CONSENT) return null + const user = await getProfile() if (!user || userHasConsent(user?.profilingConsent)) return null diff --git a/apps/scandic-web/components/MyPages/ProfilingConsent/README.md b/apps/scandic-web/components/MyPages/ProfilingConsent/README.md index f42ec5bd5..afc1ee2b6 100644 --- a/apps/scandic-web/components/MyPages/ProfilingConsent/README.md +++ b/apps/scandic-web/components/MyPages/ProfilingConsent/README.md @@ -1,83 +1,115 @@ # Profiling Consent -A full-page modal shown when a user first visits a My Pages route. If the modal is dimissed, a banner is shown on the overview page. -On `/profile`, the user can navigate to `/profile/consent` to update the consent. -On signup, it's also possible to opt in. +Profiling consent allows users to opt in/out of personalized experiences. The feature is controlled by the `ENABLE_PROFILE_CONSENT` environment variable. -## Usage +## User Journey -### My Pages Modal - -Rendered in `app/[lang]/(live)/(protected)/my-pages/layout.tsx` so it is available across all My Pages routes. The layout passes `memberKey` (derived from `membershipNumber` or `profileId`) to enable per-member dismissal tracking. - -### Signup Form Integration - -A read-only version (`Modal/ReadOnly.tsx`) is integrated into the signup form (`components/Forms/Signup/index.tsx`) to provide users with information about personalization benefits during registration. This version: - -- Has no action buttons (Accept/Decline) -- Can be opened via the "Read more about personalization at Scandic" button in the signup form -- Uses the same Contentstack content as the main modal -- Does not require a `memberKey` since it's for non-authenticated users - -## Features - -### My Pages Modal - -- Displays upon landing on any My Pages route (if not previously dismissed) -- Shows Scandic logo, title, lead text, benefit cards and an accordion -- Close via "X" button only (no overlay click or ESC key) -- Dismissal persisted in `localStorage` per member -- Includes Accept/Decline action buttons - -### Read-Only Version (Signup) - -- Same visual content as main modal but without action buttons -- Accessible during signup process via "Read more" button -- Uses Contentstack content fetched via `trpc.contentstack.profilingConsent.get.useQuery` -- No localStorage persistence since it's informational only +1. **Signup**: Optional consent checkbox in registration form +2. **My Pages Modal**: Prompt shown on first visit (if no decision made) +3. **Banner**: Shown on overview page if modal was dismissed without deciding +4. **Profile Settings**: Dedicated page at `/profile/consent` to update consent at any time ## Components -- `Modal/` — Main modal shell with header, content, and action buttons -- `Modal/ReadOnly.tsx` — Read-only version without action buttons, used in signup form +### Modal (`Modal/`) + +Full-page modal shown when a user first visits a My Pages route. + +- Rendered in `app/[lang]/(live)/(protected)/my-pages/layout.tsx` +- Only shows if user hasn't made a consent decision (`profilingConsent` is `undefined`) +- Uses `memberKey` (from `membershipNumber` or `profileId`) for dismissal tracking +- Close via "X" button only (no overlay click or ESC key) +- Accept/Decline buttons update consent via API + +**Sub-components:** + - `Modal/BenefitCards/` — Cards showcasing personalization benefits -- `Accordion/` — Privacy and personalization information -- `Banner/` — A banner shown on the account overview page that can reopen the modal +- `Modal/ReadOnly.tsx` — Informational version without action buttons (used in signup) -## Banner +### Banner (`Banner/`) -- Purpose: Offer a way to reopen the Profiling Consent modal later when the user is ready to decide. -- Visibility: Decided server-side in `Banner/index.tsx` (render only when consent status is pending once API is available). -- Behavior: The client CTA dispatches a `profiling-consent:open` event to reopen the modal. +Shown on the account overview page when consent is pending. -## Local Persistence +- Server component that checks `userHasConsent()` before rendering +- CTA button dispatches `profiling-consent:open` event to reopen the modal +- Added to overview via Contentstack Dynamic Content block + +### Alert (`Alert/`) + +Feedback alert shown after consent actions in My Pages. + +- `ProfilingConsentAlertProvider` — Context provider in My Pages layout +- Shows success/error states after accepting/declining consent +- Success alert includes link to edit preferences + +### Consent Form (`components/Forms/ProfilingConsent/`) + +Dedicated form on `/profile/consent` page for managing consent. + +- Radio button selection for Accept/Decline +- Shows current consent status +- Redirects to profile after saving +- Includes accordion with privacy information + +### Signup Integration + +The signup form (`components/Forms/Signup/index.tsx`) includes: + +- Checkbox to opt-in during registration +- "Read more" link that opens `Modal/ReadOnly.tsx` + +### Profile Settings + +`PersonalizationSlot` in `components/MyPages/Profile/CommunicationSettings/` provides a link to the consent management page. + +### Accordion (`Accordion/`) + +Expandable section with privacy and personalization details. Used in both the modal and consent form. + +## API + +### tRPC Mutations + +- `trpc.user.profilingConsent.update` — Update user's consent preference +- `trpc.user.profilingConsentPromptDate.update` — Record when user was prompted + +### tRPC Queries + +- `trpc.contentstack.profilingConsent.get` — Fetch modal/banner content from CMS + +## Hooks + +- `useUpdateProfilingConsent` (`hooks/useUpdateProfilingConsent.ts`) — Handles consent update mutation with alert feedback via `ProfilingConsentAlertProvider` + +## Local Storage + +Modal dismissal is tracked per-member to control auto-open behavior: - Key: `profiling-consent:dismissed:` -- Set when the modal is closed via the header close button. -- This flag only controls auto-open behavior; it does not reflect Accept/Decline (those are handled via API and used server-side to decide banner visibility). +- Set when modal is closed via header close button +- Does not reflect Accept/Decline (those are stored via API) ## Utilities -Located at `apps/scandic-web/utils/profilingConsent.ts`: +Located at `utils/profilingConsent.ts`: -- `storageKey(memberKey)` -- `readDismissed(memberKey)` -- `setDismissed(memberKey)` -- `clearDismissed(memberKey)` -- `profilingConsentOpenEvent` — CustomEvent name used to request the modal to open -- `requestOpen()` — Dispatches the open event +- `storageKey(memberKey)` — Generate storage key +- `readDismissed(memberKey)` — Check if modal was dismissed +- `setDismissed(memberKey)` — Mark modal as dismissed +- `clearDismissed(memberKey)` — Clear dismissal flag +- `profilingConsentOpenEvent` — CustomEvent name for opening modal +- `requestOpen()` — Dispatch event to open modal ## Testing -To re-show the modal after dismissing: +Re-show modal after dismissing: ```js -// In the browser console: localStorage.removeItem("profiling-consent:dismissed:") -// Then refresh the page +// Refresh the page ``` -To open the modal without clearing the dismissed flag: +Open modal programmatically: ```js window.dispatchEvent(new CustomEvent("profiling-consent:open")) @@ -85,13 +117,18 @@ window.dispatchEvent(new CustomEvent("profiling-consent:open")) Replace `` with the actual `membershipNumber` or `profileId`. -## Contentstack +## Contentstack Setup -Profiling Consent setup in Contentstack: +Required content for the feature: -- Profiling Consent (config) - Config needs to be created and published in respective language. -- /consent (account page) - Page needs to be created and published in respective language. -- /overview (account page) - Need to add Dynamic content: Profiling Consent Banner to respective language, and re-publish the page. +1. **Profiling Consent (config)** + + - Config needs to be created and published in each language + +2. **/consent (account page)** + + - Page needs to be created and published in each language + +3. **/overview (account page)** + - Add Dynamic content block: "Profiling Consent Banner" + - Re-publish the page for each language diff --git a/apps/scandic-web/env/server.ts b/apps/scandic-web/env/server.ts index b4a995228..492ce67c6 100644 --- a/apps/scandic-web/env/server.ts +++ b/apps/scandic-web/env/server.ts @@ -107,6 +107,11 @@ export const env = createEnv({ .refine((s) => s === "1" || s === "0") .transform((s) => s === "1") .default("0"), + ENABLE_PROFILE_CONSENT: z + .string() + .refine((s) => s === "true" || s === "false") + .transform((s) => s === "true") + .default("false"), }, emptyStringAsUndefined: true, runtimeEnv: { @@ -162,5 +167,6 @@ export const env = createEnv({ CHATBOT_LIVE_LANGS: process.env.CHATBOT_LIVE_LANGS, NEW_STAYS_ON_MY_PAGES: process.env.NEW_STAYS_ON_MY_PAGES, SEO_INERT: process.env.SEO_INERT, + ENABLE_PROFILE_CONSENT: process.env.ENABLE_PROFILE_CONSENT, }, })