feat(next): add consent checkbox

Because:

* Add consent checkbox, reminder error tooltip and payment section with
  default disabled state.

This commit:

* Adds a new client component for the consent checkbox and implements
  a Radix holdover component for the error tooltip.
* Adds PaymentSection client component.

Closes: #FXA-7806
This commit is contained in:
Reino Muhl 2024-04-30 09:41:55 -04:00
Родитель 1203c6a3c4
Коммит 6409c5e422
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: C86660FCF998897A
13 изменённых файлов: 466 добавлений и 187 удалений

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

@ -2,3 +2,8 @@
next-new-user-step-1-2 = 1. Create a { -product-mozilla-account }
next-new-user-sign-in-link-2 = Already have a { -product-mozilla-account }? <a>Sign in</a>
next-payment-confirm-with-legal-links-static-3 = I authorize { -brand-mozilla } to charge my payment method for the amount shown, according to <termsOfServiceLink>Terms of Service</termsOfServiceLink> and <privacyNoticeLink>Privacy Notice</privacyNoticeLink>, until I cancel my subscription.
next-payment-method-header = Choose your payment method
# This message is used to indicate the second step in a multi step process.
payment-method-header-second-step-next = 2. { next-payment-method-header }
next-payment-method-first-approve = First youll need to approve your subscription

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

