Because:

- Initialize PayPal SDK and add button to checkout page

This commit:

- Adds PayPal SDK and initializes provider
- Adds PayPal button and only display when PayPal is selected in Payment
  Element
- Updates CSP for PayPal URLs
- Adds config options

Closes #FXA-7585
This commit is contained in:
Reino Muhl 2024-08-29 12:07:47 -04:00
Родитель f39d66b07f
Коммит 1c50ec8870
Не найден ключ, соответствующий данной подписи
10 изменённых файлов: 151 добавлений и 21 удалений

Просмотреть файл

@ -8,6 +8,9 @@ AUTH__ISSUER_URL=http://localhost:3030
AUTH__WELL_KNOWN_URL=http://localhost:3030/.well-known/openid-configuration
AUTH__CLIENT_ID=32aaeb6f1c21316a
# PayPal
PAYPAL__CLIENT_ID=sb
# NextAuth
AUTH_SECRET=replacewithsecret
@ -74,6 +77,7 @@ STATS_D_CONFIG__PREFIX=
# CSP Config
CSP__ACCOUNTS_STATIC_CDN=https://accounts-static.cdn.mozilla.net
CSP__PAYPAL_API='https://www.sandbox.paypal.com'
# Other
CONTENT_SERVER_URL=http://localhost:3030

Просмотреть файл

@ -8,6 +8,9 @@ AUTH__ISSUER_URL=http://localhost:3030
AUTH__WELL_KNOWN_URL=http://localhost:3030/.well-known/openid-configuration
AUTH__CLIENT_ID=
# PayPal
PAYPAL__CLIENT_ID=sb
# NextAuth
AUTH_SECRET=placeholder
@ -70,6 +73,7 @@ STATS_D_CONFIG__PREFIX=
# CSP Config
CSP__ACCOUNTS_STATIC_CDN=https://accounts-static.cdn.mozilla.net
CSP__PAYPAL_API='https://www.paypal.com'
# Other
CONTENT_SERVER_URL=https://accounts.firefox.com

Просмотреть файл

