Skip to content

Commit

Permalink
Merge pull request #5104 from mozilla/mntor-3492
Browse files Browse the repository at this point in the history
Add support for Mozilla Accounts prompt none auth flow
  • Loading branch information
flozia authored Oct 10, 2024
2 parents 0995483 + 8fa072e commit 239e452
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/app/(proper_react)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -57,6 +58,9 @@ export default async function Layout({ children }: { children: ReactNode }) {
<L10nProvider bundleSources={l10nBundles}>
<ReactAriaI18nProvider locale={getLocale(l10nBundles)}>
<CountryCodeProvider countryCode={countryCode}>
{enabledFlags.includes("PromptNoneAuthFlow") && !session && (
<PromptNoneAuth />
)}
{children}
<PageLoadEvent
experimentationId={getExperimentationId(session?.user ?? null)}
Expand Down
20 changes: 19 additions & 1 deletion src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,27 @@
* 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 { NextApiRequest, NextApiResponse } from "next";
import { NextResponse } from "next/server";
import NextAuth from "next-auth";
import { authOptions } from "../../utils/auth";

const handler = NextAuth(authOptions);
// There is currently no support for handling OAuth provider callback errors:
// https://github.com/nextauthjs/next-auth/discussions/8209
const handler = async (
req: Request & NextApiRequest,
res: Request & NextApiResponse,
) => {
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<Response>;
};

export { handler as GET, handler as POST };
33 changes: 33 additions & 0 deletions src/app/components/client/PromptNoneAuth.tsx
Original file line number Diff line number Diff line change
@@ -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;
};
79 changes: 79 additions & 0 deletions src/app/functions/universal/attributions.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
9 changes: 9 additions & 0 deletions src/app/functions/universal/attributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,12 @@ export function modifyAttributionsForUrlSearchParams(

return searchParams;
}

export const containsExpectedSearchParams = (
expectedSearchParams: Record<string, string>,
searchParams: URLSearchParams,
) =>
Object.keys(expectedSearchParams).every(
(searchParamKey) =>
searchParams.get(searchParamKey) === expectedSearchParams[searchParamKey],
);
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 1 addition & 0 deletions src/db/tables/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const featureFlagNames = [
"PetitionBannerCsatSurvey",
"MonthlyReportFreeUser",
"BreachEmailRedesign",
"PromptNoneAuthFlow",
"GA4SubscriptionEvents",
] as const;
export type FeatureFlagName = (typeof featureFlagNames)[number];
Expand Down

0 comments on commit 239e452

Please sign in to comment.