diff --git a/apps/backend/src/payments/payments.service.ts b/apps/backend/src/payments/payments.service.ts index f1bf07c..bb422cc 100644 --- a/apps/backend/src/payments/payments.service.ts +++ b/apps/backend/src/payments/payments.service.ts @@ -177,7 +177,7 @@ export class PaymentsService { amount: request.amount, currency: request.currency, metadata: request.metadata, - payment_method_types: ['card', 'us_bank_accounts'], + payment_method_types: ['card'], }); this.logger.debug( diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 36b3688..bee4cb4 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -6,12 +6,13 @@ export type DonationCreateRequest = { firstName: string; lastName: string; email: string; - amount: number; // parsed to number in the form + amount: number; isAnonymous: boolean; donationType: 'one_time' | 'recurring'; - dedicationMessage: string; // allow '' from ui + dedicationMessage: string; showDedicationPublicly: boolean; - recurringInterval?: 'weekly' | 'monthly' | 'yearly'; + recurringInterval?: 'weekly' | 'monthly' | 'annually'; + paymentIntentId?: string; }; export type CreateDonationResponse = { id: string }; @@ -42,18 +43,35 @@ export type UpdateGoalRequest = { endDate: string; }; +export type CreatePaymentIntentRequest = { + amount: number; // in cents + currency: string; + metadata?: Record; +}; + +export type PaymentIntentResponse = { + id: string; + clientSecret: string; + amount: number; + currency: string; + status: string; +}; + export type SignInRequest = { email: string; password: string }; + export type SignUpRequest = { firstName: string; lastName: string; email: string; password: string; }; + export type AuthResponse = { accessToken: string; refreshToken: string; idToken: string; }; + export type RefreshRequest = { refreshToken: string; userSub: string }; export type ConfirmPasswordRequest = { email: string; @@ -71,11 +89,30 @@ export class ApiClient { } public async getHello(): Promise { - //return this.get('/api') as Promise; const res = await this.axiosInstance.get('/api'); return res.data; } + public async createPaymentIntent( + body: CreatePaymentIntentRequest, + ): Promise { + try { + const res = await this.axiosInstance.post('/api/payments/intent', body); + return res.data as PaymentIntentResponse; + } catch (err: unknown) { + if (axios.isAxiosError(err)) { + const data = err.response?.data; + const msg = + data?.error ?? + data?.message ?? + err.message ?? + 'Failed to create payment intent'; + throw new Error(msg); + } + throw new Error('Failed to create payment intent'); + } + } + public setAuthToken(token: string | null) { if (token) { this.axiosInstance.defaults.headers.common['Authorization'] = diff --git a/apps/frontend/src/containers/donations/DonationForm.tsx b/apps/frontend/src/containers/donations/DonationForm.tsx index 9e66484..6a9c88a 100644 --- a/apps/frontend/src/containers/donations/DonationForm.tsx +++ b/apps/frontend/src/containers/donations/DonationForm.tsx @@ -2,7 +2,8 @@ import apiClient, { type CreateDonationResponse, type CreateDonationRequest, } from '../../api/apiClient'; -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; +import { type Step2DetailsRef } from './steps/Step2Details'; import { useSearchParams } from 'react-router-dom'; import './donations.css'; import { @@ -15,6 +16,7 @@ import { Step1Amount } from './steps/Step1Amount'; import { Step2Details } from './steps/Step2Details'; import { Step3Confirm } from './steps/Step3Confirm'; import { Step4Receipt } from './steps/Step4Receipt'; +import { StripeProvider } from './StripeProvider'; import { Button } from '@components/ui/button'; export const DonationForm: React.FC = ({ @@ -41,15 +43,15 @@ export const DonationForm: React.FC = ({ recurringInterval: 'monthly', isDedicated: false, dedicationKind: null, - cardNumber: '', - cardExpiry: '', - cardCvc: '', coverFees: false, }); const [errors, setErrors] = useState>({}); const [isSubmitting, setIsSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); + const [paymentMethodId, setPaymentMethodId] = useState(null); + const step2Ref = useRef(null); + const step3SubmitRef = useRef<(() => void) | null>(null); const [receiptId, setReceiptId] = useState( searchParams.get('receiptId'), ); @@ -164,14 +166,37 @@ export const DonationForm: React.FC = ({ setSubmitError(null); }; - const handleNext = () => { + const handleNext = async () => { + setSubmitError(null); if (!validateStep(currentStep)) { return; } + if (currentStep === 2) { + try { + const pmId = await step2Ref.current?.createPaymentMethod(); + if (!pmId) { + setSubmitError('Could not process card. Please try again.'); + return; + } + setPaymentMethodId(pmId); + } catch (err) { + setSubmitError( + err instanceof Error ? err.message : 'Could not process card.', + ); + return; + } + } + if (currentStep === 3) { + if (step3SubmitRef.current) { + step3SubmitRef.current(); + } + return; + } setCurrentStep((prev) => clampStep(prev + 1)); }; const handleBack = () => { + setSubmitError(null); setCurrentStep((prev) => clampStep(prev - 1)); }; @@ -186,9 +211,6 @@ export const DonationForm: React.FC = ({ dedicationMessage: '', showDedicationPublicly: false, recurringInterval: 'monthly', - cardNumber: '', - cardExpiry: '', - cardCvc: '', coverFees: false, }); setErrors({}); @@ -197,27 +219,7 @@ export const DonationForm: React.FC = ({ setCurrentStep(1); }; - const handleSubmit = async () => { - if (isSubmitting) { - return; - } - - const step1Valid = validateStep(1); - - if (!step1Valid) { - setCurrentStep(1); - return; - } - - const step2Valid = validateStep(2); - if (!step2Valid) { - setCurrentStep(2); - return; - } - - setIsSubmitting(true); - setSubmitError(null); - + const handlePaymentSuccess = async (paymentIntentId: string) => { try { const payload: CreateDonationRequest = { firstName: formData.firstName.trim(), @@ -228,6 +230,7 @@ export const DonationForm: React.FC = ({ donationType: formData.donationType, dedicationMessage: formData.dedicationMessage, showDedicationPublicly: formData.showDedicationPublicly, + paymentIntentId, ...(formData.donationType === 'recurring' && { recurringInterval: formData.recurringInterval, }), @@ -241,10 +244,8 @@ export const DonationForm: React.FC = ({ setCurrentStep(4); } catch (error) { const err = error as Error; - setSubmitError(err.message || 'Failed to submit donation'); + setSubmitError(err.message || 'Failed to record donation'); onError(err); - } finally { - setIsSubmitting(false); } }; @@ -262,24 +263,39 @@ export const DonationForm: React.FC = ({ case 2: return ( - + + + ); case 3: - return ; - + return ( + + setSubmitError(error)} + isSubmitting={isSubmitting} + setIsSubmitting={setIsSubmitting} + onSubmitRef={step3SubmitRef} + /> + + ); + case 4: default: - return ; + return ; } }; const showBackButton = currentStep > 1 && currentStep < 4; - const showNextButton = currentStep < 3; + const showNextButton = currentStep < 4; return (
@@ -288,27 +304,27 @@ export const DonationForm: React.FC = ({ onSubmit={(e) => e.preventDefault()} noValidate > -
-
- -
- -
-
+ {currentStep < 4 && ( +
+
+ +
+ +
+
+ )} {submitError && (
{submitError} @@ -317,9 +333,7 @@ export const DonationForm: React.FC = ({ {renderStep()} -
+
{showBackButton && ( )} - {showNextButton && ( + {showNextButton && currentStep < 4 && ( - )} - - {currentStep === 3 && ( - )} - {currentStep === 4 && ( - - )} + {currentStep === 4 && null}
diff --git a/apps/frontend/src/containers/donations/DonationSummary.tsx b/apps/frontend/src/containers/donations/DonationSummary.tsx index 66152d2..7152f96 100644 --- a/apps/frontend/src/containers/donations/DonationSummary.tsx +++ b/apps/frontend/src/containers/donations/DonationSummary.tsx @@ -28,7 +28,7 @@ export const DonationSummary = ({ setCurrentAmount?.(next ? baseAmount + feeTotal : baseAmount); }; - const feeText = `Add $${feeTotal.toFixed(2)} to cover transaction fees and tip the fundraising platform to help keep it `; + const feeText = `Add $${feeTotal.toFixed(2)} to cover transaction fees and help keep it `; const toggleClass = feeApplied ? 'border-2 border-[#2C8974] bg-[#F0F0F0]' @@ -39,7 +39,7 @@ export const DonationSummary = ({ : 'bg-white left-[10%]'; return ( -
+

- -
); }; diff --git a/apps/frontend/src/containers/donations/StripeProvider.tsx b/apps/frontend/src/containers/donations/StripeProvider.tsx new file mode 100644 index 0000000..3e180d3 --- /dev/null +++ b/apps/frontend/src/containers/donations/StripeProvider.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { loadStripe } from '@stripe/stripe-js'; +import { Elements } from '@stripe/react-stripe-js'; + +const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY; + +if (!publishableKey) { + throw new Error('Missing VITE_STRIPE_PUBLISHABLE_KEY environment variable'); +} + +const stripePromise = loadStripe(publishableKey); + +interface StripeProviderProps { + children: React.ReactNode; +} + +export const StripeProvider: React.FC = ({ children }) => { + const options = { + appearance: { + theme: 'stripe' as const, + variables: { + colorPrimary: '#0570de', + colorBackground: '#ffffff', + colorText: '#30313d', + colorDanger: '#df1b41', + fontFamily: 'Source Sans 3, system-ui, sans-serif', + spacingUnit: '4px', + borderRadius: '4px', + }, + }, + }; + + return ( + + {children} + + ); +}; diff --git a/apps/frontend/src/containers/donations/donation-form.config.ts b/apps/frontend/src/containers/donations/donation-form.config.ts index 77faf39..a2181bb 100644 --- a/apps/frontend/src/containers/donations/donation-form.config.ts +++ b/apps/frontend/src/containers/donations/donation-form.config.ts @@ -25,7 +25,7 @@ export const DONATION_RECURRENCE_OPTIONS: DonationRecurrenceOption[] = [ }, { donationType: 'recurring', - label: 'Yearly', - recurringInterval: 'yearly', + label: 'Annually', + recurringInterval: 'annually', }, ]; diff --git a/apps/frontend/src/containers/donations/donation-form.types.ts b/apps/frontend/src/containers/donations/donation-form.types.ts index 25332d7..cefac19 100644 --- a/apps/frontend/src/containers/donations/donation-form.types.ts +++ b/apps/frontend/src/containers/donations/donation-form.types.ts @@ -1,4 +1,4 @@ -export type RecurringInterval = 'weekly' | 'monthly' | 'yearly'; +export type RecurringInterval = 'weekly' | 'monthly' | 'annually'; export type DedicationKind = 'honor' | 'memory'; @@ -16,9 +16,6 @@ export interface DonationFormData { isAnonymous: boolean; dedicationMessage: string; showDedicationPublicly: boolean; - cardNumber: string; - cardExpiry: string; - cardCvc: string; coverFees: boolean; } diff --git a/apps/frontend/src/containers/donations/steps/DonationAmount.tsx b/apps/frontend/src/containers/donations/steps/DonationAmount.tsx index 4d0f227..d4046ab 100644 --- a/apps/frontend/src/containers/donations/steps/DonationAmount.tsx +++ b/apps/frontend/src/containers/donations/steps/DonationAmount.tsx @@ -38,10 +38,10 @@ export const DonationAmount = ({ key={preset} type="button" className={cn( - 'flex-1 h-12 px-4 rounded border text-base font-semibold cursor-pointer transition-colors duration-150 ease-in-out disabled:opacity-60 disabled:cursor-not-allowed', + 'flex-1 h-12 px-4 rounded-lg border-[1.5px] text-base font-semibold cursor-pointer transition-colors duration-150 ease-in-out disabled:opacity-60 disabled:cursor-not-allowed', isAmountSelected(preset) ? 'bg-[#007b64] text-white border-[#007b64]' - : 'bg-white text-black border-gray-300', + : 'bg-white text-black border-[#4E4E4E]', )} onClick={() => onPresetClick(preset)} disabled={isSubmitting} @@ -71,7 +71,7 @@ export const DonationAmount = ({ onChange={onChange} onBlur={onAmountBlur} className={cn( - 'pl-7 pr-12 h-10 text-base font-normal', + 'pl-7 pr-12 h-10 text-base font-normal border-[#4E4E4E] border-[1.5px] shadow-none focus-visible:ring-0 rounded-lg', error ? 'border-[#d93025] bg-[#fff6f6]' : '', )} disabled={isSubmitting} diff --git a/apps/frontend/src/containers/donations/steps/DonationRecurrence.tsx b/apps/frontend/src/containers/donations/steps/DonationRecurrence.tsx index 1504b9a..507c40a 100644 --- a/apps/frontend/src/containers/donations/steps/DonationRecurrence.tsx +++ b/apps/frontend/src/containers/donations/steps/DonationRecurrence.tsx @@ -33,10 +33,10 @@ export const DonationRecurrence = ({ key={option.label} type="button" className={cn( - 'flex-1 h-12 px-4 whitespace-nowrap rounded border text-base cursor-pointer transition-colors duration-150 ease-in-out font-semibold disabled:opacity-60 disabled:cursor-not-allowed', + 'flex-1 h-12 px-4 whitespace-nowrap rounded-lg border-[1.5px] text-base cursor-pointer transition-colors duration-150 ease-in-out font-semibold disabled:opacity-60 disabled:cursor-not-allowed', isRecurrenceSelected(option) ? 'bg-[#007b64] text-white border-[#007b64]' - : 'bg-white text-black border-gray-300', + : 'bg-white text-black border-[#4E4E4E]', )} onClick={() => onRecurrenceClick(option)} disabled={isSubmitting} diff --git a/apps/frontend/src/containers/donations/steps/Step1Amount.tsx b/apps/frontend/src/containers/donations/steps/Step1Amount.tsx index b9f9a5b..3634054 100644 --- a/apps/frontend/src/containers/donations/steps/Step1Amount.tsx +++ b/apps/frontend/src/containers/donations/steps/Step1Amount.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { ToggleSwitch } from '@components/ToggleSwitch'; import { DonationRecurrence } from './DonationRecurrence'; import { DonationAmount } from './DonationAmount'; diff --git a/apps/frontend/src/containers/donations/steps/Step2Details.tsx b/apps/frontend/src/containers/donations/steps/Step2Details.tsx index 2480d9b..4e2afef 100644 --- a/apps/frontend/src/containers/donations/steps/Step2Details.tsx +++ b/apps/frontend/src/containers/donations/steps/Step2Details.tsx @@ -1,9 +1,14 @@ -import React, { useState } from 'react'; +import React, { forwardRef, useImperativeHandle, useState } from 'react'; +import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js'; import { Input } from '@components/ui/input'; import { FormField } from './FormField'; import { DonationSummary } from '../DonationSummary'; import type { DonationFormData, FormErrors } from '../donation-form.types'; +export interface Step2DetailsRef { + createPaymentMethod: () => Promise; +} + type Step2DetailsProps = { formData: DonationFormData; errors: Partial; @@ -15,151 +20,158 @@ type Step2DetailsProps = { ) => void; }; -const CardIcon = () => ( - - - - -); +const cardElementOptions = { + style: { + base: { + fontSize: '16px', + fontFamily: 'Source Sans 3, system-ui, sans-serif', + color: '#30313d', + '::placeholder': { + color: '#B3B8C7', + }, + }, + invalid: { + color: '#df1b41', + }, + }, +}; -export const Step2Details = ({ - formData, - errors, - isSubmitting, - onChange, -}: Step2DetailsProps) => { - const baseAmount = parseFloat(formData.amount) || 0; - const [currentAmount, setCurrentAmount] = useState(baseAmount); - - return ( -
-
- Total - - ${currentAmount.toFixed(2)} - -
+export const Step2Details = forwardRef( + function Step2Details({ formData, errors, isSubmitting, onChange }, ref) { + const stripe = useStripe(); + const elements = useElements(); + const baseAmount = parseFloat(formData.amount) || 0; + const [currentAmount, setCurrentAmount] = useState(baseAmount); + const [cardError, setCardError] = useState(null); -
-

Payment Details

+ useImperativeHandle(ref, () => ({ + async createPaymentMethod(): Promise { + if (!stripe || !elements) { + throw new Error('Stripe has not loaded yet. Please try again.'); + } -
- - +
+ Total + + ${currentAmount.toFixed(2)} + +
+ +
+

Payment Details

+ +
+ - + label="First Name" + required + error={errors.firstName} + > + + + + + + +
-
- - - - - - -
- - - - + +
+ { + setCardError(event.error ? event.error.message : null); + }} + /> +
+
+
- -
- +
- - -
- ); -}; + ); + }, +); diff --git a/apps/frontend/src/containers/donations/steps/Step3Confirm.tsx b/apps/frontend/src/containers/donations/steps/Step3Confirm.tsx index 4c1c015..dcb7ed5 100644 --- a/apps/frontend/src/containers/donations/steps/Step3Confirm.tsx +++ b/apps/frontend/src/containers/donations/steps/Step3Confirm.tsx @@ -1,65 +1,149 @@ -import React from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; +import { useStripe } from '@stripe/react-stripe-js'; import type { DonationFormData } from '../donation-form.types'; -import { Card, CardHeader, CardTitle, CardContent } from '@components/ui/card'; +import apiClient from '../../../api/apiClient'; +import { Card } from '@components/ui/card'; interface Step3ConfirmProps { formData: DonationFormData; + paymentMethodId: string | null; + onPaymentSuccess: (paymentIntentId: string) => void; + onPaymentError: (error: string) => void; + isSubmitting: boolean; + setIsSubmitting: (value: boolean) => void; + onSubmitRef?: React.MutableRefObject<(() => void) | null>; } -export const Step3Confirm: React.FC = ({ formData }) => { +export const Step3Confirm: React.FC = ({ + formData, + paymentMethodId, + onPaymentSuccess, + onPaymentError, + isSubmitting, // eslint-disable-line @typescript-eslint/no-unused-vars + setIsSubmitting, + onSubmitRef, +}) => { + const stripe = useStripe(); + const [error, setError] = useState(null); + const amount = parseFloat(formData.amount) || 0; - return ( -
-
-

Confirm Payment

-

- Please confirm your information before proceeding. -

-
+ const handleConfirmPayment = useCallback(async () => { + if (!stripe) { + setError('Stripe has not loaded yet. Please try again.'); + return; + } + + if (!paymentMethodId) { + setError( + 'Card information not found. Please go back and re-enter your card details.', + ); + return; + } + + setIsSubmitting(true); + setError(null); + + try { + // Step 1: Create PaymentIntent + const amountInCents = Math.round(amount * 100); + const paymentIntentResponse = await apiClient.createPaymentIntent({ + amount: amountInCents, + currency: 'usd', + metadata: { + email: formData.email, + donationType: formData.donationType, + }, + }); - - - - Transaction Details - - - -
-
-
Donor
-
- {formData.firstName} {formData.lastName} -
-
-
-
Email
-
{formData.email}
-
-
-
Anonymous?
-
- {formData.isAnonymous ? 'Yes' : 'No'} -
-
+ // Step 2: Confirm the payment with Stripe + const { error: stripeError, paymentIntent } = + await stripe.confirmCardPayment(paymentIntentResponse.clientSecret, { + payment_method: paymentMethodId, + }); -
-
+ if (paymentIntent && paymentIntent.status === 'succeeded') { + // Step 3: Payment successful - notify parent + onPaymentSuccess(paymentIntent.id); + } else { + setError('Payment was not completed. Please try again.'); + onPaymentError('Payment was not completed'); + } + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'An unexpected error occurred'; + setError(errorMessage); + onPaymentError(errorMessage); + } finally { + setIsSubmitting(false); + } + }, [ + stripe, + paymentMethodId, + amount, + formData.email, + formData.donationType, + setIsSubmitting, + onPaymentSuccess, + onPaymentError, + ]); + + useEffect(() => { + if (onSubmitRef) { + onSubmitRef.current = handleConfirmPayment; + } + }, [onSubmitRef, handleConfirmPayment]); + + return ( +
+

Confirm Payment

+

+ Please confirm your information before proceeding. +

+ +

Transaction Details

+
+
+
Donor
+
+ {formData.firstName} {formData.lastName} +
+
+
+
Email
+
{formData.email}
+
+
+
Anonymous?
+
{formData.isAnonymous ? 'Yes' : 'No'}
+
+
+
+
Recurrence
+
+ {formData.donationType === 'one_time' + ? 'One-time' + : `Recurring (${formData.recurringInterval})`} +
+
+
+
Donation Amount
+
${amount.toFixed(2)}
+
+
+ + {error && ( +
+ {error} +
+ )}
); }; diff --git a/apps/frontend/src/containers/donations/steps/Step4Receipt.tsx b/apps/frontend/src/containers/donations/steps/Step4Receipt.tsx index 0f58043..b9fa414 100644 --- a/apps/frontend/src/containers/donations/steps/Step4Receipt.tsx +++ b/apps/frontend/src/containers/donations/steps/Step4Receipt.tsx @@ -10,6 +10,7 @@ import CarouselImage3 from '@components/testimonials/TestimonialImages/Carousel_ interface Step4ReceiptProps { receiptId?: string | null; + onReset: () => void; } const TESTIMONIAL_SLIDES = [ @@ -30,7 +31,10 @@ const TESTIMONIAL_SLIDES = [ }, ]; -export const Step4Receipt: React.FC = ({ receiptId }) => { +export const Step4Receipt: React.FC = ({ + receiptId, + onReset, +}) => { const [feedback, setFeedback] = useState(''); const handleSpreadTheWord = () => { @@ -84,13 +88,30 @@ export const Step4Receipt: React.FC = ({ receiptId }) => { value={feedback} onChange={(e) => setFeedback(e.target.value)} placeholder="Share with us here" - className="min-h-[120px]" + className="min-h-[120px] border-[#4E4E4E] border-[1.5px] rounded-lg shadow-none focus-visible:ring-0" rows={4} />
-
+
+
+ + +
-
- -
); }; diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c2218ab..0e7e6bc 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -102,7 +102,7 @@ services: VITE_API_BASE_URL: http://localhost:3000 - STRIPE_SECRET_KEY: 'sk_test_1234567890' + STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY} AWS_SES_REGION: us-east-2 AWS_SES_ACCESS_KEY_ID: ${AWS_SES_ACCESS_KEY_ID} diff --git a/package.json b/package.json index 5776ff1..7ca86fb 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,8 @@ "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", + "@stripe/react-stripe-js": "^5.6.0", + "@stripe/stripe-js": "^8.7.0", "@tailwindcss/typography": "^0.5.19", "@tiptap/extension-highlight": "^3.22.3", "@tiptap/extension-link": "^3.22.3", diff --git a/yarn.lock b/yarn.lock index 723ae26..d12702e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5559,6 +5559,18 @@ resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b" integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g== +"@stripe/react-stripe-js@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-5.6.0.tgz#0e8fa984d3b982adf57a9bb81c9445e6d1dc0478" + integrity sha512-tucu/vTGc+5NXbo2pUiaVjA4ENdRBET8qGS00BM4BAU8J4Pi3eY6BHollsP2+VSuzzlvXwMg0it3ZLhbCj2fPg== + dependencies: + prop-types "^15.7.2" + +"@stripe/stripe-js@^8.7.0": + version "8.7.0" + resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-8.7.0.tgz#5e875a25768de2be1f69e9b4aa0c283af83feedd" + integrity sha512-tNUerSstwNC1KuHgX4CASGO0Md3CB26IJzSXmVlSuFvhsBP4ZaEPpY4jxWOn9tfdDscuVT4Kqb8cZ2o9nLCgRQ== + "@svgr/babel-plugin-add-jsx-attribute@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" @@ -7425,6 +7437,15 @@ axios@^1.11.0, axios@^1.12.0, axios@^1.5.0: form-data "^4.0.5" proxy-from-env "^2.1.0" +axios@^1.11.0, axios@^1.12.0, axios@^1.5.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.0.tgz#0fcee91ef03d386514474904b27863b2c683bf4f" + integrity sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q== + dependencies: + follow-redirects "^1.15.11" + form-data "^4.0.5" + proxy-from-env "^2.1.0" + axobject-query@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" @@ -7876,6 +7897,16 @@ call-bind@^1.0.7, call-bind@^1.0.8, call-bind@^1.0.9: get-intrinsic "^1.3.0" set-function-length "^1.2.2" +call-bind@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.9.tgz#39a644700c80bc7d0ca9102fc6d1d43b2fd7eee7" + integrity sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + get-intrinsic "^1.3.0" + set-function-length "^1.2.2" + call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" @@ -7953,6 +7984,14 @@ chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" @@ -9421,6 +9460,66 @@ es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23 unbox-primitive "^1.1.0" which-typed-array "^1.1.19" +es-abstract@^1.24.2: + version "1.24.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.2.tgz#2dbd38c180735ee983f77585140a2706a963ed9a" + integrity sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg== + dependencies: + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.3.0" + get-proto "^1.0.1" + get-symbol-description "^1.1.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" + is-callable "^1.2.7" + is-data-view "^1.0.2" + is-negative-zero "^2.0.3" + is-regex "^1.2.1" + is-set "^2.0.3" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.1" + math-intrinsics "^1.1.0" + object-inspect "^1.13.4" + object-keys "^1.1.1" + object.assign "^4.1.7" + own-keys "^1.0.1" + regexp.prototype.flags "^1.5.4" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + stop-iteration-iterator "^1.1.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.19" + es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" @@ -12795,6 +12894,11 @@ linkifyjs@^4.3.2: resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1" integrity sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA== +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + lint-staged@^14.0.1: version "14.0.1" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-14.0.1.tgz#57dfa3013a3d60762d9af5d9c83bdb51291a6232" @@ -15141,7 +15245,7 @@ promise.series@^0.2.0: resolved "https://registry.yarnpkg.com/promise.series/-/promise.series-0.2.0.tgz#2cc7ebe959fc3a6619c04ab4dbdc9e452d864bbd" integrity sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ== -prop-types@^15.8.1: +prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==