зеркало из https://github.com/mozilla/fxa.git
Merge pull request #17504 from mozilla/fxa-7585-add-paypal-sdk
feat(next): add paypal sdk
This commit is contained in:
Коммит
0f699a3c67
|
@ -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",
|
||||
|
|
46
yarn.lock
46
yarn.lock
|
@ -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"
|
||||
|
@ -40069,6 +40100,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
|
||||
|
@ -42295,6 +42327,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"
|
||||
|
@ -57027,6 +57066,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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче