diff --git a/packages/fxa-payments-server/public/locales/en-US/main.ftl b/packages/fxa-payments-server/public/locales/en-US/main.ftl index 4a0e28a579..c259876704 100644 --- a/packages/fxa-payments-server/public/locales/en-US/main.ftl +++ b/packages/fxa-payments-server/public/locales/en-US/main.ftl @@ -29,6 +29,9 @@ brand-name-firefox-logo = { -brand-name-firefox } logo close-aria = .aria-label = Close modal +# Aria label for spinner image indicating data is loading +app-loading-spinner-aria-label-loading = Loading... + ## App error dialog general-error-heading = General application error diff --git a/packages/fxa-payments-server/src/components/PaymentForm/index.scss b/packages/fxa-payments-server/src/components/PaymentForm/index.scss index 277a9a45f1..5006fcd214 100644 --- a/packages/fxa-payments-server/src/components/PaymentForm/index.scss +++ b/packages/fxa-payments-server/src/components/PaymentForm/index.scss @@ -1,28 +1,6 @@ @import '../../../../fxa-content-server/app/styles/breakpoints'; form.payment { - .pricing-and-saved-payment { - display: flex; - font-size: 15px; - justify-content: space-between; - - .pricing { - display: flex; - align-items: center; - } - .c-card { - margin: 0; - } - } - - hr { - margin: 24px 0 0; - - @include respond-to('simpleSmall') { - margin-top: 16px; - } - } - .input-row { margin: 24px 0 0; @@ -60,21 +38,6 @@ form.payment { line-height: 45px; } } - - .button-row button { - margin-top: 32px; - } - - .lock::before { - background-image: url('./images/lock.svg'); - background-position: 0 2px; - background-repeat: no-repeat; - content: '\00a0'; - display: inline-block; - height: 18px; - margin: 0 8px; - width: 14px; - } } @include respond-to('simpleSmall') { @@ -82,12 +45,4 @@ form.payment { form.payment .input-row input::-moz-placeholder { line-height: 38px !important; } - - .main-content .payments-card { - width: 100%; - - .input-row-group { - padding-bottom: 8px; - } - } } diff --git a/packages/fxa-payments-server/src/components/PaymentForm/index.test.tsx b/packages/fxa-payments-server/src/components/PaymentForm/index.test.tsx index 630bb9d0a5..6e113afdcd 100644 --- a/packages/fxa-payments-server/src/components/PaymentForm/index.test.tsx +++ b/packages/fxa-payments-server/src/components/PaymentForm/index.test.tsx @@ -1,5 +1,11 @@ import React from 'react'; -import { render, cleanup, act, fireEvent } from '@testing-library/react'; +import { + render, + cleanup, + act, + fireEvent, + queryByTestId, +} from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import waitForExpect from 'wait-for-expect'; @@ -68,6 +74,7 @@ it('renders all expected default fields and elements', () => { expect(container.querySelector('button.cancel')).not.toBeInTheDocument(); expect(container.querySelector('span.spinner')).not.toBeInTheDocument(); + expect(queryByTestId(container, 'loading-spinner')).not.toBeInTheDocument(); expect(getByTestId('submit')).toHaveAttribute('disabled'); for (let testid of ['name', 'cardElement']) { @@ -186,7 +193,7 @@ it('renders a progress spinner when submitted, disables further submission (issu await waitForExpect(() => expect(onSubmit).toHaveBeenCalled()); - expect(queryByTestId('spinner-submit')).toBeInTheDocument(); + expect(queryByTestId('loading-spinner')).toBeInTheDocument(); expect(getByTestId('submit')).toHaveAttribute('disabled'); fireEvent.submit(getByTestId('paymentForm')); @@ -197,7 +204,7 @@ it('renders a progress spinner when submitted, disables further submission (issu it('renders a progress spinner when inProgress = true', () => { const { queryByTestId } = render(); - expect(queryByTestId('spinner-submit')).toBeInTheDocument(); + expect(queryByTestId('loading-spinner')).toBeInTheDocument(); }); it('renders a progress spinner when inProgress = true and onCancel supplied', () => { @@ -205,7 +212,7 @@ it('renders a progress spinner when inProgress = true and onCancel supplied', () const { queryByTestId } = render( ); - expect(queryByTestId('spinner-update')).toBeInTheDocument(); + expect(queryByTestId('loading-spinner')).toBeInTheDocument(); }); it('includes the cancel button when onCancel supplied', () => { diff --git a/packages/fxa-payments-server/src/components/PaymentForm/index.tsx b/packages/fxa-payments-server/src/components/PaymentForm/index.tsx index 3f81eb4a71..c44b57c45b 100644 --- a/packages/fxa-payments-server/src/components/PaymentForm/index.tsx +++ b/packages/fxa-payments-server/src/components/PaymentForm/index.tsx @@ -42,6 +42,10 @@ import { } from '../../lib/PaymentProvider'; import { PaymentProviderDetails } from '../PaymentProviderDetails'; import { PaymentConsentCheckbox } from '../PaymentConsentCheckbox'; +import LoadingSpinner, { + SpinnerType, +} from 'fxa-react/components/LoadingSpinner'; +import LockImage from './images/lock.svg'; export type StripePaymentSubmitResult = { stripe: Stripe; @@ -194,7 +198,7 @@ export const PaymentForm = ({ const paymentSource = plan && isExistingCustomer(customer) ? ( -
+
-
+

