зеркало из https://github.com/mozilla/fxa.git
feat(next): add payment element and submit
Because: * Add the Stripe Payment Element to the checkout page * Be able to checkout and successfully add a new payment method and new subscription. This commit: * Adds the PaymentElement to the StripeWrapper * Adds the Subscribe Now button * Adds the Full Name field * Updates the NestApp with Firestore and StatsD provider factories Closes #FXA-8850 and #FXA-7810
This commit is contained in:
Родитель
08dbb1c741
Коммит
2fb70a6886
|
@ -25,8 +25,8 @@ MYSQL_CONFIG__ACQUIRE_TIMEOUT_MILLIS=
|
|||
GEODB_CONFIG__DB_PATH=../../../libs/shared/geodb/db/cities-db.mmdb
|
||||
|
||||
# GeoDBManagerConfig
|
||||
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__COUNTRY_CODE=
|
||||
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__POSTAL_CODE=
|
||||
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__COUNTRY_CODE=US
|
||||
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__POSTAL_CODE=11211
|
||||
|
||||
# StripeConfig
|
||||
STRIPE_CONFIG__API_KEY=11233
|
||||
|
@ -50,3 +50,16 @@ CONTENTFUL_CLIENT_CONFIG__GRAPHQL_ENVIRONMENT=dev
|
|||
CONTENTFUL_CLIENT_CONFIG__FIRESTORE_CACHE_COLLECTION_NAME=contentfulQueryCacheCollection
|
||||
CONTENTFUL_CLIENT_CONFIG__MEM_CACHE_T_T_L=
|
||||
CONTENTFUL_CLIENT_CONFIG__FIRESTORE_CACHE_T_T_L=
|
||||
|
||||
# Firestore Config
|
||||
FIRESTORE_CONFIG__CREDENTIALS__CLIENT_EMAIL=
|
||||
FIRESTORE_CONFIG__CREDENTIALS__PRIVATE_KEY=
|
||||
FIRESTORE_CONFIG__KEY_FILENAME=
|
||||
FIRESTORE_CONFIG__PROJECT_ID=
|
||||
|
||||
# StatsD Config
|
||||
STATS_D_CONFIG__SAMPLE_RATE=
|
||||
STATS_D_CONFIG__MAX_BUFFER_SIZE=
|
||||
STATS_D_CONFIG__HOST=
|
||||
STATS_D_CONFIG__PORT=
|
||||
STATS_D_CONFIG__PREFIX=
|
||||
|
|
|
@ -44,3 +44,16 @@ CONTENTFUL_CLIENT_CONFIG__GRAPHQL_ENVIRONMENT=master
|
|||
CONTENTFUL_CLIENT_CONFIG__FIRESTORE_CACHE_COLLECTION_NAME=contentfulQueryCacheCollection
|
||||
CONTENTFUL_CLIENT_CONFIG__MEM_CACHE_T_T_L= 300
|
||||
CONTENTFUL_CLIENT_CONFIG__FIRESTORE_CACHE_T_T_L= 604800
|
||||
|
||||
# Firestore Config
|
||||
FIRESTORE_CONFIG__CREDENTIALS__CLIENT_EMAIL=
|
||||
FIRESTORE_CONFIG__CREDENTIALS__PRIVATE_KEY=
|
||||
FIRESTORE_CONFIG__KEY_FILENAME=
|
||||
FIRESTORE_CONFIG__PROJECT_ID=
|
||||
|
||||
# StatsD Config
|
||||
STATS_D_CONFIG__SAMPLE_RATE=
|
||||
STATS_D_CONFIG__MAX_BUFFER_SIZE=
|
||||
STATS_D_CONFIG__HOST=
|
||||
STATS_D_CONFIG__PORT=
|
||||
STATS_D_CONFIG__PREFIX=
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
/* 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 Stripe from 'stripe';
|
||||
import {
|
||||
app,
|
||||
handleStripeErrorAction,
|
||||
getCartOrRedirectAction,
|
||||
SupportedPages,
|
||||
} from '@fxa/payments/ui/server';
|
||||
import { DEFAULT_LOCALE } from '@fxa/shared/l10n';
|
||||
import { auth, signIn, signOut } from 'apps/payments/next/auth';
|
||||
import { auth, signIn } from 'apps/payments/next/auth';
|
||||
import { headers } from 'next/headers';
|
||||
import { CheckoutParams } from '../layout';
|
||||
import { StripeWrapper } from '@fxa/payments/ui';
|
||||
import { getFakeCartData } from 'apps/payments/next/app/_lib/apiClient';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
@ -30,23 +29,15 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
params.cartId,
|
||||
SupportedPages.START
|
||||
);
|
||||
const [session, l10n, cart] = await Promise.all([
|
||||
//TODO - Replace with cartPromise as part of FXA-8903
|
||||
const fakeCartDataPromise = getFakeCartData(params.cartId);
|
||||
const [session, l10n, cart, fakeCart] = await Promise.all([
|
||||
sessionPromise,
|
||||
l10nPromise,
|
||||
cartPromise,
|
||||
fakeCartDataPromise,
|
||||
]);
|
||||
|
||||
// Temporary function used to test handleStripeErrorAction
|
||||
// This is to be deleted as part of FXA-8850
|
||||
async function onSubmit() {
|
||||
'use server';
|
||||
const error: Stripe.StripeRawError = new Error(
|
||||
'Simulated Stripe Error'
|
||||
) as unknown as Stripe.StripeRawError;
|
||||
error.type = 'card_error';
|
||||
await handleStripeErrorAction(cart.id, cart.version, error);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
|
@ -197,64 +188,11 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<StripeWrapper amount={1099} currency="usd" />
|
||||
{/*
|
||||
Temporary function used to test handleStripeErrorAction
|
||||
This is to be deleted as part of FXA-8850
|
||||
*/}
|
||||
<div>
|
||||
<form action={onSubmit} className="mb-8">
|
||||
<input type="text" name="name" />
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4"
|
||||
>
|
||||
<div>Test handle error</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/*
|
||||
Temporary section to test NextAuth prompt/no prompt signin
|
||||
To be deleted as part of FXA-7521/FXA-7523 if not sooner where necessary
|
||||
*/}
|
||||
{!session ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<form
|
||||
action={async () => {
|
||||
'use server';
|
||||
await signIn('fxa');
|
||||
}}
|
||||
>
|
||||
<button className="flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4">
|
||||
<div className="block">Sign In - Login</div>
|
||||
</button>
|
||||
</form>
|
||||
<form
|
||||
action={async () => {
|
||||
'use server';
|
||||
await signIn('fxa', undefined, { prompt: 'none' });
|
||||
}}
|
||||
>
|
||||
<button className="flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4">
|
||||
<div className="block">Sign In - No Prompt</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<p>Hello {session?.user?.id}</p>
|
||||
<form
|
||||
action={async () => {
|
||||
'use server';
|
||||
await signOut();
|
||||
}}
|
||||
>
|
||||
<button className="flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4">
|
||||
<div className="block">Sign Out</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
<StripeWrapper
|
||||
amount={fakeCart.amount}
|
||||
currency={fakeCart.nextInvoice.currency}
|
||||
cart={cart}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -17,19 +17,19 @@ export default function Index() {
|
|||
<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>
|
||||
<h2>VPN - 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"
|
||||
href="/en/vpn/checkout/monthly/landing"
|
||||
>
|
||||
Redirect
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-4 items-center">
|
||||
<h2>123Done - Yearly</h2>
|
||||
<h2>VPN - 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"
|
||||
href="/en/vpn/checkout/yearly/landing"
|
||||
>
|
||||
Redirect
|
||||
</Link>
|
||||
|
@ -38,19 +38,19 @@ export default function Index() {
|
|||
<h2 className="text-xl mt-8">Without auth</h2>
|
||||
<div className="flex gap-8">
|
||||
<div className="flex flex-col gap-2 p-4 items-center">
|
||||
<h2>123Done - Monthly</h2>
|
||||
<h2>VPN - 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/new"
|
||||
href="/en/vpn/checkout/monthly/new"
|
||||
>
|
||||
Redirect
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-4 items-center">
|
||||
<h2>123Done - Yearly</h2>
|
||||
<h2>VPN - 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/new"
|
||||
href="/en/vpn/checkout/yearly/new"
|
||||
>
|
||||
Redirect
|
||||
</Link>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
/* layout */
|
||||
body {
|
||||
@apply grid min-h-[100vh] tablet:justify-center text-base font-body bg-grey-10 text-grey-900;
|
||||
@apply grid min-h-[100vh] tablet:justify-center text-base font-body bg-grey-10 text-grey-900 leading-[1.15];
|
||||
}
|
||||
|
||||
/* Purchase Details, offeringId */
|
||||
|
|
|
@ -7,11 +7,11 @@ 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;
|
||||
frame-src https://js.stripe.com https://hooks.stripe.com;
|
||||
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;
|
||||
style-src 'self' 'nonce-${nonce}';
|
||||
img-src 'self' blob: data:;
|
||||
font-src 'self';
|
||||
|
|
|
@ -15,4 +15,15 @@ export default <Partial<Config>>{
|
|||
),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
inputError:
|
||||
'0 1px 2px rgba(0, 0, 0, 0.3), 0 3px 6px rgba(0, 0, 0, 0.02), 0 0 0 1px #df1b41',
|
||||
},
|
||||
colors: {
|
||||
'alert-red': '#D70022',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
} from '@fxa/shared/contentful';
|
||||
import { CapabilityManager } from './capability.manager';
|
||||
import { MockFirestoreProvider } from '@fxa/shared/db/firestore';
|
||||
import { MockStatsDFactory } from '@fxa/shared/metrics/statsd';
|
||||
import { MockStatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
|
||||
describe('CapabilityManager', () => {
|
||||
let capabilityManager: CapabilityManager;
|
||||
|
@ -32,7 +32,7 @@ describe('CapabilityManager', () => {
|
|||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
MockFirestoreProvider,
|
||||
MockStatsDFactory,
|
||||
MockStatsDProvider,
|
||||
ContentfulClientConfig,
|
||||
ContentfulClient,
|
||||
ContentfulManager,
|
||||
|
|
|
@ -43,7 +43,7 @@ import {
|
|||
ContentfulClientConfig,
|
||||
ContentfulManager,
|
||||
} from '@fxa/shared/contentful';
|
||||
import { MockStatsDFactory } from '@fxa/shared/metrics/statsd';
|
||||
import { MockStatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { MockFirestoreProvider } from '@fxa/shared/db/firestore';
|
||||
import {
|
||||
|
@ -70,7 +70,7 @@ describe('CartService', () => {
|
|||
AccountCustomerManager,
|
||||
ConfigService,
|
||||
MockFirestoreProvider,
|
||||
MockStatsDFactory,
|
||||
MockStatsDProvider,
|
||||
ContentfulClientConfig,
|
||||
ContentfulClient,
|
||||
ContentfulManager,
|
||||
|
|
|
@ -113,7 +113,7 @@ export class CartService {
|
|||
try {
|
||||
const cart = await this.cartManager.fetchCartById(cartId);
|
||||
|
||||
this.checkoutService.payWithStripe(cart, paymentMethodId);
|
||||
await this.checkoutService.payWithStripe(cart, paymentMethodId);
|
||||
|
||||
await this.cartManager.finishCart(cartId, version, {});
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import Stripe from 'stripe';
|
||||
import { StripeError } from '@stripe/stripe-js';
|
||||
import { stripeErrorToErrorReasonId } from './cart.utils';
|
||||
import { CartErrorReasonId } from '@fxa/shared/db/mysql/account';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('stripeErrorReasonId', () => {
|
||||
let mockStripeError: Stripe.StripeRawError;
|
||||
let mockStripeError: StripeError;
|
||||
|
||||
beforeEach(() => {
|
||||
mockStripeError = new Error() as unknown as Stripe.StripeRawError;
|
||||
mockStripeError = new Error() as unknown as StripeError;
|
||||
});
|
||||
|
||||
it('should return for type card_error', () => {
|
||||
|
@ -17,7 +17,7 @@ describe('utils', () => {
|
|||
});
|
||||
|
||||
it('should return for default', () => {
|
||||
mockStripeError.type = 'invalid_grant';
|
||||
mockStripeError.type = 'api_error';
|
||||
const result = stripeErrorToErrorReasonId(mockStripeError);
|
||||
expect(result).toBe(CartErrorReasonId.Unknown);
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* 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 { Stripe } from 'stripe';
|
||||
import { StripeError } from '@stripe/stripe-js';
|
||||
import { EligibilityStatus } from '@fxa/payments/eligibility';
|
||||
import {
|
||||
CartEligibilityStatus,
|
||||
|
@ -18,7 +18,7 @@ export const handleEligibilityStatusMap = {
|
|||
};
|
||||
|
||||
export function stripeErrorToErrorReasonId(
|
||||
stripeError: Stripe.StripeRawError
|
||||
stripeError: StripeError
|
||||
): CartErrorReasonId {
|
||||
switch (stripeError.type) {
|
||||
case 'card_error':
|
||||
|
|
|
@ -33,7 +33,7 @@ import {
|
|||
OfferingComparison,
|
||||
OfferingOverlapProductResult,
|
||||
} from './eligibility.types';
|
||||
import { MockStatsDFactory } from '@fxa/shared/metrics/statsd';
|
||||
import { MockStatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
import { MockFirestoreProvider } from '@fxa/shared/db/firestore';
|
||||
|
||||
describe('EligibilityService', () => {
|
||||
|
@ -52,7 +52,7 @@ describe('EligibilityService', () => {
|
|||
MockFirestoreProvider,
|
||||
ContentfulClientConfig,
|
||||
ContentfulClient,
|
||||
MockStatsDFactory,
|
||||
MockStatsDProvider,
|
||||
ContentfulManager,
|
||||
EligibilityManager,
|
||||
StripeConfig,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
'use server';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
import { StripeError } from '@stripe/stripe-js';
|
||||
import { app } from '../nestapp/app';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { stripeErrorToErrorReasonId } from '@fxa/payments/cart';
|
||||
|
@ -12,7 +12,7 @@ import { stripeErrorToErrorReasonId } from '@fxa/payments/cart';
|
|||
export const handleStripeErrorAction = async (
|
||||
cartId: string,
|
||||
version: number,
|
||||
stripeError: Stripe.StripeRawError
|
||||
stripeError: StripeError
|
||||
) => {
|
||||
const errorReasonId = stripeErrorToErrorReasonId(stripeError);
|
||||
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
/* 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/. */
|
||||
'use client';
|
||||
|
||||
import {
|
||||
PaymentElement,
|
||||
useStripe,
|
||||
useElements,
|
||||
} from '@stripe/react-stripe-js';
|
||||
import { StripePaymentElementChangeEvent } from '@stripe/stripe-js';
|
||||
import { checkoutCartWithStripe } from '../../actions/checkoutCartWithStripe';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { handleStripeErrorAction } from '../../actions/handleStripeError';
|
||||
import LockImage from '@fxa/shared/assets/images/lock.svg';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
|
||||
interface CheckoutFormProps {
|
||||
cart: {
|
||||
id: string;
|
||||
version: number;
|
||||
email: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export function CheckoutForm({ cart }: CheckoutFormProps) {
|
||||
const router = useRouter();
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const [isPaymentElementLoading, setIsPaymentElementLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [stripeFieldsComplete, setStripeFieldsComplete] = useState(false);
|
||||
const [fullName, setFullName] = useState('');
|
||||
const [hasFullNameError, setHasFullNameError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (elements) {
|
||||
const element = elements.getElement('payment');
|
||||
if (element) {
|
||||
element.on('ready', () => {
|
||||
setIsPaymentElementLoading(false);
|
||||
});
|
||||
|
||||
element.on('change', (event: StripePaymentElementChangeEvent) => {
|
||||
if (event.complete) {
|
||||
setStripeFieldsComplete(true);
|
||||
} else {
|
||||
if (!stripeFieldsComplete) {
|
||||
setStripeFieldsComplete(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setIsPaymentElementLoading(false);
|
||||
}
|
||||
}
|
||||
}, [elements, stripeFieldsComplete]);
|
||||
|
||||
const submitHandler = async (
|
||||
event: React.SyntheticEvent<HTMLFormElement>
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!stripe || !elements) {
|
||||
// Stripe.js hasn't yet loaded.
|
||||
// Make sure to disable form submission until Stripe.js has loaded.
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
setHasFullNameError(!fullName);
|
||||
|
||||
// Trigger form validation and wallet collection
|
||||
const { error: submitError } = await elements.submit();
|
||||
if (submitError) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { paymentMethod, error: methodError } =
|
||||
await stripe.createPaymentMethod({
|
||||
elements,
|
||||
params: {
|
||||
billing_details: {
|
||||
name: fullName,
|
||||
email: cart.email || '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (methodError || !paymentMethod) {
|
||||
if (methodError.type === 'validation_error') {
|
||||
return;
|
||||
} else {
|
||||
await handleStripeErrorAction(cart.id, cart.version, methodError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await checkoutCartWithStripe(cart.id, cart.version, paymentMethod.id);
|
||||
|
||||
// TODO - To be added in M3B - Redirect customer to '/processing' page
|
||||
router.push('./start');
|
||||
// TODO - To be moved in M3B - Confirm Payment on '/processing' page
|
||||
// Confirm the Intent using the details collected by the Payment Element
|
||||
//const { error } = await stripe.confirmPayment({
|
||||
// clientSecret,
|
||||
// confirmParams: {
|
||||
// return_url: successRedirectUrl,
|
||||
// },
|
||||
//});
|
||||
//
|
||||
//if (error) {
|
||||
// // This point is only reached if there's an immediate error when confirming the Intent.
|
||||
// // Show the error to your customer (for example, "payment details incomplete").
|
||||
// await handleStripeErrorAction(cart.id, cart.version, error);
|
||||
//} else {
|
||||
// // Your customer is redirected to your `return_url`. For some payment
|
||||
// // methods like iDEAL, your customer is redirected to an intermediate
|
||||
// // site first to authorize the payment, then redirected to the `return_url`.
|
||||
//}
|
||||
};
|
||||
|
||||
const nonStripeFieldsComplete = !!fullName;
|
||||
|
||||
return (
|
||||
<Form.Root className="flex flex-col gap-4" onSubmit={submitHandler}>
|
||||
{!isPaymentElementLoading && (
|
||||
<Form.Field name="name" serverInvalid={hasFullNameError}>
|
||||
<Form.Label className="font-medium text-sm text-grey-400 block mb-1 text-start">
|
||||
Name as it appears on your card
|
||||
</Form.Label>
|
||||
<Form.Control asChild>
|
||||
<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="text"
|
||||
data-testid="name"
|
||||
placeholder="Full Name"
|
||||
value={fullName}
|
||||
onChange={(e) => {
|
||||
setFullName(e.target.value);
|
||||
setHasFullNameError(!e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
{hasFullNameError && (
|
||||
<Form.Message asChild>
|
||||
<p className="text-sm mt-1 text-alert-red">
|
||||
Please enter your name
|
||||
</p>
|
||||
</Form.Message>
|
||||
)}
|
||||
</Form.Field>
|
||||
)}
|
||||
<PaymentElement />
|
||||
{!isPaymentElementLoading && (
|
||||
<Form.Submit asChild>
|
||||
<button
|
||||
className="flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4 mt-6 hover:bg-blue-700 aria-disabled:relative aria-disabled:after:absolute aria-disabled:after:content-[''] aria-disabled:after:top-0 aria-disabled:after:left-0 aria-disabled:after:w-full aria-disabled:after:h-full aria-disabled:after:bg-white aria-disabled:after:opacity-50 aria-disabled:after:z-30 aria-disabled:border-none"
|
||||
type="submit"
|
||||
aria-disabled={
|
||||
!stripeFieldsComplete || !nonStripeFieldsComplete || loading
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={LockImage}
|
||||
className="h-4 w-4 my-0 mx-3 relative top-0.5"
|
||||
alt=""
|
||||
/>
|
||||
Subscribe Now
|
||||
</button>
|
||||
</Form.Submit>
|
||||
)}
|
||||
</Form.Root>
|
||||
);
|
||||
}
|
|
@ -5,13 +5,19 @@
|
|||
|
||||
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { CheckoutForm } from './CheckoutForm';
|
||||
|
||||
interface StripeWrapperProps {
|
||||
amount: number;
|
||||
currency: string;
|
||||
cart: {
|
||||
id: string;
|
||||
version: number;
|
||||
email: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export function StripeWrapper({ amount, currency }: StripeWrapperProps) {
|
||||
export function StripeWrapper({ amount, currency, cart }: StripeWrapperProps) {
|
||||
// TODO - Load from config
|
||||
const stripePromise = loadStripe(
|
||||
'pk_test_VNpCidC0a2TJJB3wqXq7drhN00sF8r9mhs'
|
||||
|
@ -22,11 +28,37 @@ export function StripeWrapper({ amount, currency }: StripeWrapperProps) {
|
|||
amount,
|
||||
currency,
|
||||
paymentMethodCreation: 'manual',
|
||||
appearance: {
|
||||
variables: {
|
||||
fontFamily:
|
||||
'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
||||
fontSizeBase: '16px',
|
||||
fontWeightNormal: '500',
|
||||
colorDanger: '#D70022',
|
||||
},
|
||||
rules: {
|
||||
'.Tab': {
|
||||
borderColor: 'rgba(0,0,0,0.3)',
|
||||
},
|
||||
'.Input': {
|
||||
borderColor: 'rgba(0,0,0,0.3)',
|
||||
},
|
||||
'.Input::placeholder': {
|
||||
color: '#5E5E72', // Matches grey-500 of tailwind.config.js
|
||||
fontWeight: '400',
|
||||
},
|
||||
'.Label': {
|
||||
color: '#6D6D6E', // Matches grey-400 of tailwind.config.js
|
||||
fontWeight: '500',
|
||||
lineHeight: '1.15',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Elements stripe={stripePromise} options={options}>
|
||||
Hello there from Stripe
|
||||
<CheckoutForm cart={cart} />
|
||||
</Elements>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ import {
|
|||
PayPalManager,
|
||||
PaypalCustomerManager,
|
||||
} from '@fxa/payments/paypal';
|
||||
import { FirestoreService } from '@fxa/shared/db/firestore';
|
||||
import { StatsDService } from '@fxa/shared/metrics/statsd';
|
||||
import { FirestoreProvider } from '@fxa/shared/db/firestore';
|
||||
import { StatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -44,14 +44,8 @@ import { StatsDService } from '@fxa/shared/metrics/statsd';
|
|||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
{
|
||||
provide: FirestoreService,
|
||||
useValue: {}, //Temporary value to resolve Payments Next startup issues
|
||||
},
|
||||
{
|
||||
provide: StatsDService,
|
||||
useValue: {}, //Temporary value to resolve Payments Next startup issues
|
||||
},
|
||||
FirestoreProvider,
|
||||
StatsDProvider,
|
||||
LocalizerRscFactoryProvider,
|
||||
NextJSActionsService,
|
||||
AccountDatabaseNestFactory,
|
||||
|
|
|
@ -10,6 +10,8 @@ import { GeoDBConfig, GeoDBManagerConfig } from '@fxa/shared/geodb';
|
|||
import { PaypalClientConfig } from 'libs/payments/paypal/src/lib/paypal.client.config';
|
||||
import { StripeConfig } from '@fxa/payments/stripe';
|
||||
import { ContentfulClientConfig } from '@fxa/shared/contentful';
|
||||
import { FirestoreConfig } from 'libs/shared/db/firestore/src/lib/firestore.config';
|
||||
import { StatsDConfig } from 'libs/shared/metrics/statsd/src/lib/statsd.config';
|
||||
|
||||
export class RootConfig {
|
||||
@Type(() => MySQLConfig)
|
||||
|
@ -40,4 +42,14 @@ export class RootConfig {
|
|||
@ValidateNested()
|
||||
@IsDefined()
|
||||
public readonly contentfulClientConfig!: Partial<ContentfulClientConfig>;
|
||||
|
||||
@Type(() => FirestoreConfig)
|
||||
@ValidateNested()
|
||||
@IsDefined()
|
||||
public readonly firestoreConfig!: Partial<FirestoreConfig>;
|
||||
|
||||
@Type(() => StatsDConfig)
|
||||
@ValidateNested()
|
||||
@IsDefined()
|
||||
public readonly statsDConfig!: Partial<StatsDConfig>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 6.65132H12V4.66666C12 3.60579 11.5786 2.58837 10.8284 1.83823C10.0783 1.08808 9.06087 0.666656 8 0.666656C6.93913 0.666656 5.92172 1.08808 5.17157 1.83823C4.42143 2.58837 4 3.60579 4 4.66666V6.65132H3.5C3.10218 6.65132 2.72064 6.80936 2.43934 7.09066C2.15804 7.37197 2 7.7535 2 8.15132V13.1667C2 13.5645 2.15804 13.946 2.43934 14.2273C2.72064 14.5086 3.10218 14.6667 3.5 14.6667H12.5C12.8978 14.6667 13.2794 14.5086 13.5607 14.2273C13.842 13.946 14 13.5645 14 13.1667V8.15132C14 7.7535 13.842 7.37197 13.5607 7.09066C13.2794 6.80936 12.8978 6.65132 12.5 6.65132ZM6 4.66666C6 4.13622 6.21071 3.62752 6.58579 3.25244C6.96086 2.87737 7.46957 2.66666 8 2.66666C8.53043 2.66666 9.03914 2.87737 9.41421 3.25244C9.78929 3.62752 10 4.13622 10 4.66666V6.65132H6V4.66666Z" fill="white"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 896 B |
|
@ -1,3 +1,5 @@
|
|||
// Temporarily ignore. To be fixed during CMS refactor FXA-XXXX
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* 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/. */
|
||||
|
@ -71,19 +73,21 @@ export class ContentfulClient {
|
|||
return result;
|
||||
}
|
||||
|
||||
@Cacheable({
|
||||
cacheKey: (args: any) => cacheKeyForQuery(args[0], args[1]),
|
||||
strategy: new NetworkFirstStrategy(),
|
||||
ttlSeconds: (_, context: ContentfulClient) =>
|
||||
context.contentfulClientConfig.firestoreCacheTTL ||
|
||||
DEFAULT_FIRESTORE_CACHE_TTL,
|
||||
client: (_, context: ContentfulClient) =>
|
||||
new FirestoreAdapter(
|
||||
context.firestore,
|
||||
context.contentfulClientConfig.firestoreCacheCollectionName ||
|
||||
CONTENTFUL_QUERY_CACHE_KEY
|
||||
),
|
||||
})
|
||||
// Not sure what's happening here. Context is undefined which results in an error.
|
||||
// To be fixed during CMS refactor
|
||||
// @Cacheable({
|
||||
// cacheKey: (args: any) => cacheKeyForQuery(args[0], args[1]),
|
||||
// strategy: new NetworkFirstStrategy(),
|
||||
// ttlSeconds: (_, context: ContentfulClient) =>
|
||||
// context.contentfulClientConfig.firestoreCacheTTL ||
|
||||
// DEFAULT_FIRESTORE_CACHE_TTL,
|
||||
// client: (_, context: ContentfulClient) =>
|
||||
// new FirestoreAdapter(
|
||||
// context.firestore,
|
||||
// context.contentfulClientConfig.firestoreCacheCollectionName ||
|
||||
// CONTENTFUL_QUERY_CACHE_KEY
|
||||
// ),
|
||||
// })
|
||||
async query<Result, Variables extends OperationVariables>(
|
||||
query: TypedDocumentNode<Result, Variables>,
|
||||
variables: Variables
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/* 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 { Type } from 'class-transformer';
|
||||
import { IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||
|
||||
class FirestoreCredentialsConfig {
|
||||
@IsString()
|
||||
clientEmail!: string;
|
||||
|
||||
@IsString()
|
||||
privateKey!: string;
|
||||
}
|
||||
|
||||
export class FirestoreConfig {
|
||||
@Type(() => FirestoreCredentialsConfig)
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
credentials?: FirestoreCredentialsConfig;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
keyFilename?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
projectId?: string;
|
||||
}
|
|
@ -6,6 +6,7 @@ import { Firestore } from '@google-cloud/firestore';
|
|||
import * as grpc from '@grpc/grpc-js';
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { FirestoreConfig } from './firestore.config';
|
||||
|
||||
/**
|
||||
* Creates a firestore instance from a settings object.
|
||||
|
@ -41,19 +42,20 @@ export function setupFirestore(config: FirebaseFirestore.Settings) {
|
|||
* Factory for providing access to firestore
|
||||
*/
|
||||
export const FirestoreService = Symbol('FIRESTORE');
|
||||
|
||||
export const FirestoreProvider: Provider<Firestore> = {
|
||||
provide: FirestoreService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const firestoreConfig = configService.get('authFirestore');
|
||||
if (firestoreConfig == null) {
|
||||
throw new Error(
|
||||
"Could not locate config for firestore missing 'authFirestore';"
|
||||
);
|
||||
}
|
||||
const firestore = setupFirestore(firestoreConfig);
|
||||
return firestore;
|
||||
useFactory: (config: FirestoreConfig) => {
|
||||
const firestoreConfig: FirebaseFirestore.Settings = {
|
||||
...config,
|
||||
credentials: {
|
||||
client_email: config.credentials?.clientEmail,
|
||||
private_key: config.credentials?.privateKey,
|
||||
},
|
||||
};
|
||||
return setupFirestore(firestoreConfig);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
inject: [FirestoreConfig],
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -67,3 +69,18 @@ export const MockFirestoreProvider: Provider<Firestore> = {
|
|||
return {} as Firestore;
|
||||
},
|
||||
};
|
||||
|
||||
export const LegacyFirestoreProvider: Provider<Firestore> = {
|
||||
provide: FirestoreService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const firestoreConfig = configService.get('authFirestore');
|
||||
if (firestoreConfig == null) {
|
||||
throw new Error(
|
||||
"Could not locate config for firestore missing 'authFirestore';"
|
||||
);
|
||||
}
|
||||
const firestore = setupFirestore(firestoreConfig);
|
||||
return firestore;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
};
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/* 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 { IsInt, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class StatsDConfig {
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
sampleRate?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
maxBufferSize?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
host?: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
port?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
prefix?: string;
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { StatsDFactory, StatsDService } from './statsd.provider';
|
||||
import { LegacyStatsDProvider, StatsDService } from './statsd.provider';
|
||||
import { StatsD } from 'hot-shots';
|
||||
|
||||
const mockStatsd = jest.fn();
|
||||
|
@ -16,7 +16,7 @@ jest.mock('hot-shots', () => {
|
|||
};
|
||||
});
|
||||
|
||||
describe('StatsDFactory', () => {
|
||||
describe('LegacyStatsDProvider', () => {
|
||||
let statsd: StatsD;
|
||||
|
||||
const mockConfig = {
|
||||
|
@ -35,7 +35,7 @@ describe('StatsDFactory', () => {
|
|||
jest.clearAllMocks();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
StatsDFactory,
|
||||
LegacyStatsDProvider,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: mockConfigService,
|
||||
|
|
|
@ -5,9 +5,34 @@
|
|||
import { Provider } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { StatsD } from 'hot-shots';
|
||||
import { StatsDConfig } from './statsd.config';
|
||||
|
||||
export const StatsDService = Symbol('STATSD');
|
||||
export const StatsDFactory: Provider<StatsD> = {
|
||||
|
||||
export const StatsDProvider: Provider<StatsD> = {
|
||||
provide: StatsDService,
|
||||
useFactory: (config: StatsDConfig) => {
|
||||
if (config.host === '') {
|
||||
return new StatsD({ mock: true });
|
||||
}
|
||||
return new StatsD(config);
|
||||
},
|
||||
inject: [StatsDConfig],
|
||||
};
|
||||
|
||||
/**
|
||||
* Can be used to satisfy DI when unit testing things that should not need
|
||||
* statsd.
|
||||
* Note: this will cause errors to be thrown if statsd is used
|
||||
*/
|
||||
export const MockStatsDProvider: Provider<StatsD> = {
|
||||
provide: StatsDService,
|
||||
useFactory: () => {
|
||||
return {} as StatsD;
|
||||
},
|
||||
};
|
||||
|
||||
export const LegacyStatsDProvider: Provider<StatsD> = {
|
||||
provide: StatsDService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const config = configService.get('metrics');
|
||||
|
@ -18,15 +43,3 @@ export const StatsDFactory: Provider<StatsD> = {
|
|||
},
|
||||
inject: [ConfigService],
|
||||
};
|
||||
|
||||
/**
|
||||
* Can be used to satisfy DI when unit testing things that should not need
|
||||
* statsd.
|
||||
* Note: this will cause errors to be thrown if statsd is used
|
||||
*/
|
||||
export const MockStatsDFactory: Provider<StatsD> = {
|
||||
provide: StatsDService,
|
||||
useFactory: () => {
|
||||
return {} as StatsD;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
"@opentelemetry/sdk-trace-base": "^1.23.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.23.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.23.0",
|
||||
"@radix-ui/react-form": "^0.0.3",
|
||||
"@sentry/browser": "^7.113.0",
|
||||
"@sentry/integrations": "^7.113.0",
|
||||
"@sentry/node": "^7.113.0",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import { HealthModule } from 'fxa-shared/nestjs/health/health.module';
|
||||
import { MozLoggerService } from '@fxa/shared/mozlog';
|
||||
import { StatsDFactory } from '@fxa/shared/metrics/statsd';
|
||||
import { LegacyStatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
import { NotifierSnsFactory, NotifierService } from '@fxa/shared/notifier';
|
||||
|
||||
import { getVersionInfo } from 'fxa-shared/nestjs/version';
|
||||
|
@ -51,7 +51,7 @@ const version = getVersionInfo(__dirname);
|
|||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
StatsDFactory,
|
||||
LegacyStatsDProvider,
|
||||
NotifierSnsFactory,
|
||||
NotifierService,
|
||||
MozLoggerService,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { DatabaseService } from './database.service';
|
||||
import { MozLoggerService } from '@fxa/shared/mozlog';
|
||||
import { StatsDFactory } from '@fxa/shared/metrics/statsd';
|
||||
import { LegacyStatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
|
||||
@Module({
|
||||
providers: [DatabaseService, MozLoggerService, StatsDFactory],
|
||||
providers: [DatabaseService, MozLoggerService, LegacyStatsDProvider],
|
||||
exports: [DatabaseService],
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
|
|
|
@ -24,7 +24,7 @@ import { SubscriptionResolver } from './subscription.resolver';
|
|||
import { ClientInfoResolver } from './clientInfo.resolver';
|
||||
import { SessionResolver } from './session.resolver';
|
||||
import { NotifierService, NotifierSnsFactory } from '@fxa/shared/notifier';
|
||||
import { StatsDFactory } from '@fxa/shared/metrics/statsd';
|
||||
import { LegacyStatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
import { MozLoggerService } from '@fxa/shared/mozlog';
|
||||
|
||||
const config = Config.getProperties();
|
||||
|
@ -52,7 +52,7 @@ export const GraphQLConfigFactory = async (
|
|||
@Module({
|
||||
imports: [BackendModule, CustomsModule],
|
||||
providers: [
|
||||
StatsDFactory,
|
||||
LegacyStatsDProvider,
|
||||
NotifierSnsFactory,
|
||||
NotifierService,
|
||||
AccountResolver,
|
||||
|
|
46
yarn.lock
46
yarn.lock
|
@ -14915,6 +14915,31 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-form@npm:^0.0.3":
|
||||
version: 0.0.3
|
||||
resolution: "@radix-ui/react-form@npm:0.0.3"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/primitive": 1.0.1
|
||||
"@radix-ui/react-compose-refs": 1.0.1
|
||||
"@radix-ui/react-context": 1.0.1
|
||||
"@radix-ui/react-id": 1.0.1
|
||||
"@radix-ui/react-label": 2.0.2
|
||||
"@radix-ui/react-primitive": 1.0.3
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: a2cc8e3787643b16e8c7a77aeac3707e7d7858ca65818631d46184abd7ab7047d496f646f404cbeddc4aeaf974a4e0d778044b09236d156ca2ab6f5e21851dac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-id@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@radix-ui/react-id@npm:1.0.1"
|
||||
|
@ -14931,6 +14956,26 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-label@npm:2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "@radix-ui/react-label@npm:2.0.2"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-primitive": 1.0.3
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: fe3bd8902bc523fb5125fa96167a13b8a60d007413787eae9573e4b00b0edff0487c4c0620ea5dc37e6da13833ebc4f8d7e00b6c846f2a5686e7f173672b8dde
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-popper@npm:1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "@radix-ui/react-popper@npm:1.1.2"
|
||||
|
@ -38670,6 +38715,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
|
||||
"@radix-ui/react-form": ^0.0.3
|
||||
"@sentry/browser": ^7.113.0
|
||||
"@sentry/integrations": ^7.113.0
|
||||
"@sentry/node": ^7.113.0
|
||||
|
|
Загрузка…
Ссылка в новой задаче