зеркало из https://github.com/mozilla/fxa.git
Merge pull request #17056 from mozilla/FXA-9708
fix(payments-next): Implement a11y improvements
This commit is contained in:
Коммит
18e5d172b0
|
@ -1,22 +1,23 @@
|
|||
/* 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 { revalidatePath } from 'next/cache';
|
||||
import { headers } from 'next/headers';
|
||||
import { PaymentSection } from '@fxa/payments/ui';
|
||||
import {
|
||||
app,
|
||||
getCartOrRedirectAction,
|
||||
SupportedPages,
|
||||
} from '@fxa/payments/ui/server';
|
||||
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 {
|
||||
getFakeCartData,
|
||||
getContentfulContent,
|
||||
} from 'apps/payments/next/app/_lib/apiClient';
|
||||
import { PaymentSection } from '@fxa/payments/ui';
|
||||
import { auth, signIn } from 'apps/payments/next/auth';
|
||||
import { PrimaryButton } from 'libs/payments/ui/src/lib/client/components/PrimaryButton';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { CheckoutParams } from '../layout';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
@ -62,13 +63,13 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
await signIn('fxa');
|
||||
}}
|
||||
>
|
||||
<p className="text-grey-400 text-sm mt-2 mb-4">
|
||||
<p className="text-grey-400 text-sm mt-2 pb-4 row-divider-grey-200">
|
||||
{l10n.getFragmentWithSource(
|
||||
'next-new-user-sign-in-link-2',
|
||||
{
|
||||
elems: {
|
||||
a: (
|
||||
<button className="underline text-grey-400 hover:text-grey-400">
|
||||
<button className="underline hover:text-grey-400">
|
||||
Sign in
|
||||
</button>
|
||||
),
|
||||
|
@ -76,7 +77,7 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
},
|
||||
<>
|
||||
Already have a Mozilla account?
|
||||
<button className="underline text-grey-400 hover:text-grey-400">
|
||||
<button className="underline hover:text-grey-400">
|
||||
Sign in
|
||||
</button>
|
||||
</>
|
||||
|
@ -84,8 +85,6 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
</p>
|
||||
</form>
|
||||
|
||||
<hr className="mx-auto w-full border-grey-200" />
|
||||
|
||||
<div className="p-6 text-center">
|
||||
{/**
|
||||
Temporary Content. This will be replaced in M3b by the Passwordless
|
||||
|
@ -122,7 +121,7 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
|
||||
{!session ? (
|
||||
<h2
|
||||
className="font-semibold text-grey-600 text-lg mt-14 mb-5"
|
||||
className="font-semibold text-grey-600 text-lg mt-10 mb-5"
|
||||
data-testid="header-prefix"
|
||||
>
|
||||
{l10n.getString(
|
||||
|
@ -132,7 +131,7 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
</h2>
|
||||
) : (
|
||||
<h2
|
||||
className="font-semibold text-grey-600 text-lg mt-14 mb-5"
|
||||
className="font-semibold text-grey-600 text-lg mt-10 mb-5"
|
||||
data-testid="header"
|
||||
>
|
||||
{l10n.getString(
|
||||
|
@ -141,7 +140,7 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
)}
|
||||
</h2>
|
||||
)}
|
||||
<h3 className="font-semibold my-3 text-grey-600 text-start">
|
||||
<h3 className="font-semibold text-grey-600 text-start">
|
||||
{l10n.getString(
|
||||
'next-payment-method-first-approve',
|
||||
`First you'll need to approve your subscription`
|
||||
|
|
|
@ -21,7 +21,7 @@ export default function RootLayout({
|
|||
<html lang="en">
|
||||
<body>
|
||||
<header
|
||||
className="bg-white fixed flex justify-between items-center shadow h-16 left-0 top-0 mx-auto my-0 px-4 py-0 w-full z-10 tablet:h-20"
|
||||
className="bg-white fixed flex justify-between items-center shadow h-16 left-0 top-0 mx-auto my-0 px-4 py-0 w-full z-40 tablet:h-20"
|
||||
role="banner"
|
||||
data-testid="header"
|
||||
>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// Use this file to export React client components (e.g. those with 'use client' directive) or other non-server utilities
|
||||
|
||||
export * from './lib/utils/helpers';
|
||||
export * from './lib/client/components/StripeWrapper';
|
||||
export * from './lib/client/components/CheckoutForm';
|
||||
export * from './lib/client/components/CheckoutCheckbox';
|
||||
export * from './lib/client/components/PaymentSection';
|
||||
export * from './lib/client/providers/Providers';
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
'use client';
|
||||
|
||||
import * as HoverCard from '@radix-ui/react-hover-card';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Localized } from '@fluent/react';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LinkExternal } from '@fxa/shared/react';
|
||||
|
||||
interface CheckoutCheckboxProps {
|
||||
isRequired: boolean;
|
||||
|
@ -38,84 +39,92 @@ export function CheckoutCheckbox({
|
|||
};
|
||||
|
||||
return (
|
||||
<HoverCard.Root open={isRequired && !isChecked}>
|
||||
<label className="flex gap-5 items-center mt-6">
|
||||
<HoverCard.Trigger>
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root open={isRequired && !isChecked}>
|
||||
<label className="flex gap-5 items-center my-6">
|
||||
<Tooltip.Trigger asChild>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="confirm"
|
||||
className="grow-0 shrink-0 basis-4 scale-150 cursor-pointer"
|
||||
checked={isChecked}
|
||||
onChange={changeHandler}
|
||||
required
|
||||
aria-describedby="checkboxError"
|
||||
aria-required
|
||||
/>
|
||||
</HoverCard.Trigger>
|
||||
</Tooltip.Trigger>
|
||||
{isClient && (
|
||||
<Localized
|
||||
id="next-payment-confirm-with-legal-links-static-3"
|
||||
elems={{
|
||||
termsOfServiceLink: (
|
||||
<a
|
||||
<LinkExternal
|
||||
href={termsOfService}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 underline"
|
||||
data-testid="link-external-terms-of-service"
|
||||
>
|
||||
Terms of Service
|
||||
</a>
|
||||
</LinkExternal>
|
||||
),
|
||||
privacyNoticeLink: (
|
||||
<a
|
||||
<LinkExternal
|
||||
href={privacyNotice}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 underline"
|
||||
data-testid="link-external-privacy-notice"
|
||||
>
|
||||
Privacy Notice
|
||||
</a>
|
||||
</LinkExternal>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<span className="font-normal text-sm block">
|
||||
<span className="font-normal text-sm leading-5 block">
|
||||
I authorize Mozilla to charge my payment method for the amount
|
||||
shown, according to{' '}
|
||||
<a
|
||||
<LinkExternal
|
||||
href={termsOfService}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 underline"
|
||||
data-testid="link-external-terms-of-service"
|
||||
>
|
||||
Terms of Service
|
||||
</a>{' '}
|
||||
</LinkExternal>{' '}
|
||||
and{' '}
|
||||
<a
|
||||
<LinkExternal
|
||||
href={privacyNotice}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 underline"
|
||||
data-testid="link-external-privacy-notice"
|
||||
>
|
||||
Privacy Notice
|
||||
</a>
|
||||
</LinkExternal>
|
||||
, until I cancel my subscription.
|
||||
</span>
|
||||
</Localized>
|
||||
)}
|
||||
</label>
|
||||
<HoverCard.Portal>
|
||||
<HoverCard.Content
|
||||
className="animate-slide-up z-20"
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
id="checkboxError"
|
||||
className="animate-slide-down z-20"
|
||||
side="bottom"
|
||||
sideOffset={20}
|
||||
align="start"
|
||||
alignOffset={50}
|
||||
arrowPadding={20}
|
||||
role="alert"
|
||||
>
|
||||
<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>
|
||||
<Tooltip.Arrow
|
||||
className="fill-alert-red"
|
||||
height={11}
|
||||
width={22}
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</label>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
## Checkout Form
|
||||
|
||||
next-new-user-submit = Subscribe Now
|
||||
next-payment-validate-name-error = Please enter your full name
|
||||
|
||||
# Label for the Full Name input
|
||||
payment-name-label = Name as it appears on your card
|
||||
payment-name-placeholder = Full Name
|
||||
|
||||
|
|
|
@ -3,24 +3,28 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
'use client';
|
||||
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
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';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import LockImage from '@fxa/shared/assets/images/lock.svg';
|
||||
import { CheckoutCheckbox } from './CheckoutCheckbox';
|
||||
import { PrimaryButton } from './PrimaryButton';
|
||||
import { checkoutCartWithStripe } from '../../actions/checkoutCartWithStripe';
|
||||
import { handleStripeErrorAction } from '../../actions/handleStripeError';
|
||||
|
||||
interface CheckoutFormProps {
|
||||
readOnly: boolean;
|
||||
cmsCommonContent: {
|
||||
termsOfServiceUrl: string;
|
||||
privacyNoticeUrl: string;
|
||||
};
|
||||
cart: {
|
||||
id: string;
|
||||
version: number;
|
||||
|
@ -29,11 +33,18 @@ interface CheckoutFormProps {
|
|||
locale: string;
|
||||
}
|
||||
|
||||
export function CheckoutForm({ readOnly, cart, locale }: CheckoutFormProps) {
|
||||
const router = useRouter();
|
||||
const stripe = useStripe();
|
||||
export function CheckoutForm({
|
||||
cmsCommonContent,
|
||||
cart,
|
||||
locale,
|
||||
}: CheckoutFormProps) {
|
||||
const { l10n } = useLocalization();
|
||||
const elements = useElements();
|
||||
const router = useRouter();
|
||||
const stripe = useStripe();
|
||||
|
||||
const [formEnabled, setFormEnabled] = useState(false);
|
||||
const [showConsentError, setShowConsentError] = useState(false);
|
||||
const [isPaymentElementLoading, setIsPaymentElementLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [stripeFieldsComplete, setStripeFieldsComplete] = useState(false);
|
||||
|
@ -68,7 +79,7 @@ export function CheckoutForm({ readOnly, cart, locale }: CheckoutFormProps) {
|
|||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!stripe || !elements || readOnly) {
|
||||
if (!stripe || !elements) {
|
||||
// Stripe.js hasn't yet loaded.
|
||||
// Make sure to disable form submission until Stripe.js has loaded.
|
||||
return;
|
||||
|
@ -135,10 +146,36 @@ export function CheckoutForm({ readOnly, cart, locale }: CheckoutFormProps) {
|
|||
const nonStripeFieldsComplete = !!fullName;
|
||||
|
||||
return (
|
||||
<Form.Root className="flex flex-col gap-4" onSubmit={submitHandler}>
|
||||
<Form.Root onSubmit={submitHandler}>
|
||||
<CheckoutCheckbox
|
||||
isRequired={showConsentError}
|
||||
termsOfService={cmsCommonContent.termsOfServiceUrl}
|
||||
privacyNotice={cmsCommonContent.privacyNoticeUrl}
|
||||
notifyCheckboxChange={(consentCheckbox) => {
|
||||
setFormEnabled(consentCheckbox);
|
||||
setShowConsentError(true);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
formEnabled
|
||||
? 'mt-10'
|
||||
: 'mt-10 cursor-not-allowed relative 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'
|
||||
}
|
||||
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 && (
|
||||
<Form.Field name="name" serverInvalid={hasFullNameError}>
|
||||
<Form.Label className="font-medium text-sm text-grey-400 block mb-1 text-start">
|
||||
<Form.Field
|
||||
name="name"
|
||||
serverInvalid={hasFullNameError}
|
||||
className="my-6"
|
||||
>
|
||||
<Form.Label className="text-grey-400 block mb-1 text-start">
|
||||
<Localized id="payment-name-label">
|
||||
Name as it appears on your card
|
||||
</Localized>
|
||||
|
@ -153,19 +190,23 @@ export function CheckoutForm({ readOnly, cart, locale }: CheckoutFormProps) {
|
|||
{},
|
||||
'Full Name'
|
||||
)}
|
||||
readOnly={readOnly}
|
||||
readOnly={!formEnabled}
|
||||
tabIndex={formEnabled ? 0 : -1}
|
||||
value={fullName}
|
||||
onChange={(e) => {
|
||||
setFullName(e.target.value);
|
||||
setHasFullNameError(!e.target.value);
|
||||
}}
|
||||
aria-required
|
||||
/>
|
||||
</Form.Control>
|
||||
{hasFullNameError && (
|
||||
<Form.Message asChild>
|
||||
<p className="text-sm mt-1 text-alert-red">
|
||||
<Localized id="next-payment-validate-name-error">
|
||||
<p className="mt-1 text-alert-red" role="alert">
|
||||
Please enter your name
|
||||
</p>
|
||||
</Localized>
|
||||
</Form.Message>
|
||||
)}
|
||||
</Form.Field>
|
||||
|
@ -178,28 +219,23 @@ export function CheckoutForm({ readOnly, cart, locale }: CheckoutFormProps) {
|
|||
radios: false,
|
||||
spacedAccordionItems: true,
|
||||
},
|
||||
readOnly,
|
||||
readOnly: !formEnabled,
|
||||
}}
|
||||
/>
|
||||
{!isPaymentElementLoading && (
|
||||
<Form.Submit asChild>
|
||||
<Localized id="next-new-user-submit">
|
||||
<PrimaryButton
|
||||
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
|
||||
<Image src={LockImage} className="h-4 w-4 mx-3" alt="" />
|
||||
<Localized id="next-new-user-submit">Subscribe Now</Localized>
|
||||
</PrimaryButton>
|
||||
</Localized>
|
||||
</Form.Submit>
|
||||
)}
|
||||
</div>
|
||||
</Form.Root>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
/* 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 { CheckoutForm } from './CheckoutForm';
|
||||
import { StripeWrapper } from './StripeWrapper';
|
||||
import { Localized } from '@fluent/react';
|
||||
|
||||
interface PaymentFormProps {
|
||||
cmsCommonContent: {
|
||||
|
@ -31,42 +30,16 @@ export function PaymentSection({
|
|||
cart,
|
||||
locale,
|
||||
}: PaymentFormProps) {
|
||||
const [formEnabled, setFormEnabled] = useState(false);
|
||||
const [showConsentError, setShowConsentError] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<h3 className="font-semibold text-grey-600 text-start mt-3 mb-6">
|
||||
Enter your card information
|
||||
</h3>
|
||||
</Localized>
|
||||
<StripeWrapper
|
||||
readOnly={!formEnabled}
|
||||
amount={paymentsInfo.amount}
|
||||
currency={paymentsInfo.currency}
|
||||
>
|
||||
<CheckoutForm
|
||||
cmsCommonContent={cmsCommonContent}
|
||||
cart={cart}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</StripeWrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export function PrimaryButton({ children, ...props }: PrimaryButtonProps) {
|
|||
return (
|
||||
<button
|
||||
{...props}
|
||||
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 ${props.className}`}
|
||||
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 z-10 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 ${props.className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
|
|
@ -3,30 +3,21 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
'use client';
|
||||
|
||||
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { CheckoutForm } from './CheckoutForm';
|
||||
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
|
||||
import { useContext, useState } from 'react';
|
||||
import { ConfigContext } from '../providers/ConfigProvider';
|
||||
|
||||
interface StripeWrapperProps {
|
||||
readOnly: boolean;
|
||||
amount: number;
|
||||
currency: string;
|
||||
cart: {
|
||||
id: string;
|
||||
version: number;
|
||||
email: string | null;
|
||||
};
|
||||
locale: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function StripeWrapper({
|
||||
readOnly,
|
||||
amount,
|
||||
currency,
|
||||
cart,
|
||||
locale,
|
||||
children,
|
||||
}: StripeWrapperProps) {
|
||||
const config = useContext(ConfigContext);
|
||||
const [stripePromise] = useState(() => loadStripe(config.stripePublicApiKey));
|
||||
|
@ -42,6 +33,7 @@ export function StripeWrapper({
|
|||
fontFamily:
|
||||
'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
||||
fontSizeBase: '16px',
|
||||
fontSizeSm: '16px',
|
||||
fontWeightNormal: '500',
|
||||
colorDanger: '#D70022',
|
||||
},
|
||||
|
@ -67,7 +59,7 @@ export function StripeWrapper({
|
|||
|
||||
return (
|
||||
<Elements stripe={stripePromise} options={options}>
|
||||
<CheckoutForm readOnly={readOnly} cart={cart} locale={locale} />
|
||||
{children}
|
||||
</Elements>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
* 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 { Invoice } from '@fxa/payments/cart';
|
||||
import Image from 'next/image';
|
||||
import { formatPlanPricing } from '../utils/helpers';
|
||||
import { Invoice } from '@fxa/payments/cart';
|
||||
import { LocalizerRsc } from '@fxa/shared/l10n/server';
|
||||
import { formatPlanPricing } from '../utils/helpers';
|
||||
|
||||
type ListLabelItemProps = {
|
||||
labelLocalizationId: string;
|
||||
|
@ -114,7 +114,7 @@ export async function PurchaseDetails(props: PurchaseDetailsProps) {
|
|||
))}
|
||||
</ul>
|
||||
|
||||
<ul className="row-divider-grey-200 py-6">
|
||||
<ul className="pt-6">
|
||||
{!!listAmount && (
|
||||
<ListLabelItem
|
||||
{...{
|
||||
|
@ -165,9 +165,8 @@ export async function PurchaseDetails(props: PurchaseDetailsProps) {
|
|||
key={taxRate.title}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="plan-details-item pt-4 pb-6 font-semibold">
|
||||
<li className="plan-details-item mt-6 pt-4 pb-6 font-semibold border-t border-grey-200">
|
||||
<h3 className="text-base">
|
||||
{l10n.getString('next-plan-details-total-label', 'Total')}
|
||||
</h3>
|
||||
|
@ -184,7 +183,8 @@ export async function PurchaseDetails(props: PurchaseDetailsProps) {
|
|||
formatPlanPricing(totalAmount, currency, interval)
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{/* TODO - Add InfoBox as part of Coupon Form - Consider adding as child component */}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Image from 'next/image';
|
||||
import checkLogo from '@fxa/shared/assets/images/check.svg';
|
||||
import { CartState } from '@fxa/shared/db/mysql/account';
|
||||
import { LocalizerRsc } from '@fxa/shared/l10n/server';
|
||||
import checkLogo from '@fxa/shared/assets/images/check.svg';
|
||||
|
||||
const getComponentTitle = (cartState: CartState) => {
|
||||
switch (cartState) {
|
||||
|
@ -53,7 +53,10 @@ export async function SubscriptionTitle({
|
|||
const displaySubtitle = subheaders.includes(cartState);
|
||||
|
||||
return (
|
||||
<header className="page-title-container">
|
||||
<header
|
||||
className="page-title-container"
|
||||
aria-label={l10n.getString(componentTitle.titleFtl, componentTitle.title)}
|
||||
>
|
||||
<h1 className="page-header">
|
||||
{l10n.getString(componentTitle.titleFtl, componentTitle.title)}
|
||||
</h1>
|
||||
|
|
|
@ -84,15 +84,15 @@ module.exports = {
|
|||
'0%': { transform: 'rotate(0)' },
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
'slide-up': {
|
||||
'0%': { opacity: 0, transform: 'translateY(10px)' },
|
||||
'slide-down': {
|
||||
'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)',
|
||||
'slide-down': 'slide-down 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
},
|
||||
listStyleType: {
|
||||
circle: 'circle',
|
||||
|
@ -275,7 +275,7 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
plugins: [
|
||||
plugin(function({ addUtilities }) {
|
||||
plugin(function ({ addUtilities }) {
|
||||
const customUtilities = {
|
||||
'.clip-auto': {
|
||||
clip: 'auto',
|
||||
|
@ -284,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',
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": ["../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
# shared-react
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Building
|
||||
|
||||
Run `nx build shared-react` to build the library.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test shared-react` to execute the unit tests via [Jest](https://jestjs.io).
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'shared-react',
|
||||
preset: '../../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../../../coverage/libs/shared/react',
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "@fxa/shared/react",
|
||||
"version": "0.0.1"
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "shared-react",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shared/react/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/shared/react",
|
||||
"tsConfig": "libs/shared/react/tsconfig.lib.json",
|
||||
"packageJson": "libs/shared/react/package.json",
|
||||
"main": "libs/shared/react/src/index.ts",
|
||||
"assets": ["libs/shared/react/*.md"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/shared/react/jest.config.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
## Strings shared between multiple FxA products for external link
|
||||
|
||||
# Message for screen readers, to announce that external link will open in new window
|
||||
next-link-sr-new-window = Opens in new window
|
|
@ -0,0 +1,49 @@
|
|||
/* 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 { Localized } from '@fluent/react';
|
||||
|
||||
interface LinkExternalProps {
|
||||
className?: string;
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
'data-testid'?: string;
|
||||
rel?: 'noopener noreferrer' | 'author';
|
||||
tabIndex?: number;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const LinkExternal = ({
|
||||
className,
|
||||
href,
|
||||
children,
|
||||
title,
|
||||
'data-testid': testid = 'link-external',
|
||||
rel = 'noopener noreferrer',
|
||||
tabIndex,
|
||||
onClick,
|
||||
}: LinkExternalProps) => (
|
||||
<a
|
||||
data-testid={testid}
|
||||
target="_blank"
|
||||
{...{
|
||||
className,
|
||||
href,
|
||||
title,
|
||||
rel,
|
||||
tabIndex,
|
||||
onClick,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<Localized id="next-link-sr-new-window">
|
||||
<span className="sr-only">Opens in new window</span>
|
||||
</Localized>
|
||||
</a>
|
||||
);
|
||||
|
||||
export default LinkExternal;
|
|
@ -0,0 +1,5 @@
|
|||
/* 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/. */
|
||||
|
||||
export * from './components/LinkExternal';
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
|
@ -72,7 +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",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@sentry/browser": "^7.113.0",
|
||||
"@sentry/integrations": "^7.113.0",
|
||||
"@sentry/node": "^7.113.0",
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
"@fxa/shared/notifier": ["libs/shared/notifier/src/index.ts"],
|
||||
"@fxa/shared/otp": ["libs/shared/otp/src/index.ts"],
|
||||
"@fxa/shared/pem-jwk": ["libs/shared/pem-jwk/src/index.ts"],
|
||||
"@fxa/shared/react": ["libs/shared/react/src/index.ts"],
|
||||
"@fxa/shared/sentry": ["libs/shared/sentry/src/index.ts"],
|
||||
"@fxa/vendored/common-password-list": [
|
||||
"libs/vendored/common-password-list/src/index.ts"
|
||||
|
|
61
yarn.lock
61
yarn.lock
|
@ -14964,34 +14964,6 @@ __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"
|
||||
|
@ -15345,6 +15317,37 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-tooltip@npm:^1.0.7":
|
||||
version: 1.0.7
|
||||
resolution: "@radix-ui/react-tooltip@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-id": 1.0.1
|
||||
"@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-slot": 1.0.2
|
||||
"@radix-ui/react-use-controllable-state": 1.0.1
|
||||
"@radix-ui/react-visually-hidden": 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: 894d448c69a3e4d7626759f9f6c7997018fe8ef9cde098393bd83e10743d493dfd284eef041e46accc45486d5a5cd5f76d97f56afbdace7aed6e0cb14007bf15
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-callback-ref@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1"
|
||||
|
@ -38801,7 +38804,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
|
||||
"@radix-ui/react-tooltip": ^1.0.7
|
||||
"@sentry/browser": ^7.113.0
|
||||
"@sentry/integrations": ^7.113.0
|
||||
"@sentry/node": ^7.113.0
|
||||
|
|
Загрузка…
Ссылка в новой задаче