diff --git a/locales-pending/premium.ftl b/locales-pending/premium.ftl index 73dda9884..cbfce53af 100644 --- a/locales-pending/premium.ftl +++ b/locales-pending/premium.ftl @@ -517,3 +517,6 @@ leaked-security-questions-steps-subtitle = This requires access to your account, # $breach_name is the name of the breach where the security questions were found. leaked-security-questions-step-one = Update your security questions on { $breach_name }. leaked-security-questions-step-two = Update them on any other site where you used the same security questions. Be sure to use different security questions for every account. + +# Subscription +subscription-check-loading = Loading, please wait… diff --git a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/data-broker-profiles/welcome-to-premium/page.tsx b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/data-broker-profiles/welcome-to-premium/page.tsx index 0700dc90e..6b9e31334 100644 --- a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/data-broker-profiles/welcome-to-premium/page.tsx +++ b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/data-broker-profiles/welcome-to-premium/page.tsx @@ -16,6 +16,7 @@ import { getExposureReduction, } from "../../../../../../../../functions/server/dashboard"; import { Button } from "../../../../../../../../components/server/Button"; +import { hasPremium } from "../../../../../../../../functions/universal/user"; export default async function WelcomeToPremium() { const l10n = getL10n(); @@ -25,6 +26,11 @@ export default async function WelcomeToPremium() { redirect("/redesign/user/dashboard/"); } + // The user may have subscribed and just need their session updated - they will be redirected back to try again if it looks valid. + if (!hasPremium(session.user)) { + redirect(`${process.env.NEXTAUTH_URL}/redesign/user/dashboard/subscribed`); + } + const result = await getOnerepProfileId(session.user.subscriber.id); const profileId = result[0]["onerep_profile_id"] ?? -1; const scanResultItems = diff --git a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/subscribed/page.tsx b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/subscribed/page.tsx index ddc00f3ae..f03ab4bd2 100644 --- a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/subscribed/page.tsx +++ b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/subscribed/page.tsx @@ -2,77 +2,50 @@ * 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 { getServerSession } from "next-auth"; -import { getOnerepProfileId } from "../../../../../../../db/tables/subscribers"; -import { authOptions } from "../../../../../../api/utils/auth"; -import { - activateProfile, - getAllScanResults, - isEligibleForPremium, - optoutProfile, -} from "../../../../../../functions/server/onerep"; -import { - getLatestOnerepScanResults, - addOnerepScanResults, -} from "../../../../../../../db/tables/onerep_scans"; -import { getCountryCode } from "../../../../../../functions/server/getCountryCode"; -import { headers } from "next/headers"; +"use client"; -export default async function Subscribed() { - const session = await getServerSession(authOptions); - if (!session?.user?.subscriber) { - throw new Error("No session"); - } +import { useSession } from "next-auth/react"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; - if (!(await isEligibleForPremium(session.user, getCountryCode(headers())))) { - throw new Error("Not eligible for premium"); - } +import { hasPremium } from "../../../../../../functions/universal/user"; +import { captureException } from "@sentry/browser"; +import { useL10n } from "../../../../../../hooks/l10n"; - const result = await getOnerepProfileId(session.user.subscriber.id); - const profileId = result[0]["onerep_profile_id"] as number; +/** + * Client-side page to update session info. + * + * Next-Auth does not have a simple way to do this purely from the server-side, so we + * use this page to check and redirect appropriately. + * + * NOTE: this does not replace doing server-side `hasPremium` checks! This is just + * a convenience so users do not need to sign out and back in to refresh their session + * after subscribing. + */ +export default function Subscribed() { + const l10n = useL10n(); + const { update } = useSession(); + const router = useRouter(); - try { - await activateProfile(profileId); - await optoutProfile(profileId); - } catch (ex) { - console.debug(ex); // TODO handle - } + useEffect(() => { + async function updateSession() { + try { + const result = await update(); + if (hasPremium(result?.user)) { + router.replace( + `/redesign/user/dashboard/fix/data-broker-profiles/welcome-to-premium` + ); + } else { + router.replace(`/`); + } + } catch (ex) { + console.error(ex); + captureException(ex); + router.replace(`/`); + } + } + void updateSession(); + }, [update, router]); - const dev = - process.env.NODE_ENV === "development" || process.env.APP_ENV === "heroku"; - const latestScan = await getLatestOnerepScanResults(profileId); - if (!latestScan.scan) { - throw new Error("Must have performed manual scan"); - } - - const scans = await getAllScanResults(profileId); - - // In dev mode, record scans every time this page is reloaded. - // The webhook does this in production. - if (dev) { - await addOnerepScanResults( - profileId, - latestScan.scan.onerep_scan_id, - scans, - "initial", - latestScan.scan.onerep_scan_status - ); - } - - return ( -
-
-

You are now subscribed