@ -10,8 +10,11 @@ import { DEFAULT_LOCALE } from '@fxa/shared/l10n';
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';
import {
getFakeCartData,
getContentfulContent,
} from 'apps/payments/next/app/_lib/apiClient';
import { PaymentSection } from '@fxa/payments/ui';
export const dynamic = 'force-dynamic';
@ -31,169 +34,100 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
);
//TODO - Replace with cartPromise as part of FXA-8903
const fakeCartDataPromise = getFakeCartData(params.cartId);
const [session, l10n, cart, fakeCart] = await Promise.all([
const cmsPromise = getContentfulContent(params.offeringId, locale);
const [session, l10n, cart, fakeCart, cms] = await Promise.all([
sessionPromise,
l10nPromise,
cartPromise,
fakeCartDataPromise,
cmsPromise,
]);
return (
<>
<section
className="h-min-[640px]"
aria-label="Section under construction"
>
{!session && (
<>
<h4 className="font-semibold text-grey-600 text-lg">
{l10n.getString(
'next-new-user-step-1-2',
'1. Create a Mozilla account'
)}
</h4>
<section>
{!session && (
<>
<h4 className="font-semibold text-grey-600 text-lg mt-10">
{l10n.getString(
'next-new-user-step-1-2',
'1. Create a Mozilla account'
)}
</h4>
<form
action={async () => {
'use server';
await signIn('fxa');
}}
>
<div className="text-grey-400 text-sm">
{l10n.getFragmentWithSource(
'next-new-user-sign-in-link-2',
{
elems: {
a: (
<button className="underline text-grey-400 hover:text-grey-400">
Sign in
</button>
),
},
},
<button className="underline text-grey-400 hover:text-grey-400">
Sign in
</button>
)}
</div>
</form>
<hr className="mx-auto my-4 w-full border-grey-200" />
</>
)}
<section className="flex flex-col gap-2 mb-8">
<div>
<h3 className="text-xl">Temporary L10n Section</h3>
<p className="text-sm">
Temporary section to illustrate various translations using the
Localizer classes
</p>
</div>
<div>
<h4>Regular translation - no variables</h4>
<p className="text-sm">
{l10n.getString('app-footer-mozilla-logo-label', 'testing2')}
</p>
</div>
<div>
<h4>Regular translation - with variables</h4>
<p className="text-sm">
{l10n.getString(
'app-page-title-2',
{ title: 'Test Title' },
'testing2'
)}
</p>
</div>
<div>
<h4>Regular translation - With Selector</h4>
<p className="text-sm">
{l10n.getString(
'next-plan-price-interval-day',
{ intervalCount: 2, amount: 20 },
'testing2'
)}
</p>
</div>
<div>
<h4>Regular translation - With Currency</h4>
<p className="text-sm">
{l10n.getString(
'list-positive-amount',
{
amount: l10n.getLocalizedCurrency(502, 'usd'),
},
`${l10n.getLocalizedCurrencyString(502, 'usd')}`
)}
</p>
</div>
<div>
<h4>Regular translation - With Date</h4>
<p className="text-sm">
{l10n.getString(
'list-positive-amount',
{
amount: l10n.getLocalizedCurrency(502, 'usd'),
},
`${l10n.getLocalizedCurrencyString(502, 'usd')}`
)}
</p>
</div>
<div>
<h4>Get Fragment with Fallback element</h4>
<p className="text-sm">
<form
action={async () => {
'use server';
await signIn('fxa');
}}
>
<p className="text-grey-400 text-sm mt-2 mb-4">
{l10n.getFragmentWithSource(
'next-payment-legal-link-stripe-3',
'next-new-user-sign-in-link-2',
{
elems: {
stripePrivacyLink: (
<a href="https://stripe.com/privacy">
Stripe privacy policy
</a>
),
},
},
<a href="https://stripe.com/privacy">Stripe privacy policy</a>
)}
</p>
</div>
<div>
<h4>Get Element - With reference</h4>
<p className="text-sm">
{l10n.getFragmentWithSource(
'next-payment-confirm-with-legal-links-static-3',
{
elems: {
termsOfServiceLink: (
<a href="https://stripe.com/privacy">
Stripe privacy policy
</a>
),
privacyNoticeLink: (
<a href="https://stripe.com/privacy">
Stripe privacy policy
</a>
a: (
<button className="underline text-grey-400 hover:text-grey-400">
Sign in
</button>
),
},
},
<>
I authorize Mozilla to charge my payment method for the amount
shown, according to{' '}
<a href="https://www.example.com">Terms of Service</a> and{' '}
<a href="https://www.example.com">Privacy Notice</a>, until I
cancel my subscription.
Already have a Mozilla account?&nbsp;
<button className="underline text-grey-400 hover:text-grey-400">
Sign in
</button>
</>
)}
</p>
</form>
<hr className="mx-auto w-full border-grey-200" />
<div className="h-64 text-center flex items-center justify-center">
{'<placeholder>Passwordless signup</placeholder>'}
</div>
</section>
<StripeWrapper
amount={fakeCart.amount}
currency={fakeCart.nextInvoice.currency}
cart={cart}
/>
</section>
</>
<hr className="mx-auto w-full border-grey-200" />
</>
)}
{!session ? (
<h4
className="font-semibold text-grey-600 text-lg mt-14 mb-5"
data-testid="header-prefix"
>
{l10n.getString(
'payment-method-header-second-step-next',
'2. Choose your payment method2'
)}
</h4>
) : (
<h4
className="font-semibold text-grey-600 text-lg mt-14 mb-5"
data-testid="header"
>
{l10n.getString(
'next-payment-method-header',
'Choose your payment method'
)}
</h4>
)}
<p className="font-semibold my-3 text-grey-600 text-start">
{l10n.getString(
'next-payment-method-first-approve',
`First you'll need to approve your subscription`
)}
</p>
<PaymentSection
cmsCommonContent={cms.commonContent}
paymentsInfo={{
amount: fakeCart.amount,
currency: fakeCart.nextInvoice.currency,
}}
cart={cart}
/>
</section>
);
}

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

@ -7,3 +7,5 @@
export * from './lib/utils/helpers';
export * from './lib/client/components/StripeWrapper';
export * from './lib/client/components/Providers';
export * from './lib/client/components/CheckoutCheckbox';
export * from './lib/client/components/PaymentSection';

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

@ -0,0 +1,5 @@
## Component - Payment Consent Checkbox
next-payment-confirm-with-legal-links-static-3 = I authorize { -brand-mozilla } to charge my payment method for the amount shown, according to <termsOfServiceLink>Terms of Service</termsOfServiceLink> and <privacyNoticeLink>Privacy Notice</privacyNoticeLink>, until I cancel my subscription.
next-payment-confirm-checkbox-error = You need to complete this before moving forward

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

@ -0,0 +1,110 @@
/* 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 * as HoverCard from '@radix-ui/react-hover-card';
import { useState } from 'react';
import { Localized } from '@fluent/react';
interface CheckoutCheckboxProps {
isRequired: boolean;
termsOfService: string;
privacyNotice: string;
notifyCheckboxChange: (isChecked: boolean) => void;
}
export function CheckoutCheckbox({
isRequired,
termsOfService,
privacyNotice,
notifyCheckboxChange,
}: CheckoutCheckboxProps) {
const [isChecked, setIsChecked] = useState(false);
const changeHandler = () => {
const newValue = !isChecked;
setIsChecked(newValue);
notifyCheckboxChange(newValue);
};
return (
<HoverCard.Root open={isRequired && !isChecked}>
<label className="flex gap-5 items-center mt-6">
<HoverCard.Trigger>
<input
type="checkbox"
name="confirm"
className="grow-0 shrink-0 basis-4 scale-150 cursor-pointer"
checked={isChecked}
onChange={changeHandler}
/>
</HoverCard.Trigger>
<Localized
id="next-payment-confirm-with-legal-links-static-3"
elems={{
termsOfServiceLink: (
<a
href={termsOfService}
target="_blank"
rel="noreferrer"
className="text-blue-500 underline"
>
Terms of Service
</a>
),
privacyNoticeLink: (
<a
href={privacyNotice}
target="_blank"
rel="noreferrer"
className="text-blue-500 underline"
>
Privacy Notice
</a>
),
}}
>
<span className="font-normal text-sm block">
I authorize Mozilla to charge my payment method for the amount
shown, according to{' '}
<a
href={termsOfService}
target="_blank"
rel="noreferrer"
className="text-blue-500 underline"
>
Terms of Service
</a>{' '}
and{' '}
<a
href={privacyNotice}
target="_blank"
rel="noreferrer"
className="text-blue-500 underline"
>
Privacy Notice
</a>
, until I cancel my subscription.
</span>
</Localized>
</label>
<HoverCard.Portal>
<HoverCard.Content
className="animate-slide-up z-20"
sideOffset={20}
align="start"
alignOffset={50}
arrowPadding={20}
>
<Localized id="next-payment-confirm-checkbox-error">
<div className="text-white text-sm bg-alert-red py-1.5 px-4">
You need to complete this before moving forward
</div>
</Localized>
<HoverCard.Arrow className="fill-alert-red" height={11} width={22} />
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
);
}

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

@ -0,0 +1,11 @@
## Checkout Form
next-new-user-submit = Subscribe Now
# Label for the Full Name input
payment-name-label = Name as it appears on your card
# Full Name input
next-payment-name =
.placeholder = Full Name

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

@ -16,8 +16,10 @@ 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';
import { Localized } from '@fluent/react';
interface CheckoutFormProps {
readOnly: boolean;
cart: {
id: string;
version: number;
@ -25,7 +27,7 @@ interface CheckoutFormProps {
};
}
export function CheckoutForm({ cart }: CheckoutFormProps) {
export function CheckoutForm({ readOnly, cart }: CheckoutFormProps) {
const router = useRouter();
const stripe = useStripe();
const elements = useElements();
@ -63,7 +65,7 @@ export function CheckoutForm({ cart }: CheckoutFormProps) {
) => {
event.preventDefault();
if (!stripe || !elements) {
if (!stripe || !elements || readOnly) {
// Stripe.js hasn't yet loaded.
// Make sure to disable form submission until Stripe.js has loaded.
return;
@ -131,20 +133,25 @@ export function CheckoutForm({ cart }: CheckoutFormProps) {
{!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
<Localized id="payment-name-label">
Name as it appears on your card
</Localized>
</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);
}}
/>
<Localized id="next-payment-name" attrs={{ placeholder: true }}>
<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"
readOnly={readOnly}
value={fullName}
onChange={(e) => {
setFullName(e.target.value);
setHasFullNameError(!e.target.value);
}}
/>
</Localized>
</Form.Control>
{hasFullNameError && (
<Form.Message asChild>
@ -155,23 +162,25 @@ export function CheckoutForm({ cart }: CheckoutFormProps) {
)}
</Form.Field>
)}
<PaymentElement />
<PaymentElement options={{ readOnly }} />
{!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>
<Localized id="next-new-user-submit">
<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>
</Localized>
</Form.Submit>
)}
</Form.Root>

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

@ -0,0 +1,3 @@
## Payment Section
next-new-user-card-title = Enter your card information

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

@ -0,0 +1,70 @@
/* 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 { useState } from 'react';
import { CheckoutCheckbox } from './CheckoutCheckbox';
import { Providers } from './Providers';
import { StripeWrapper } from './StripeWrapper';
import { Localized } from '@fluent/react';
interface PaymentFormProps {
cmsCommonContent: {
termsOfServiceUrl: string;
privacyNoticeUrl: string;
};
paymentsInfo: {
amount: number;
currency: string;
};
cart: {
id: string;
version: number;
email: string | null;
};
}
export function PaymentSection({
cmsCommonContent,
paymentsInfo,
cart,
}: PaymentFormProps) {
const [formEnabled, setFormEnabled] = useState(false);
const [showConsentError, setShowConsentError] = useState(false);
return (
<Providers>
<CheckoutCheckbox
isRequired={showConsentError}
termsOfService={cmsCommonContent.termsOfServiceUrl}
privacyNotice={cmsCommonContent.privacyNoticeUrl}
notifyCheckboxChange={(consentCheckbox) => {
setFormEnabled(consentCheckbox);
setShowConsentError(true);
}}
/>
<div
className={
formEnabled
? 'mt-14'
: 'mt-14 relative cursor-not-allowed focus:border-blue-400 focus:outline-none focus:shadow-input-blue-focus after:absolute after:content-[""] after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:opacity-50 after:z-10'
}
aria-disabled={!formEnabled}
onClick={() => setShowConsentError(true)}
>
<Localized id="next-new-user-card-title">
<p className="font-semibold text-grey-600 text-start mt-3 mb-6">
Enter your card information
</p>
</Localized>
<StripeWrapper
readOnly={!formEnabled}
amount={paymentsInfo.amount}
currency={paymentsInfo.currency}
cart={cart}
/>
</div>
</Providers>
);
}

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

@ -3,13 +3,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use client';
import { Localized } from '@fluent/react';
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import { CheckoutForm } from './CheckoutForm';
import { Providers } from './Providers';
interface StripeWrapperProps {
readOnly: boolean;
amount: number;
currency: string;
cart: {
@ -19,7 +18,12 @@ interface StripeWrapperProps {
};
}
export function StripeWrapper({ amount, currency, cart }: StripeWrapperProps) {
export function StripeWrapper({
readOnly,
amount,
currency,
cart,
}: StripeWrapperProps) {
// TODO - Load from config
const stripePromise = loadStripe(
'pk_test_VNpCidC0a2TJJB3wqXq7drhN00sF8r9mhs'
@ -59,11 +63,8 @@ export function StripeWrapper({ amount, currency, cart }: StripeWrapperProps) {
};
return (
<Providers>
<Localized id="next-pay-with-heading-card-only"></Localized>
<Elements stripe={stripePromise} options={options}>
<CheckoutForm cart={cart} />
</Elements>
</Providers>
<Elements stripe={stripePromise} options={options}>
<CheckoutForm readOnly={readOnly} cart={cart} />
</Elements>
);
}

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

@ -84,10 +84,15 @@ module.exports = {
'0%': { transform: 'rotate(0)' },
'100%': { transform: 'rotate(360deg)' },
},
'slide-up': {
'0%': { opacity: 0, transform: 'translateY(10px)' },
'100%': { opacity: 1, transform: 'translateY(0)' },
},
},
animation: {
'delayed-fade-in': 'fade-in 1s linear 5s forwards',
spin: 'rotate 0.8s linear infinite',
'slide-up': 'slide-up 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
},
listStyleType: {
circle: 'circle',
@ -270,7 +275,7 @@ module.exports = {
},
},
plugins: [
plugin(function ({ addUtilities }) {
plugin(function({ addUtilities }) {
const customUtilities = {
'.clip-auto': {
clip: 'auto',
@ -279,7 +284,7 @@ module.exports = {
addUtilities(customUtilities, ['responsive', 'hover', 'focus']);
}),
plugin(function ({ addComponents }) {
plugin(function({ addComponents }) {
const carets = {
'.caret-top': {
borderLeft: '0.75rem solid transparent',

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

@ -72,6 +72,7 @@
"@opentelemetry/sdk-trace-node": "^1.23.0",
"@opentelemetry/sdk-trace-web": "^1.23.0",
"@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-hover-card": "^1.0.7",
"@sentry/browser": "^7.113.0",
"@sentry/integrations": "^7.113.0",
"@sentry/node": "^7.113.0",

123
yarn.lock
Просмотреть файл

@ -14878,6 +14878,30 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-dismissable-layer@npm:1.0.5":
version: 1.0.5
resolution: "@radix-ui/react-dismissable-layer@npm:1.0.5"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-callback-ref": 1.0.1
"@radix-ui/react-use-escape-keydown": 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: e73cf4bd3763f4d55b1bea7486a9700384d7d94dc00b1d5a75e222b2f1e4f32bc667a206ca4ed3baaaf7424dce7a239afd0ba59a6f0d89c3462c4e6e8d029a04
languageName: node
linkType: hard
"@radix-ui/react-focus-guards@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-focus-guards@npm:1.0.1"
@ -14940,6 +14964,34 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-hover-card@npm:^1.0.7":
version: 1.0.7
resolution: "@radix-ui/react-hover-card@npm:1.0.7"
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-dismissable-layer": 1.0.5
"@radix-ui/react-popper": 1.1.3
"@radix-ui/react-portal": 1.0.4
"@radix-ui/react-presence": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-controllable-state": 1.0.1
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: 812c348d8331348774b0460cd9058fdb34e0a4e167cc3ab7350d60d0ac374c673e8159573919da299f58860b8eeb9d43c21ccb679cf6db70f5db0386359871ef
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"
@ -15005,6 +15057,35 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-popper@npm:1.1.3":
version: 1.1.3
resolution: "@radix-ui/react-popper@npm:1.1.3"
dependencies:
"@babel/runtime": ^7.13.10
"@floating-ui/react-dom": ^2.0.0
"@radix-ui/react-arrow": 1.0.3
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-callback-ref": 1.0.1
"@radix-ui/react-use-layout-effect": 1.0.1
"@radix-ui/react-use-rect": 1.0.1
"@radix-ui/react-use-size": 1.0.1
"@radix-ui/rect": 1.0.1
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: b18a15958623f9222b6ed3e24b9fbcc2ba67b8df5a5272412f261de1592b3f05002af1c8b94c065830c3c74267ce00cf6c1d70d4d507ec92ba639501f98aa348
languageName: node
linkType: hard
"@radix-ui/react-portal@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-portal@npm:1.0.3"
@ -15025,6 +15106,47 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-portal@npm:1.0.4":
version: 1.0.4
resolution: "@radix-ui/react-portal@npm:1.0.4"
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: c4cf35e2f26a89703189d0eef3ceeeb706ae0832e98e558730a5e929ca7c72c7cb510413a24eca94c7732f8d659a1e81942bec7b90540cb73ce9e4885d040b64
languageName: node
linkType: hard
"@radix-ui/react-presence@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-presence@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-use-layout-effect": 1.0.1
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: ed2ff9faf9e4257a4065034d3771459e5a91c2d840b2fcec94661761704dbcb65bcdd927d28177a2a129b3dab5664eb90a9b88309afe0257a9f8ba99338c0d95
languageName: node
linkType: hard
"@radix-ui/react-primitive@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-primitive@npm:1.0.3"
@ -38715,6 +38837,7 @@ fsevents@~2.1.1:
"@opentelemetry/sdk-trace-node": ^1.23.0
"@opentelemetry/sdk-trace-web": ^1.23.0
"@radix-ui/react-form": ^0.0.3
"@radix-ui/react-hover-card": ^1.0.7
"@sentry/browser": ^7.113.0
"@sentry/integrations": ^7.113.0
"@sentry/node": ^7.113.0