@@ -238,11 +242,11 @@ export const PaymentForm = ({ ); const buttons = onCancel ? ( -
+
) : ( -
+
{showProgressSpinner ? ( - -   - + ) : ( - - {submitButtonCopy} - +
+ + + {submitButtonCopy} + +
)}
@@ -308,14 +321,12 @@ export const PaymentForm = ({ {...{ onChange }} > {paymentSource} - {confirm && plan && ( <> -
+
)} - {buttons} ); diff --git a/packages/fxa-payments-server/src/lib/stripe.ts b/packages/fxa-payments-server/src/lib/stripe.ts index e35b8db49d..70dedab386 100644 --- a/packages/fxa-payments-server/src/lib/stripe.ts +++ b/packages/fxa-payments-server/src/lib/stripe.ts @@ -292,6 +292,19 @@ export const STRIPE_ELEMENT_STYLES = { 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif', fontSize: '16px', fontWeight: '500', + '::placeholder': { + color: '#767676', + fontSize: '16px', + fontWeight: '400', + height: '45px', + lineHeight: 'normal', + }, + '::-moz-placeholder': { + lineHeight: '45px', + }, + '&:-moz-ui-invalid': { + boxShadow: 'none', + }, }, invalid: { color: '#0c0c0d', diff --git a/packages/fxa-payments-server/src/styles/tailwind.css b/packages/fxa-payments-server/src/styles/tailwind.css index b8e951a45f..99c2f86e9f 100644 --- a/packages/fxa-payments-server/src/styles/tailwind.css +++ b/packages/fxa-payments-server/src/styles/tailwind.css @@ -125,3 +125,7 @@ @apply tablet:grid-cols-[minmax(min-content,500px)_minmax(20rem,1fr)] tablet:gap-x-8 tablet:mt-[80px] tablet:mb-auto tablet:mx-3; @apply desktop:grid-cols-[600px_1fr]; } + +.payment-button { + @apply cta-base-p flex-1 font-semibold rounded-md text-base; +} diff --git a/packages/fxa-react/components/LoadingSpinner/index.test.tsx b/packages/fxa-react/components/LoadingSpinner/index.test.tsx index d755208f60..c86138c6ea 100644 --- a/packages/fxa-react/components/LoadingSpinner/index.test.tsx +++ b/packages/fxa-react/components/LoadingSpinner/index.test.tsx @@ -1,10 +1,36 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { LoadingSpinner } from './index'; +import { LoadingSpinner, SpinnerType } from './index'; -it('renders as expected', () => { +it('renders defaults as expected', () => { render(); const result = screen.queryByTestId('loading-spinner'); + const spinnerType = screen.queryByTestId('loading-spinner-blue'); expect(result).toBeInTheDocument(); + expect(spinnerType).toBeInTheDocument(); +}); + +it('renders with custom spinner classNames', () => { + render(); + const result = screen.queryByTestId('loading-spinner'); + const spinnerImage = screen.queryByTestId('loading-spinner-blue'); + expect(result).toBeInTheDocument(); + expect(spinnerImage).toHaveClass('testclass'); +}); + +it('renders blue spinner as expected', () => { + render(); + const result = screen.queryByTestId('loading-spinner'); + const spinnerType = screen.queryByTestId('loading-spinner-blue'); + expect(result).toBeInTheDocument(); + expect(spinnerType).toBeInTheDocument(); +}); + +it('renders blue spinner as expected', () => { + render(); + const result = screen.queryByTestId('loading-spinner'); + const spinnerType = screen.queryByTestId('loading-spinner-white'); + expect(result).toBeInTheDocument(); + expect(spinnerType).toBeInTheDocument(); }); diff --git a/packages/fxa-react/components/LoadingSpinner/index.tsx b/packages/fxa-react/components/LoadingSpinner/index.tsx index d0245cbc0e..eb40122877 100644 --- a/packages/fxa-react/components/LoadingSpinner/index.tsx +++ b/packages/fxa-react/components/LoadingSpinner/index.tsx @@ -1,14 +1,57 @@ +import { useLocalization } from '@fluent/react'; import React from 'react'; import { ReactComponent as Spinner } from './spinner.svg'; +import { ReactComponent as SpinnerWhite } from './spinnerwhite.svg'; -export const LoadingSpinner = ({ className }: { className?: string }) => ( -
- -
-); +export enum SpinnerType { + Blue, + White, +} + +export const LoadingSpinner = ({ + className, + imageClassName = 'w-10 h-10 animate-spin', + spinnerType = SpinnerType.Blue, +}: { + className?: string; + imageClassName?: string; + spinnerType?: SpinnerType; +}) => { + const { l10n } = useLocalization(); + const loadingAriaLabel = l10n.getString( + 'app-loading-spinner-aria-label-loading', + null, + 'Loading...' + ); + let spinnerImage; + switch (spinnerType) { + case SpinnerType.White: + spinnerImage = ( + + ); + break; + case SpinnerType.Blue: + default: + spinnerImage = ( + + ); + } + + return ( +
+ {spinnerImage} +
+ ); +}; export default LoadingSpinner; diff --git a/packages/fxa-react/components/LoadingSpinner/spinnerwhite.svg b/packages/fxa-react/components/LoadingSpinner/spinnerwhite.svg new file mode 100644 index 0000000000..7af3f5ebbf --- /dev/null +++ b/packages/fxa-react/components/LoadingSpinner/spinnerwhite.svg @@ -0,0 +1 @@ + diff --git a/packages/fxa-react/styles/animations.css b/packages/fxa-react/styles/animations.css index 3cb6b77dfe..c3bc430d40 100644 --- a/packages/fxa-react/styles/animations.css +++ b/packages/fxa-react/styles/animations.css @@ -4,10 +4,10 @@ .transition-standard { @apply transition duration-150; +} - &:active { - @apply transition duration-150; - } +.transition-standard:active { + @apply transition duration-150; } @media (hover: hover) { diff --git a/packages/fxa-react/styles/ctas.css b/packages/fxa-react/styles/ctas.css index 0b65fd5252..b85f3f41d0 100644 --- a/packages/fxa-react/styles/ctas.css +++ b/packages/fxa-react/styles/ctas.css @@ -15,53 +15,53 @@ .cta-neutral { @apply bg-grey-10 border-grey-200; + } - &:active { - @apply border-grey-400 bg-grey-100 text-grey-400; - } + .cta-neutral:active { + @apply border-grey-400 bg-grey-100 text-grey-400; + } - &:focus, - &:focus-visible { - @apply bg-white outline outline-2 outline-offset-2 outline-blue-500; - } + .cta-neutral:focus, + .cta-neutral:focus-visible { + @apply bg-white outline outline-2 outline-offset-2 outline-blue-500; + } - &:disabled { - @apply bg-white text-grey-300 border-grey-200 cursor-not-allowed; - } + .cta-neutral:disabled { + @apply bg-white text-grey-300 border-grey-200 cursor-not-allowed; } .cta-primary { @apply bg-blue-500 border-blue-600 text-white; + } - &:active { - @apply bg-blue-800 border-blue-800; - } + .cta-primary:active { + @apply bg-blue-800 border-blue-800; + } - &:focus, - &:focus-visible { - @apply outline outline-2 outline-offset-2 outline-blue-500; - } + .cta-primary:focus, + .cta-primary:focus-visible { + @apply outline outline-2 outline-offset-2 outline-blue-500; + } - &:disabled { - @apply bg-blue-500/40 border-transparent text-white/50 cursor-not-allowed; - } + .cta-primary:disabled { + @apply bg-blue-500/40 border-transparent text-white/50 cursor-not-allowed; } .cta-caution { @apply bg-red-500 border-red-600 text-white; + } - &:active { - @apply bg-red-800 border-red-800; - } + .cta-caution:active { + @apply bg-red-800 border-red-800; + } - &:focus, - &:focus-visible { - @apply border-transparent outline-dotted outline-black; - } + .cta-caution:focus, + .cta-caution:focus-visible { + @apply border-transparent outline-dotted outline-black; + } - &:disabled { - @apply bg-red-500/40 border-transparent text-white/50 cursor-not-allowed; - } + .cta-caution:disabled { + @apply bg-red-500/40 border-transparent text-white/50 cursor-not-allowed; } .cta-base {