@ -19,12 +19,17 @@ export default async function RootProviderLayout({
// headers().get('accept-language')
//);
const locale = headers().get('accept-language') || DEFAULT_LOCALE;
const nonce = headers().get('x-nonce') || undefined;
const fetchedMessages = getApp().getFetchedMessages(locale);
return (
<Providers
config={{ stripePublicApiKey: config.stripePublicApiKey }}
config={{
stripePublicApiKey: config.stripePublicApiKey,
paypalClientId: config.paypal.clientId,
}}
fetchedMessages={fetchedMessages}
nonce={nonce}
>
{children}
</Providers>

Просмотреть файл

@ -10,6 +10,14 @@ import {
class CspConfig {
@IsUrl()
accountsStaticCdn!: string;
@IsUrl()
paypalApi!: string;
}
class PaypalConfig {
@IsString()
clientId!: string;
}
class AuthJSConfig {
@ -29,6 +37,11 @@ export class PaymentsNextConfig extends NestAppRootConfig {
@IsDefined()
auth!: AuthJSConfig;
@Type(() => PaypalConfig)
@ValidateNested()
@IsDefined()
paypal!: PaypalConfig;
@Type(() => CspConfig)
@ValidateNested()
@IsDefined()

Просмотреть файл

@ -7,6 +7,9 @@ export function middleware(request: NextRequest) {
// Read env vars directly from process.env
// As of 05-15-2024 its not possible to use app/config in middleware
const accountsStaticCdn = process.env.CSP__ACCOUNTS_STATIC_CDN;
const PAYPAL_SCRIPT_URL = 'https://www.paypal.com';
const PAYPAL_API_URL = process.env.CSP__PAYPAL_API;
const PAYPAL_OBJECTS = 'https://www.paypalobjects.com';
/*
* CSP Notes
@ -18,16 +21,16 @@ export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const cspHeader = `
default-src 'self';
connect-src 'self' https://api.stripe.com;
frame-src https://js.stripe.com https://hooks.stripe.com;
connect-src 'self' https://api.stripe.com ${PAYPAL_API_URL};
frame-src https://js.stripe.com https://hooks.stripe.com ${PAYPAL_API_URL} ${PAYPAL_SCRIPT_URL};
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https: http: 'unsafe-inline' ${
process.env.NODE_ENV === 'production' ? '' : `'unsafe-eval'`
} https://js.stripe.com;
} https://js.stripe.com ${PAYPAL_SCRIPT_URL};
script-src-elem 'self' 'nonce-${nonce}' 'strict-dynamic' https: http: 'unsafe-inline' ${
process.env.NODE_ENV === 'production' ? '' : `'unsafe-eval'`
} https://js.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data: ${accountsStaticCdn};
img-src 'self' blob: data: ${accountsStaticCdn} ${PAYPAL_OBJECTS};
font-src 'self';
object-src 'none';
base-uri 'self';
@ -41,6 +44,8 @@ export function middleware(request: NextRequest) {
.trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
requestHeaders.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue

Просмотреть файл

@ -19,6 +19,7 @@ import { CheckoutCheckbox } from '../CheckoutCheckbox';
import { PrimaryButton } from '../PrimaryButton';
import { checkoutCartWithStripe } from '../../../actions/checkoutCartWithStripe';
import { handleStripeErrorAction } from '../../../actions/handleStripeError';
import { PayPalButtons } from '@paypal/react-paypal-js';
interface CheckoutFormProps {
cmsCommonContent: {
@ -50,6 +51,7 @@ export function CheckoutForm({
const [stripeFieldsComplete, setStripeFieldsComplete] = useState(false);
const [fullName, setFullName] = useState('');
const [hasFullNameError, setHasFullNameError] = useState(false);
const [showPayPalButton, setShowPayPalButton] = useState(false);
useEffect(() => {
if (elements) {
@ -67,6 +69,16 @@ export function CheckoutForm({
setStripeFieldsComplete(false);
}
}
//Show or hide the PayPal button
const selectedPaymentMethod = event?.value?.type;
if (selectedPaymentMethod === 'external_paypal') {
// Show the PayPal button
setShowPayPalButton(true);
} else {
// Hide the PayPal button
setShowPayPalButton(false);
}
});
} else {
setIsPaymentElementLoading(false);
@ -164,12 +176,14 @@ export function CheckoutForm({
}
onClick={() => setShowConsentError(true)}
>
<Localized id="next-new-user-card-title">
<h3 className="font-semibold text-grey-600 text-start">
Enter your card information
</h3>
</Localized>
{!isPaymentElementLoading && (
{!showPayPalButton && (
<Localized id="next-new-user-card-title">
<h3 className="font-semibold text-grey-600 text-start">
Enter your card information
</h3>
</Localized>
)}
{!isPaymentElementLoading && !showPayPalButton && (
<Form.Field
name="name"
serverInvalid={hasFullNameError}
@ -224,15 +238,29 @@ export function CheckoutForm({
/>
{!isPaymentElementLoading && (
<Form.Submit asChild>
<PrimaryButton
type="submit"
aria-disabled={
!stripeFieldsComplete || !nonStripeFieldsComplete || loading
}
>
<Image src={LockImage} className="h-4 w-4 mx-3" alt="" />
<Localized id="next-new-user-submit">Subscribe Now</Localized>
</PrimaryButton>
{showPayPalButton ? (
<PayPalButtons
style={{
layout: 'horizontal',
color: 'gold',
shape: 'pill',
label: 'paypal',
height: 48,
tagline: false,
}}
className="mt-6"
/>
) : (
<PrimaryButton
type="submit"
aria-disabled={
!stripeFieldsComplete || !nonStripeFieldsComplete || loading
}
>
<Image src={LockImage} className="h-4 w-4 mx-3" alt="" />
<Localized id="next-new-user-submit">Subscribe Now</Localized>
</PrimaryButton>
)}
</Form.Submit>
)}
</div>

Просмотреть файл

@ -8,10 +8,12 @@ import { createContext } from 'react';
export interface ConfigContextValues {
stripePublicApiKey: string;
paypalClientId: string;
}
export const ConfigContext = createContext<ConfigContextValues>({
stripePublicApiKey: '',
paypalClientId: '',
});
export function ConfigProvider({

Просмотреть файл

@ -6,22 +6,44 @@
import { ConfigContextValues, ConfigProvider } from './ConfigProvider';
import { FluentLocalizationProvider } from './FluentLocalizationProvider';
import {
PayPalScriptProvider,
ReactPayPalScriptOptions,
} from '@paypal/react-paypal-js';
interface ProvidersProps {
config: ConfigContextValues;
fetchedMessages: Record<string, string>;
nonce?: string;
children: React.ReactNode;
}
const paypalInitialOptions: ReactPayPalScriptOptions = {
clientId: '',
vault: true,
commit: false,
intent: 'capture',
disableFunding: ['credit', 'card'],
};
export function Providers({
config,
fetchedMessages,
nonce,
children,
}: ProvidersProps) {
return (
<ConfigProvider config={config}>
<FluentLocalizationProvider fetchedMessages={fetchedMessages}>
{children}
<PayPalScriptProvider
options={{
...paypalInitialOptions,
clientId: config.paypalClientId,
dataCspNonce: nonce,
}}
>
{children}
</PayPalScriptProvider>
</FluentLocalizationProvider>
</ConfigProvider>
);

Просмотреть файл

@ -73,6 +73,7 @@
"@opentelemetry/sdk-trace-base": "^1.23.0",
"@opentelemetry/sdk-trace-node": "^1.23.0",
"@opentelemetry/sdk-trace-web": "^1.23.0",
"@paypal/react-paypal-js": "^8.6.0",
"@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-tooltip": "^1.1.2",
"@sentry/browser": "^7.113.0",

Просмотреть файл

@ -15176,6 +15176,37 @@ __metadata:
languageName: node
linkType: hard
"@paypal/paypal-js@npm:^8.1.1":
version: 8.1.1
resolution: "@paypal/paypal-js@npm:8.1.1"
dependencies:
promise-polyfill: ^8.3.0
checksum: 5952989095307c2f9a071a492955370b7f82d04854f57d7684f5d34344d1597c6b9536c52d64973391e9857dcb787e5f5261c7a2fa4fb9583ab32a267135cb41
languageName: node
linkType: hard
"@paypal/react-paypal-js@npm:^8.6.0":
version: 8.6.0
resolution: "@paypal/react-paypal-js@npm:8.6.0"
dependencies:
"@paypal/paypal-js": ^8.1.1
"@paypal/sdk-constants": ^1.0.122
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 936ed9dab68ebeca353101e6044ca136f73921f0ad11a9f9b1e811f60ec03d8841fb98375298cae141b56fde59b643d6edfe631df371f40f6b5edaac72976b73
languageName: node
linkType: hard
"@paypal/sdk-constants@npm:^1.0.122":
version: 1.0.149
resolution: "@paypal/sdk-constants@npm:1.0.149"
dependencies:
hi-base32: ^0.5.0
checksum: ecc6e10987ca1ab15671c2574620da6e16a39877cd47e48db3cc9ba13d0c970612470bba1c2c179b09135691776ab62114ba6203df3946ad14d662251961c40f
languageName: node
linkType: hard
"@peculiar/asn1-schema@npm:^2.1.6":
version: 2.1.8
resolution: "@peculiar/asn1-schema@npm:2.1.8"
@ -40115,6 +40146,7 @@ fsevents@~2.1.1:
"@opentelemetry/sdk-trace-base": ^1.23.0
"@opentelemetry/sdk-trace-node": ^1.23.0
"@opentelemetry/sdk-trace-web": ^1.23.0
"@paypal/react-paypal-js": ^8.6.0
"@radix-ui/react-form": ^0.0.3
"@radix-ui/react-tooltip": ^1.1.2
"@sentry/browser": ^7.113.0
@ -42341,6 +42373,13 @@ fsevents@~2.1.1:
languageName: node
linkType: hard
"hi-base32@npm:^0.5.0":
version: 0.5.1
resolution: "hi-base32@npm:0.5.1"
checksum: 6655682b5796d75ed3068071e61d05a490e2086c4908af3b94a730059147b8a4a5e8870e656b828d0550dcc9988d8748bda54a53e428cbce28e0d7a785b2ffde
languageName: node
linkType: hard
"highlight.js@npm:^10.1.1, highlight.js@npm:~10.6.0":
version: 10.6.0
resolution: "highlight.js@npm:10.6.0"
@ -57073,6 +57112,13 @@ fsevents@~2.1.1:
languageName: node
linkType: hard
"promise-polyfill@npm:^8.3.0":
version: 8.3.0
resolution: "promise-polyfill@npm:8.3.0"
checksum: 206373802076c77def0805758d0a8ece64120dfa6603f092404a1004211f8f2f67f33cadbc35953fc2a8ed0b0d38c774e88bdf01e20ce7a920723a60df84b7a5
languageName: node
linkType: hard
"promise-retry@npm:^2.0.1":
version: 2.0.1
resolution: "promise-retry@npm:2.0.1"