diff --git a/src/app/(proper_react)/layout.tsx b/src/app/(proper_react)/layout.tsx index da44fc8d187..8d4d059e4b7 100644 --- a/src/app/(proper_react)/layout.tsx +++ b/src/app/(proper_react)/layout.tsx @@ -14,6 +14,7 @@ import { getCountryCode } from "../functions/server/getCountryCode"; import { PageLoadEvent } from "../components/client/PageLoadEvent"; import { getExperimentationId } from "../functions/server/getExperimentationId"; import { getEnabledFeatureFlags } from "../../db/tables/featureFlags"; +import { PromptNoneAuth } from "../components/client/PromptNoneAuth"; import { addClientIdForSubscriber } from "../../db/tables/google_analytics_clients"; import { logger } from "../functions/server/logging"; @@ -57,6 +58,9 @@ export default async function Layout({ children }: { children: ReactNode }) { + {enabledFlags.includes("PromptNoneAuthFlow") && !session && ( + + )} {children} { + if ( + req.method === "GET" && + req.url?.startsWith( + `${process.env.SERVER_URL}/api/auth/callback/fxa?error=`, + ) + ) { + return NextResponse.redirect(`${process.env.SERVER_URL}/user/dashboard`); + } + + return NextAuth(req, res, authOptions) as Promise; +}; export { handler as GET, handler as POST }; diff --git a/src/app/components/client/PromptNoneAuth.tsx b/src/app/components/client/PromptNoneAuth.tsx new file mode 100644 index 00000000000..0c024c9bac4 --- /dev/null +++ b/src/app/components/client/PromptNoneAuth.tsx @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use client"; + +import { ReactNode, useEffect } from "react"; +import { useSearchParams } from "next/navigation"; +import { signIn } from "next-auth/react"; +import { containsExpectedSearchParams } from "../../functions/universal/attributions"; +import { CONST_MOZILLA_ACCOUNTS_SETTINGS_PROMO_SEARCH_PARAMS } from "../../../constants"; + +export const PromptNoneAuth = (): ReactNode => { + const searchParams = useSearchParams(); + + useEffect(() => { + const isPromptNoneAuthAttempt = containsExpectedSearchParams( + CONST_MOZILLA_ACCOUNTS_SETTINGS_PROMO_SEARCH_PARAMS, + searchParams, + ); + if (isPromptNoneAuthAttempt) { + void signIn( + "fxa", + { callbackUrl: "/user/dashboard" }, + { prompt: "none" }, + ); + } + // This effect should only run once + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return null; +}; diff --git a/src/app/functions/universal/attributions.test.ts b/src/app/functions/universal/attributions.test.ts new file mode 100644 index 00000000000..2527fab6aeb --- /dev/null +++ b/src/app/functions/universal/attributions.test.ts @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { it, expect } from "@jest/globals"; +import { containsExpectedSearchParams } from "./attributions"; + +it("returns `true` when the actual search params are matching the expected params", () => { + expect( + containsExpectedSearchParams( + { + one: "1", + two: "2", + }, + new URLSearchParams({ + one: "1", + two: "2", + }), + ), + ).toBe(true); +}); + +it("returns `true` when the actual search params are a superset of the expected params", () => { + expect( + containsExpectedSearchParams( + { + one: "1", + two: "2", + }, + new URLSearchParams({ + one: "1", + two: "2", + three: "3", + }), + ), + ).toBe(true); +}); + +it("returns `false` if any expected param is missing from the actual search params", () => { + expect( + containsExpectedSearchParams( + { + one: "1", + two: "2", + }, + new URLSearchParams({ + two: "2", + }), + ), + ).toBe(false); +}); + +it("returns `false` if any of the actual search param keys are not matching the expected params", () => { + expect( + containsExpectedSearchParams( + { + one: "1", + three: "2", + }, + new URLSearchParams({ + two: "2", + }), + ), + ).toBe(false); +}); + +it("returns `false` if any of the actual search param values are not matching the expected params", () => { + expect( + containsExpectedSearchParams( + { + one: "1", + two: "3", + }, + new URLSearchParams({ + two: "2", + }), + ), + ).toBe(false); +}); diff --git a/src/app/functions/universal/attributions.ts b/src/app/functions/universal/attributions.ts index c12e6f5c7a4..0ff0a9db73a 100644 --- a/src/app/functions/universal/attributions.ts +++ b/src/app/functions/universal/attributions.ts @@ -43,3 +43,12 @@ export function modifyAttributionsForUrlSearchParams( return searchParams; } + +export const containsExpectedSearchParams = ( + expectedSearchParams: Record, + searchParams: URLSearchParams, +) => + Object.keys(expectedSearchParams).every( + (searchParamKey) => + searchParams.get(searchParamKey) === expectedSearchParams[searchParamKey], + ); diff --git a/src/constants.ts b/src/constants.ts index df2da02d826..c9951f57154 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -33,3 +33,8 @@ export const CONST_URL_MONITOR_GITHUB = export const CONST_DAY_MILLISECONDS = 24 * 60 * 60 * 1000; export const CONST_URL_MONITOR_LANDING_PAGE_ID = "monitor.mozilla.org-monitor-product-page"; +export const CONST_MOZILLA_ACCOUNTS_SETTINGS_PROMO_SEARCH_PARAMS = { + utm_source: "moz-account", + utm_campaign: "settings-promo", + utm_content: "monitor-free", +} as const; diff --git a/src/db/tables/featureFlags.ts b/src/db/tables/featureFlags.ts index e7a7d3a99f0..1df406204cb 100644 --- a/src/db/tables/featureFlags.ts +++ b/src/db/tables/featureFlags.ts @@ -50,6 +50,7 @@ export const featureFlagNames = [ "PetitionBannerCsatSurvey", "MonthlyReportFreeUser", "BreachEmailRedesign", + "PromptNoneAuthFlow", "GA4SubscriptionEvents", ] as const; export type FeatureFlagName = (typeof featureFlagNames)[number];