зеркало из https://github.com/mozilla/fxa.git
feat(next): add email, google, apple sign in
Because: - On the checkout page, after populating the email clicking continue should navigate the user to the FxA signin page for signin. - On the checkout page, when clicking Google/Apple button, it should start the third party auth via FxA. - Completion of either signin option should navigate the user back to the checkout page in a signed in state. This commit: - Updated Auth.js provider to support email, google and apple login - Customers are redirected to the '/new' page on successful login, which creates a new cart with the customers uid added. - Updates Payments Next auth config to a non-public client. Closes #FXA-10548
This commit is contained in:
Родитель
a1b137c04f
Коммит
182c00b3c1
|
@ -6,13 +6,17 @@
|
|||
# Auth
|
||||
AUTH__ISSUER_URL=http://localhost:3030
|
||||
AUTH__WELL_KNOWN_URL=http://localhost:3030/.well-known/openid-configuration
|
||||
AUTH__TOKEN_URL=http://localhost:9000/v1/token
|
||||
AUTH__USERINFO_URL=http://localhost:1111/v1/profile
|
||||
AUTH__CLIENT_ID=32aaeb6f1c21316a
|
||||
AUTH__CLIENT_SECRET=024f261551f122335424aee381701615a659b91a6f2017429a5832c231c19f9a
|
||||
|
||||
# PayPal
|
||||
PAYPAL__CLIENT_ID=sb
|
||||
|
||||
# NextAuth
|
||||
AUTH_SECRET=replacewithsecret
|
||||
AUTH_TRUST_HOST=true
|
||||
|
||||
# MySQLConfig
|
||||
MYSQL_CONFIG__DATABASE=fxa
|
||||
|
@ -106,6 +110,7 @@ PROFILE_CLIENT_CONFIG__SERVICE_NAME='subhub'
|
|||
# Other
|
||||
CONTENT_SERVER_URL=http://localhost:3030
|
||||
SUPPORT_URL=https://support.mozilla.org
|
||||
BASE_URL=http://localhost:3035
|
||||
|
||||
# Nextjs Public Environment Variables
|
||||
|
||||
|
|
|
@ -6,13 +6,17 @@
|
|||
# Auth
|
||||
AUTH__ISSUER_URL=http://localhost:3030
|
||||
AUTH__WELL_KNOWN_URL=http://localhost:3030/.well-known/openid-configuration
|
||||
AUTH__TOKEN_URL=http://localhost:9000/v1/token
|
||||
AUTH__USERINFO_URL=http://localhost:1111/v1/profile
|
||||
AUTH__CLIENT_ID=
|
||||
AUTH__CLIENT_SECRET=placeholder
|
||||
|
||||
# PayPal
|
||||
PAYPAL__CLIENT_ID=sb
|
||||
|
||||
# NextAuth
|
||||
AUTH_SECRET=placeholder
|
||||
AUTH_TRUST_HOST=true
|
||||
|
||||
# MySQLConfig
|
||||
MYSQL_CONFIG__DATABASE=fxa
|
||||
|
@ -102,6 +106,7 @@ PROFILE_CLIENT_CONFIG__SERVICE_NAME='subhub'
|
|||
# Other
|
||||
CONTENT_SERVER_URL=https://accounts.firefox.com
|
||||
SUPPORT_URL=https://support.mozilla.org
|
||||
BASE_URL=http://localhost:3035
|
||||
|
||||
# Nextjs Public Environment Variables
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { headers } from 'next/headers';
|
|||
import Image from 'next/image';
|
||||
import {
|
||||
BaseButton,
|
||||
buildRedirectUrl,
|
||||
ButtonVariant,
|
||||
PaymentSection,
|
||||
SignInForm,
|
||||
|
@ -19,12 +20,22 @@ import {
|
|||
import AppleLogo from '@fxa/shared/assets/images/apple-logo.svg';
|
||||
import GoogleLogo from '@fxa/shared/assets/images/google-logo.svg';
|
||||
import { DEFAULT_LOCALE } from '@fxa/shared/l10n';
|
||||
import { auth } from 'apps/payments/next/auth';
|
||||
import { fetchCMSData, getCartOrRedirectAction } from '@fxa/payments/ui/actions';
|
||||
import { auth, signIn } from 'apps/payments/next/auth';
|
||||
import {
|
||||
fetchCMSData,
|
||||
getCartOrRedirectAction,
|
||||
} from '@fxa/payments/ui/actions';
|
||||
import { config } from 'apps/payments/next/config';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function Checkout({ params }: { params: CheckoutParams }) {
|
||||
export default async function Checkout({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: CheckoutParams;
|
||||
searchParams: Record<string, string> | undefined;
|
||||
}) {
|
||||
// Temporarily defaulting to `accept-language`
|
||||
// This to be updated in FXA-9404
|
||||
//const locale = getLocaleFromRequest(
|
||||
|
@ -46,9 +57,17 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
cmsDataPromise,
|
||||
]);
|
||||
|
||||
const redirectTo = buildRedirectUrl(
|
||||
params.offeringId,
|
||||
params.interval,
|
||||
'new',
|
||||
'checkout',
|
||||
{ baseUrl: config.baseUrl, locale: params.locale, searchParams }
|
||||
);
|
||||
|
||||
return (
|
||||
<section aria-label="Checkout">
|
||||
{!session && (
|
||||
{!session?.user && (
|
||||
<>
|
||||
<h2 className="font-semibold text-grey-600 text-lg mt-10">
|
||||
{l10n.getString(
|
||||
|
@ -58,6 +77,13 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
</h2>
|
||||
|
||||
<SignInForm
|
||||
submitAction={async (email?: string) => {
|
||||
'use server';
|
||||
const additionalParams = email
|
||||
? { login_hint: email }
|
||||
: undefined;
|
||||
await signIn('fxa', { redirectTo }, additionalParams);
|
||||
}}
|
||||
newsletterLabel={cms.commonContent.newsletterLabelTextCode}
|
||||
/>
|
||||
|
||||
|
@ -69,6 +95,16 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
</h3>
|
||||
|
||||
<div className="flex flex-col gap-4 mt-6 mb-10 desktop:flex-row desktop:items-center desktop:justify-center">
|
||||
<form
|
||||
action={async () => {
|
||||
'use server';
|
||||
await signIn(
|
||||
'fxa',
|
||||
{ redirectTo },
|
||||
{ deeplink: 'googleLogin' }
|
||||
);
|
||||
}}
|
||||
>
|
||||
<BaseButton variant={ButtonVariant.ThirdParty}>
|
||||
<Image src={GoogleLogo} alt="" />
|
||||
{l10n.getString(
|
||||
|
@ -76,6 +112,13 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
'Continue with Google'
|
||||
)}
|
||||
</BaseButton>
|
||||
</form>
|
||||
<form
|
||||
action={async () => {
|
||||
'use server';
|
||||
await signIn('fxa', { redirectTo }, { deeplink: 'appleLogin' });
|
||||
}}
|
||||
>
|
||||
<BaseButton variant={ButtonVariant.ThirdParty}>
|
||||
<Image src={AppleLogo} alt="" />
|
||||
{l10n.getString(
|
||||
|
@ -83,6 +126,7 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
'Continue with Apple'
|
||||
)}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<hr className="mx-auto w-full border-grey-200" />
|
||||
|
|
|
@ -17,15 +17,15 @@ export const {
|
|||
{
|
||||
id: 'fxa',
|
||||
name: 'Firefox Accounts',
|
||||
type: 'oidc',
|
||||
type: 'oauth',
|
||||
issuer: config.auth.issuerUrl,
|
||||
wellKnown: config.auth.wellKnownUrl,
|
||||
checks: ['pkce', 'state'],
|
||||
client: {
|
||||
token_endpoint_auth_method: 'none',
|
||||
},
|
||||
authorization: { params: { scope: 'openid email profile' } },
|
||||
checks: ['state'],
|
||||
authorization: { params: { scope: 'email profile' } },
|
||||
clientId: config.auth.clientId,
|
||||
clientSecret: config.auth.clientSecret,
|
||||
token: config.auth.tokenUrl,
|
||||
userinfo: config.auth.userinfoUrl,
|
||||
},
|
||||
],
|
||||
callbacks: {
|
||||
|
@ -33,8 +33,9 @@ export const {
|
|||
params.session.user.id = params.token.fxaUid;
|
||||
return params.session;
|
||||
},
|
||||
async jwt({ token, account }) {
|
||||
const fxaUid = token.fxaUid || account?.providerAccountId;
|
||||
async jwt({ token, profile }) {
|
||||
// Note profile is only defined once after user sign in.
|
||||
const fxaUid = token.fxaUid || profile?.uid;
|
||||
return {
|
||||
...token,
|
||||
fxaUid,
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
IsUrl,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
RootConfig as NestAppRootConfig,
|
||||
|
@ -46,8 +47,17 @@ class AuthJSConfig {
|
|||
@IsUrl({ require_tld: false })
|
||||
wellKnownUrl!: string;
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
tokenUrl!: string;
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
userinfoUrl!: string;
|
||||
|
||||
@IsString()
|
||||
clientId!: string;
|
||||
|
||||
@IsString()
|
||||
clientSecret!: string;
|
||||
}
|
||||
|
||||
export class PaymentsNextConfig extends NestAppRootConfig {
|
||||
|
@ -74,12 +84,18 @@ export class PaymentsNextConfig extends NestAppRootConfig {
|
|||
@IsString()
|
||||
authSecret!: string;
|
||||
|
||||
@IsBoolean()
|
||||
authTrustHost!: boolean;
|
||||
|
||||
@IsString()
|
||||
stripePublicApiKey!: string;
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
contentServerUrl!: string;
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
baseUrl!: string;
|
||||
|
||||
/**
|
||||
* Nextjs Public Environment Variables
|
||||
*/
|
||||
|
|
|
@ -22,3 +22,4 @@ export * from './lib/utils/helpers';
|
|||
export * from './lib/utils/types';
|
||||
export * from './lib/utils/get-cart';
|
||||
export * from './lib/utils/poll-cart';
|
||||
export * from './lib/utils/buildRedirectUrl';
|
||||
|
|
|
@ -38,14 +38,23 @@ function getNewsletterStringInfo(newsletterLabelTextCode?: string | null) {
|
|||
}
|
||||
|
||||
interface SignInFormProps {
|
||||
submitAction: (email?: string) => Promise<void>;
|
||||
newsletterLabel?: string | null;
|
||||
}
|
||||
|
||||
export const SignInForm = ({ newsletterLabel }: SignInFormProps) => {
|
||||
export const SignInForm = ({
|
||||
newsletterLabel,
|
||||
submitAction,
|
||||
}: SignInFormProps) => {
|
||||
const { newsletterStringId, newsletterStringFallbackText } =
|
||||
getNewsletterStringInfo(newsletterLabel);
|
||||
return (
|
||||
<Form.Root aria-label="Sign-in/sign-up form">
|
||||
<Form.Root
|
||||
action={async (formData: FormData) =>
|
||||
submitAction(formData.get('email')?.toString())
|
||||
}
|
||||
aria-label="Sign-in/sign-up form"
|
||||
>
|
||||
<Form.Field name="email" className="my-6">
|
||||
<Form.Label className="text-grey-400 block mb-1 text-start">
|
||||
<Localized id="checkout-enter-your-email">Enter your email</Localized>
|
||||
|
@ -54,6 +63,7 @@ export const SignInForm = ({ newsletterLabel }: SignInFormProps) => {
|
|||
<input
|
||||
className="w-full border rounded-md border-black/30 p-3 placeholder:text-grey-500 placeholder:font-normal focus:border focus:!border-black/30 focus:!shadow-[0_0_0_3px_rgba(10,132,255,0.3)] focus-visible:outline-none data-[invalid=true]:border-alert-red data-[invalid=true]:text-alert-red data-[invalid=true]:shadow-inputError"
|
||||
type="email"
|
||||
name="email"
|
||||
data-testid="email"
|
||||
required
|
||||
aria-required
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/* 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/. */
|
||||
|
||||
type Page = 'landing' | 'new' | 'start' | 'success' | 'error';
|
||||
type PageType = 'checkout' | 'upgrade';
|
||||
|
||||
interface Optional {
|
||||
baseUrl?: string;
|
||||
locale?: string;
|
||||
cartId?: string;
|
||||
searchParams?: Record<string, string>;
|
||||
}
|
||||
|
||||
export function buildRedirectUrl(
|
||||
offeringId: string,
|
||||
interval: string,
|
||||
page: Page,
|
||||
pageType: PageType,
|
||||
optional?: Optional
|
||||
) {
|
||||
const baseUrl = optional?.baseUrl ? optional?.baseUrl : '';
|
||||
|
||||
const startUrl = baseUrl === '/' ? baseUrl : `${baseUrl}/`;
|
||||
const endUrl = optional?.cartId ? `${optional?.cartId}/${page}` : page;
|
||||
|
||||
const searchParamsString = optional?.searchParams
|
||||
? `?${new URLSearchParams(optional?.searchParams).toString()}`
|
||||
: '';
|
||||
|
||||
return `${startUrl}${optional?.locale}/${offeringId}/${pageType}/${interval}/${endUrl}${searchParamsString}`;
|
||||
}
|
|
@ -100,7 +100,7 @@
|
|||
"nest-typed-config": "^2.9.2",
|
||||
"nest-winston": "^1.10.0",
|
||||
"next": "^14.2.14",
|
||||
"next-auth": "beta",
|
||||
"next-auth": "5.0.0-beta.22",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-hkdf": "^0.0.2",
|
||||
"node-jose": "^2.2.0",
|
||||
|
|
|
@ -369,15 +369,14 @@
|
|||
{
|
||||
"id": "32aaeb6f1c21316a",
|
||||
"name": "Firefox Accounts Payments Next",
|
||||
"hashedSecret": "b312c769bfce7682c2482c701199d88a8b67e40cee43cbe03bafe6eca47fff75",
|
||||
"hashedSecret": "674609e00c64cedb4b486caf5ed3914f1fa86be25cc5cc6927a8a3d1c6981c4e",
|
||||
"redirectUri": "http://localhost:3035/api/auth/callback/fxa",
|
||||
"imageUri": "",
|
||||
"canGrant": true,
|
||||
"termsUri": "",
|
||||
"privacyUri": "",
|
||||
"trusted": true,
|
||||
"allowedScopes": "https://identity.mozilla.com/account/subscriptions https://identity.mozilla.com/account/newsletters",
|
||||
"publicClient": true
|
||||
"allowedScopes": "https://identity.mozilla.com/account/subscriptions https://identity.mozilla.com/account/newsletters"
|
||||
},
|
||||
{
|
||||
"name": "Thunderbird",
|
||||
|
|
36
yarn.lock
36
yarn.lock
|
@ -528,15 +528,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@auth/core@npm:0.37.2":
|
||||
version: 0.37.2
|
||||
resolution: "@auth/core@npm:0.37.2"
|
||||
"@auth/core@npm:0.35.3":
|
||||
version: 0.35.3
|
||||
resolution: "@auth/core@npm:0.35.3"
|
||||
dependencies:
|
||||
"@panva/hkdf": ^1.2.1
|
||||
"@types/cookie": 0.6.0
|
||||
cookie: 0.7.1
|
||||
jose: ^5.9.3
|
||||
oauth4webapi: ^3.0.0
|
||||
cookie: 0.6.0
|
||||
jose: ^5.1.3
|
||||
oauth4webapi: ^2.10.4
|
||||
preact: 10.11.3
|
||||
preact-render-to-string: 5.2.3
|
||||
peerDependencies:
|
||||
|
@ -550,7 +550,7 @@ __metadata:
|
|||
optional: true
|
||||
nodemailer:
|
||||
optional: true
|
||||
checksum: 669eea9bf2d84e97d179f2dd0b7f7915269aa23d6014d0dd177f072bf3f894f1a6844a346bcdea868325976cc064baa6c26925715447ba61aa96875782458969
|
||||
checksum: 93b2db76c80807598f56aaf685cbd1ad9df083a7859ab58a24b55e422683e53c5497d814fc71255b22207275eef99aff253d6698027ae1f03f35ed567b2c907c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -41996,7 +41996,7 @@ fsevents@~2.1.1:
|
|||
nest-typed-config: ^2.9.2
|
||||
nest-winston: ^1.10.0
|
||||
next: ^14.2.14
|
||||
next-auth: beta
|
||||
next-auth: 5.0.0-beta.22
|
||||
node-fetch: ^2.6.7
|
||||
node-hkdf: ^0.0.2
|
||||
node-jose: ^2.2.0
|
||||
|
@ -49290,7 +49290,7 @@ fsevents@~2.1.1:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jose@npm:^5.9.3, jose@npm:^5.9.6":
|
||||
"jose@npm:^5.1.3, jose@npm:^5.9.6":
|
||||
version: 5.9.6
|
||||
resolution: "jose@npm:5.9.6"
|
||||
checksum: 4b536da0201858ed4c4582e8bb479081f11e0c63dd0f5e473adde16fc539785e1f2f0409bc1fc7cbbb5b68026776c960b4952da3a06f6fdfff0b9764c9127ae0
|
||||
|
@ -54004,11 +54004,11 @@ fsevents@~2.1.1:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"next-auth@npm:beta":
|
||||
version: 5.0.0-beta.25
|
||||
resolution: "next-auth@npm:5.0.0-beta.25"
|
||||
"next-auth@npm:5.0.0-beta.22":
|
||||
version: 5.0.0-beta.22
|
||||
resolution: "next-auth@npm:5.0.0-beta.22"
|
||||
dependencies:
|
||||
"@auth/core": 0.37.2
|
||||
"@auth/core": 0.35.3
|
||||
peerDependencies:
|
||||
"@simplewebauthn/browser": ^9.0.1
|
||||
"@simplewebauthn/server": ^9.0.2
|
||||
|
@ -54022,7 +54022,7 @@ fsevents@~2.1.1:
|
|||
optional: true
|
||||
nodemailer:
|
||||
optional: true
|
||||
checksum: 5a3f52e6d32ae6c954e438496b96bb8d46ba60ca6dbfab146af718d50e7fe8b967b7019fda162e5fa32bfc5219160f7b995c327d62d5144e7935bb18b9b61e39
|
||||
checksum: 4b82f3fc147b444ec4942900c0e0a430372117e4c2c2ad84dff0afbce5dea4f36ef147e647f8b88e9894b492d8a92faa26801203d2d2839541092713c8767866
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -55243,10 +55243,10 @@ fsevents@~2.1.1:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oauth4webapi@npm:^3.0.0":
|
||||
version: 3.1.2
|
||||
resolution: "oauth4webapi@npm:3.1.2"
|
||||
checksum: cd986eba0ee3d8853a4abb1ee3296cafa876cc9a8d2716213d01a3d396f1758581d1fe83e8b9fb1be16c226b0e1392ce528f06558532082ea2a928bb244407d8
|
||||
"oauth4webapi@npm:^2.10.4":
|
||||
version: 2.17.0
|
||||
resolution: "oauth4webapi@npm:2.17.0"
|
||||
checksum: 2a29c1d4c2f597aa8e41f51bc102cf098c23295a05b0ab4769f53800b6ede7abae3f8b00136634e1ec32bea7849cbefdd9e0f21ec7c215f45fa6d9e82cb6701c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче