зеркало из https://github.com/mozilla/fxa.git
feat(next): attempt no prompt auth on landing
Because: * Attempt to perform a no prompt authentication request to FxA when customer is first navigated to Payment Next This commit: * Adds a landing route to initiate the no prompt signin * If user is authenticated, add FxA uid to cart * Add signin error page to handle FxA error for unauthenticated users * Add placeholder generic error page. Closes #FXA-7523
This commit is contained in:
Родитель
c06042e709
Коммит
888ccaad90
|
@ -0,0 +1,53 @@
|
||||||
|
/* 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 { signIn } from 'apps/payments/next/auth';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'; // defaults to auto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This landing route will initiate the OAuth no prompt signin
|
||||||
|
* attempt with FxA.
|
||||||
|
*
|
||||||
|
* Only when an unexpected error occurs, will the user be redirected
|
||||||
|
* to a generic error page.
|
||||||
|
*
|
||||||
|
* On successful authentication, the customer will be redirected
|
||||||
|
* to the /new page in a "Signed In" state. (i.e. FxA uid added to cart)
|
||||||
|
*
|
||||||
|
* On failure the customer will be redirected to /error/auth/signin
|
||||||
|
* where the error will be handled correctly, and ideally redirect
|
||||||
|
* the customer to the /new page in a "Signed Out" state. (i.e. no
|
||||||
|
* FxA uid added to the cart)
|
||||||
|
*
|
||||||
|
* This needs to be a route handler, since the `signIn` server
|
||||||
|
* action needs to be executed from a route handler.
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
// Replace 'landing' in the URL with 'new'
|
||||||
|
const redirectToUrl = request.nextUrl.clone();
|
||||||
|
redirectToUrl.pathname = redirectToUrl.pathname.replace('/landing', '/new');
|
||||||
|
|
||||||
|
let redirectUrl;
|
||||||
|
try {
|
||||||
|
redirectUrl = await signIn(
|
||||||
|
'fxa',
|
||||||
|
{
|
||||||
|
redirect: false,
|
||||||
|
redirectTo: redirectToUrl.href,
|
||||||
|
},
|
||||||
|
{ prompt: 'none' }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// Log the error and redirect to /new without attempting signIn
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!redirectUrl) {
|
||||||
|
redirectUrl = redirectToUrl.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(redirectUrl);
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import { setupCartAction } from '@fxa/payments/ui/server';
|
import { setupCartAction } from '@fxa/payments/ui/server';
|
||||||
|
import { auth } from 'apps/payments/next/auth';
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
@ -14,19 +15,30 @@ interface CheckoutNewParams {
|
||||||
|
|
||||||
export default async function CheckoutNew({
|
export default async function CheckoutNew({
|
||||||
params,
|
params,
|
||||||
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
params: CheckoutNewParams;
|
params: CheckoutNewParams;
|
||||||
|
searchParams: Record<string, string>;
|
||||||
}) {
|
}) {
|
||||||
const { offeringId, interval } = params;
|
const { offeringId, interval } = params;
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
const fxaUid = session?.user?.id;
|
||||||
const ip = (headers().get('x-forwarded-for') ?? '127.0.0.1').split(',')[0];
|
const ip = (headers().get('x-forwarded-for') ?? '127.0.0.1').split(',')[0];
|
||||||
|
|
||||||
const { id: cartId } = await setupCartAction(
|
const { id: cartId } = await setupCartAction(
|
||||||
interval,
|
interval,
|
||||||
offeringId,
|
offeringId,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
fxaUid,
|
||||||
ip
|
ip
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const searchParamsString = new URLSearchParams(searchParams).toString();
|
||||||
|
if (searchParamsString) {
|
||||||
|
redirect(`${cartId}/start?${searchParamsString}`);
|
||||||
|
} else {
|
||||||
redirect(`${cartId}/start`);
|
redirect(`${cartId}/start`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* 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 { cookies } from 'next/headers';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
interface ErrorAuthSigninSearchParams {
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Errors that might occur during Auth SignIn
|
||||||
|
*
|
||||||
|
* Most common error would be OAuthCallbackError. This error is expected if
|
||||||
|
* no prompt signin is attempted on a user thats not currently signed into FxA.
|
||||||
|
*
|
||||||
|
* In situations like this, ideally the customer should be redirected to the
|
||||||
|
* appropriate checkout page in a "signed out" state. To achieve this the
|
||||||
|
* authjs.callback-url cookie is read to retrieve the correct redirect url,
|
||||||
|
* otherwise it's not possible to know what offering and interval the customer
|
||||||
|
* was attempting to reach.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default async function Error({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: ErrorAuthSigninSearchParams;
|
||||||
|
}) {
|
||||||
|
const fallbackErrorPagePath = '/error';
|
||||||
|
|
||||||
|
if (searchParams?.error !== 'OAuthCallbackError') {
|
||||||
|
redirect(fallbackErrorPagePath);
|
||||||
|
}
|
||||||
|
const cookieStore = cookies();
|
||||||
|
const redirectUrl = cookieStore.get('authjs.callback-url');
|
||||||
|
|
||||||
|
if (redirectUrl?.value) {
|
||||||
|
redirect(`${redirectUrl.value}`);
|
||||||
|
} else {
|
||||||
|
redirect(fallbackErrorPagePath);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
/* 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/. */
|
||||||
|
export default function ErrorGeneric() {
|
||||||
|
return <div>Generic Error Page</div>;
|
||||||
|
}
|
|
@ -12,7 +12,30 @@ export default function Index() {
|
||||||
// This page will be fixed before launch by FXA-8304
|
// This page will be fixed before launch by FXA-8304
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-center m-4">Welcome</h1>
|
<h1 className="text-xxl text-center m-4">Welcome</h1>
|
||||||
|
<div className="flex-col">
|
||||||
|
<h2 className="text-xl">With auth</h2>
|
||||||
|
<div className="flex gap-8">
|
||||||
|
<div className="flex flex-col gap-2 p-4 items-center">
|
||||||
|
<h2>123Done - Monthly</h2>
|
||||||
|
<Link
|
||||||
|
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||||
|
href="/en/123done/checkout/monthly/landing"
|
||||||
|
>
|
||||||
|
Redirect
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 p-4 items-center">
|
||||||
|
<h2>123Done - Yearly</h2>
|
||||||
|
<Link
|
||||||
|
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||||
|
href="/en/123done/checkout/yearly/landing"
|
||||||
|
>
|
||||||
|
Redirect
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl mt-8">Without auth</h2>
|
||||||
<div className="flex gap-8">
|
<div className="flex gap-8">
|
||||||
<div className="flex flex-col gap-2 p-4 items-center">
|
<div className="flex flex-col gap-2 p-4 items-center">
|
||||||
<h2>123Done - Monthly</h2>
|
<h2>123Done - Monthly</h2>
|
||||||
|
@ -34,5 +57,6 @@ export default function Index() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,4 +6,7 @@ import type { NextAuthConfig } from 'next-auth';
|
||||||
|
|
||||||
export const authConfig = {
|
export const authConfig = {
|
||||||
providers: [], // Add providers with an empty array for now
|
providers: [], // Add providers with an empty array for now
|
||||||
|
pages: {
|
||||||
|
signIn: '/en-US/error/auth/signin',
|
||||||
|
},
|
||||||
} satisfies NextAuthConfig;
|
} satisfies NextAuthConfig;
|
||||||
|
|
|
@ -30,8 +30,15 @@ export const {
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async session(params: any) {
|
async session(params: any) {
|
||||||
params.session.user.id = params.token.sub;
|
params.session.user.id = params.token.fxaUid;
|
||||||
return params.session;
|
return params.session;
|
||||||
},
|
},
|
||||||
|
async jwt({ token, account }) {
|
||||||
|
const fxaUid = token.fxaUid || account?.providerAccountId;
|
||||||
|
return {
|
||||||
|
...token,
|
||||||
|
fxaUid,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,10 @@ import { CartManager } from './cart.manager';
|
||||||
import { ResultCart, TaxAddress, UpdateCart } from './cart.types';
|
import { ResultCart, TaxAddress, UpdateCart } from './cart.types';
|
||||||
import { handleEligibilityStatusMap } from './cart.utils';
|
import { handleEligibilityStatusMap } from './cart.utils';
|
||||||
import { CartErrorReasonId, CartState } from '@fxa/shared/db/mysql/account';
|
import { CartErrorReasonId, CartState } from '@fxa/shared/db/mysql/account';
|
||||||
import { AccountCustomerManager } from '@fxa/payments/stripe';
|
import {
|
||||||
|
AccountCustomerManager,
|
||||||
|
AccountCustomerNotFoundError,
|
||||||
|
} from '@fxa/payments/stripe';
|
||||||
import { GeoDBManager } from '@fxa/shared/geodb';
|
import { GeoDBManager } from '@fxa/shared/geodb';
|
||||||
import { CheckoutService } from './checkout.service';
|
import { CheckoutService } from './checkout.service';
|
||||||
|
|
||||||
|
@ -42,9 +45,17 @@ export class CartService {
|
||||||
// - Check if user is eligible to subscribe to plan, else throw error
|
// - Check if user is eligible to subscribe to plan, else throw error
|
||||||
// - Fetch invoice preview total from Stripe for `amount`
|
// - Fetch invoice preview total from Stripe for `amount`
|
||||||
// - Fetch stripeCustomerId if uid is passed and has a customer id
|
// - Fetch stripeCustomerId if uid is passed and has a customer id
|
||||||
const accountCustomer = args.uid
|
let accountCustomer;
|
||||||
? await this.accountCustomerManager.getAccountCustomerByUid(args.uid)
|
if (args.uid) {
|
||||||
: undefined;
|
try {
|
||||||
|
accountCustomer =
|
||||||
|
await this.accountCustomerManager.getAccountCustomerByUid(args.uid);
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof AccountCustomerNotFoundError)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const taxAddress = args.ip
|
const taxAddress = args.ip
|
||||||
? this.geodbManager.getTaxAddress(args.ip)
|
? this.geodbManager.getTaxAddress(args.ip)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
export * from './lib/accountCustomer/accountCustomer.error';
|
||||||
export * from './lib/accountCustomer/accountCustomer.factories';
|
export * from './lib/accountCustomer/accountCustomer.factories';
|
||||||
export * from './lib/accountCustomer/accountCustomer.manager';
|
export * from './lib/accountCustomer/accountCustomer.manager';
|
||||||
export {
|
export {
|
||||||
|
|
Загрузка…
Ссылка в новой задаче