- {dev ? ( -
-

Dev mode enabled

-

Reload this page to update scan

-
{JSON.stringify(scans, null, 2)}
-
- ) : ( - "" - )} -
-
- ); + return
{l10n.getString("subscription-check-loading")}
; } diff --git a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/unsubscribed/page.tsx b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/unsubscribed/page.tsx deleted file mode 100644 index 6259d83a4..000000000 --- a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/unsubscribed/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* 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 { getServerSession } from "next-auth"; -import { getOnerepProfileId } from "../../../../../../../db/tables/subscribers"; -import { authOptions } from "../../../../../../api/utils/auth"; -import { - deactivateProfile, - isEligibleForPremium, -} from "../../../../../../functions/server/onerep"; -import { getCountryCode } from "../../../../../../functions/server/getCountryCode"; -import { headers } from "next/headers"; - -export default async function Unsubscribed() { - const session = await getServerSession(authOptions); - if (!session?.user?.subscriber) { - throw new Error("No session"); - } - - if (!(await isEligibleForPremium(session.user, getCountryCode(headers())))) { - throw new Error("Not eligible for premium"); - } - - const result = await getOnerepProfileId(session.user.subscriber.id); - const profileId = result[0]["onerep_profile_id"] as number; - - try { - await deactivateProfile(profileId); - } catch (ex) { - console.debug(ex); // TODO handle - } - - return ( -
-
-

You are now unsubscribed

-
-
- ); -} diff --git a/src/app/(proper_react)/redesign/(authenticated)/user/welcome/remove/page.tsx b/src/app/(proper_react)/redesign/(authenticated)/user/welcome/remove/page.tsx deleted file mode 100644 index 14d751881..000000000 --- a/src/app/(proper_react)/redesign/(authenticated)/user/welcome/remove/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* 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 { getOnerepProfileId } from "../../../../../../../db/tables/subscribers"; -import { getServerSession } from "next-auth"; -import { authOptions } from "../../../../../../api/utils/auth"; -import { - activateProfile, - optoutProfile, -} from "../../../../../../functions/server/onerep"; -import Script from "next/script"; -import { isUserSubscribed } from "../../../../../../functions/server/isUserSubscribed"; - -export function generateMetadata() { - return { - title: "Welcome - Results", - }; -} - -export default async function UserWelcomeRemove() { - const session = await getServerSession(authOptions); - if (!session?.user?.subscriber?.id) { - throw new Error("No session"); - } - - if (!(await isUserSubscribed())) { - return ( - - ); - } - - const result = await getOnerepProfileId(session.user.subscriber.id); - const profileId = result[0]["onerep_profile_id"] as number; - await activateProfile(profileId); - await optoutProfile(profileId); - - return ( -
-

Profile activated and removal has started

-
- ); -} diff --git a/src/app/api/utils/auth.ts b/src/app/api/utils/auth.ts index 072086d98..1df283843 100644 --- a/src/app/api/utils/auth.ts +++ b/src/app/api/utils/auth.ts @@ -63,15 +63,8 @@ export const authOptions: AuthOptions = { }, token: AppConstants.OAUTH_TOKEN_URI, userinfo: { - request: async (context) => { - const response = await fetch(AppConstants.OAUTH_PROFILE_URI, { - headers: { - Authorization: `Bearer ${context.tokens.access_token ?? ""}`, - }, - }); - const userInfo = (await response.json()) as Profile; - return userInfo; - }, + request: async (context) => + fetchUserInfo(context.tokens.access_token ?? ""), }, clientId: AppConstants.OAUTH_CLIENT_ID, clientSecret: AppConstants.OAUTH_CLIENT_SECRET, @@ -86,14 +79,16 @@ export const authOptions: AuthOptions = { twoFactorAuthentication: profile.twoFactorAuthentication, metricsEnabled: profile.metricsEnabled, locale: profile.locale, - }; + } as Profile; }, }, ], callbacks: { // Unused arguments also listed to show what's available: - // eslint-disable-next-line @typescript-eslint/no-unused-vars async jwt({ token, account, profile, trigger }) { + if (trigger === "update") { + profile = await fetchUserInfo(token.subscriber?.fxa_access_token ?? ""); + } if (profile) { token.fxa = { locale: profile.locale, @@ -200,6 +195,16 @@ export const authOptions: AuthOptions = { }, }; +async function fetchUserInfo(accessToken: string) { + const response = await fetch(AppConstants.OAUTH_PROFILE_URI, { + headers: { + Authorization: `Bearer ${accessToken ?? ""}`, + }, + }); + const userInfo = (await response.json()) as Profile; + return userInfo; +} + export function bearerToken(req: NextRequest) { const requestHeaders = new Headers(req.headers); requestHeaders.get("authorization"); diff --git a/src/app/functions/server/isUserSubscribed.ts b/src/app/functions/server/isUserSubscribed.ts deleted file mode 100644 index d57bf3d45..000000000 --- a/src/app/functions/server/isUserSubscribed.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* 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 { getServerSession } from "next-auth"; -import { authOptions } from "../../api/utils/auth"; - -type FxaSubscriptionResponse = { - subscriptions: Array<{ - product_id: string; - plan_id: string; - status: "active"; - }>; -}; - -export const isUserSubscribed = async () => { - // Fetch list of subscriptions. - let subscriptions: FxaSubscriptionResponse; - - const session = await getServerSession(authOptions); - if (!session?.user?.subscriber?.id) { - throw new Error("No session"); - } - - const bearerToken = session?.user.subscriber?.fxa_access_token; - if (bearerToken) { - const url = `${ - process.env.OAUTH_API_URI ?? "" - }/oauth/mozilla-subscriptions/customer/billing-and-subscriptions`; - const result = await fetch(url, { - headers: { - "Content-Type": "application/json", - authorization: `Bearer ${bearerToken}`, - }, - }); - if (result.ok) { - subscriptions = await result.json(); - for (const subscription of subscriptions.subscriptions) { - if ( - subscription.product_id === - process.env.NEXT_PUBLIC_PREMIUM_PRODUCT_ID && - (subscription.plan_id === - process.env.NEXT_PUBLIC_PREMIUM_PLAN_ID_MONTHLY_US || - subscription.plan_id === - process.env.NEXT_PUBLIC_PREMIUM_PLAN_ID_YEARLY_US) && - subscription.status === "active" - ) { - return true; - } - } - return false; - } - } else { - console.error("User has no bearer token"); - } -};