зеркало из https://github.com/mozilla/fxa.git
feat(next): reorg checkout pages
Because: * Reorganize checkout pages to include intent and cartId in path, and allow for landing page. This commit: * Reorganizes checkout pages * Add checkout landing page * Add getLocaleFromRequest and update l10n locale logic Closes #FXA-7804
This commit is contained in:
Родитель
0b55f8816c
Коммит
275ecdb0b8
|
@ -3,41 +3,47 @@ import Image from 'next/image';
|
|||
import Link from 'next/link';
|
||||
|
||||
import { PurchaseDetails, TermsAndPrivacy } from '@fxa/payments/ui/server';
|
||||
import { getBundle } from '@fxa/shared/l10n';
|
||||
import { getBundle, getLocaleFromRequest } from '@fxa/shared/l10n';
|
||||
|
||||
import { getCartData, getContentfulContent } from '../../_lib/apiClient';
|
||||
import checkLogo from '../../../images/check.svg';
|
||||
import errorIcon from '../../../images/error.svg';
|
||||
import { getCartData, getContentfulContent } from '../../../../_lib/apiClient';
|
||||
import checkLogo from '../../../../../images/check.svg';
|
||||
import errorIcon from '../../../../../images/error.svg';
|
||||
import { CheckoutSearchParams } from '../../layout';
|
||||
// import { app } from '../../_nestapp/app';
|
||||
|
||||
interface CheckoutParams {
|
||||
offeringId: string;
|
||||
}
|
||||
|
||||
// forces dynamic rendering
|
||||
// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function Error({ params }: { params: CheckoutParams }) {
|
||||
// TODO - Fetch Cart ID from cookie
|
||||
// https://nextjs.org/docs/app/api-reference/functions/cookies
|
||||
const cartId = 'cart-uuid';
|
||||
// TODO - Fetch locale from params
|
||||
// Possible solution could be as link below
|
||||
// https://nextjs.org/docs/app/building-your-application/routing/internationalization
|
||||
const locale = 'en-US';
|
||||
// Temporary code for demo purposes only - Replaced as part of FXA-8822
|
||||
const demoSupportedLanguages = ['en-US', 'fr-FR', 'es-ES', 'de-DE'];
|
||||
|
||||
interface CheckoutParams {
|
||||
offeringId: string;
|
||||
cartId: string;
|
||||
}
|
||||
|
||||
export default async function CheckoutError({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: CheckoutParams;
|
||||
searchParams: CheckoutSearchParams;
|
||||
}) {
|
||||
const headersList = headers();
|
||||
const locale = getLocaleFromRequest(
|
||||
searchParams,
|
||||
headersList.get('accept-language'),
|
||||
demoSupportedLanguages
|
||||
);
|
||||
|
||||
const contentfulData = getContentfulContent(params.offeringId, locale);
|
||||
const cartData = getCartData(cartId);
|
||||
const cartData = getCartData(params.cartId);
|
||||
const [contentful, cart] = await Promise.all([contentfulData, cartData]);
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// const cartService = await app.getCartService();
|
||||
|
||||
const languages = headers()
|
||||
.get('Accept-Language')
|
||||
?.split(',')
|
||||
.map((language) => language.split(';')[0]);
|
||||
const l10n = await getBundle(languages);
|
||||
const l10n = await getBundle([locale]);
|
||||
|
||||
const getErrorReason = (reason: string) => {
|
||||
switch (reason) {
|
||||
|
@ -80,6 +86,7 @@ export default async function Error({ params }: { params: CheckoutParams }) {
|
|||
aria-label="Purchase details"
|
||||
>
|
||||
<PurchaseDetails
|
||||
locale={locale}
|
||||
interval={cart.interval}
|
||||
invoice={cart.nextInvoice}
|
||||
purchaseDetails={contentful.purchaseDetails}
|
||||
|
@ -110,6 +117,7 @@ export default async function Error({ params }: { params: CheckoutParams }) {
|
|||
</section>
|
||||
|
||||
<TermsAndPrivacy
|
||||
locale={locale}
|
||||
{...cart}
|
||||
{...contentful.commonContent}
|
||||
{...contentful.purchaseDetails}
|
|
@ -1,26 +1,37 @@
|
|||
import { PurchaseDetails, TermsAndPrivacy } from '@fxa/payments/ui/server';
|
||||
|
||||
import { getCartData, getContentfulContent } from '../../_lib/apiClient';
|
||||
import { app } from '../../_nestapp/app';
|
||||
import { getCartData, getContentfulContent } from '../../../_lib/apiClient';
|
||||
import { app } from '../../../_nestapp/app';
|
||||
import { headers } from 'next/headers';
|
||||
import { getLocaleFromRequest } from '@fxa/shared/l10n';
|
||||
import { CheckoutSearchParams } from '../layout';
|
||||
import { auth, signIn, signOut } from 'apps/payments/next/auth';
|
||||
|
||||
interface CheckoutParams {
|
||||
offeringId: string;
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function Index({ params }: { params: CheckoutParams }) {
|
||||
// TODO - Fetch Cart ID from cookie
|
||||
// https://nextjs.org/docs/app/api-reference/functions/cookies
|
||||
const cartId = 'cart-uuid';
|
||||
// TODO - Fetch locale from params
|
||||
// Possible solution could be as link below
|
||||
// https://nextjs.org/docs/app/building-your-application/routing/internationalization
|
||||
const locale = 'en-US';
|
||||
interface CheckoutParams {
|
||||
offeringId: string;
|
||||
cartId: string;
|
||||
}
|
||||
|
||||
// Temporary code for demo purposes only - Replaced as part of FXA-8822
|
||||
const demoSupportedLanguages = ['en-US', 'fr-FR', 'es-ES', 'de-DE'];
|
||||
|
||||
export default async function Checkout({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: CheckoutParams;
|
||||
searchParams: CheckoutSearchParams;
|
||||
}) {
|
||||
const headersList = headers();
|
||||
const locale = getLocaleFromRequest(
|
||||
searchParams,
|
||||
headersList.get('accept-language'),
|
||||
demoSupportedLanguages
|
||||
);
|
||||
|
||||
const contentfulData = getContentfulContent(params.offeringId, locale);
|
||||
const cartData = getCartData(cartId);
|
||||
const cartData = getCartData(params.cartId);
|
||||
const [contentful, cart] = await Promise.all([contentfulData, cartData]);
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const cartService = await app.getCartService();
|
||||
|
@ -35,6 +46,7 @@ export default async function Index({ params }: { params: CheckoutParams }) {
|
|||
<section className="payment-panel" aria-label="Purchase details">
|
||||
<PurchaseDetails
|
||||
interval={cart.interval}
|
||||
locale={locale}
|
||||
invoice={cart.nextInvoice}
|
||||
purchaseDetails={contentful.purchaseDetails}
|
||||
/>
|
||||
|
@ -90,6 +102,7 @@ export default async function Index({ params }: { params: CheckoutParams }) {
|
|||
</section>
|
||||
|
||||
<TermsAndPrivacy
|
||||
locale={locale}
|
||||
{...cart}
|
||||
{...contentful.commonContent}
|
||||
{...contentful.purchaseDetails}
|
|
@ -1,25 +1,26 @@
|
|||
import { PurchaseDetails, TermsAndPrivacy } from '@fxa/payments/ui/server';
|
||||
import { getCartData, getContentfulContent } from '../../_lib/apiClient';
|
||||
import { getCartData, getContentfulContent } from '../../../../_lib/apiClient';
|
||||
import { headers } from 'next/headers';
|
||||
import Image from 'next/image';
|
||||
import checkLogo from '../../../images/check.svg';
|
||||
import circledConfirm from '../../../images/circled-confirm.svg';
|
||||
import { formatPlanPricing } from '../../../../../../libs/payments/ui/src/lib/utils/helpers';
|
||||
import checkLogo from '../../../../../images/check.svg';
|
||||
import circledConfirm from '../../../../../images/circled-confirm.svg';
|
||||
import { formatPlanPricing } from '../../../../../../../../libs/payments/ui/src/lib/utils/helpers';
|
||||
import {
|
||||
getBundle,
|
||||
getFormattedMsg,
|
||||
getLocaleFromRequest,
|
||||
getLocalizedCurrency,
|
||||
getLocalizedDate,
|
||||
getLocalizedDateString,
|
||||
} from '@fxa/shared/l10n';
|
||||
import { CheckoutSearchParams } from '../../layout';
|
||||
// import { app } from '../../_nestapp/app';
|
||||
|
||||
interface CheckoutParams {
|
||||
offeringId: string;
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// Temporary code for demo purposes only - Replaced as part of FXA-8822
|
||||
const demoSupportedLanguages = ['en-US', 'fr-FR', 'es-ES', 'de-DE'];
|
||||
|
||||
type ConfirmationDetailProps = {
|
||||
title: string;
|
||||
detail1: string | Promise<string>;
|
||||
|
@ -42,17 +43,27 @@ const ConfirmationDetail = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: CheckoutParams }) {
|
||||
// TODO - Fetch Cart ID from cookie
|
||||
// https://nextjs.org/docs/app/api-reference/functions/cookies
|
||||
const cartId = 'cart-uuid';
|
||||
// TODO - Fetch locale from params
|
||||
// Possible solution could be as link below
|
||||
// https://nextjs.org/docs/app/building-your-application/routing/internationalization
|
||||
const locale = 'en-US';
|
||||
interface CheckoutParams {
|
||||
offeringId: string;
|
||||
cartId: string;
|
||||
}
|
||||
|
||||
export default async function CheckoutSuccess({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: CheckoutParams;
|
||||
searchParams: CheckoutSearchParams;
|
||||
}) {
|
||||
const headersList = headers();
|
||||
const locale = getLocaleFromRequest(
|
||||
searchParams,
|
||||
headersList.get('accept-language'),
|
||||
demoSupportedLanguages
|
||||
);
|
||||
|
||||
const contentfulData = getContentfulContent(params.offeringId, locale);
|
||||
const cartData = getCartData(cartId);
|
||||
const cartData = getCartData(params.cartId);
|
||||
const [contentful, cart] = await Promise.all([contentfulData, cartData]);
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// const cartService = await app.getCartService();
|
||||
|
@ -63,11 +74,7 @@ export default async function Page({ params }: { params: CheckoutParams }) {
|
|||
cart.interval
|
||||
);
|
||||
|
||||
const languages = headers()
|
||||
.get('Accept-Language')
|
||||
?.split(',')
|
||||
.map((language) => language.split(';')[0]);
|
||||
const l10n = await getBundle(languages);
|
||||
const l10n = await getBundle([locale]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -87,6 +94,7 @@ export default async function Page({ params }: { params: CheckoutParams }) {
|
|||
|
||||
<section className="payment-panel" aria-label="Purchase details">
|
||||
<PurchaseDetails
|
||||
locale={locale}
|
||||
interval={cart.interval}
|
||||
invoice={cart.nextInvoice}
|
||||
purchaseDetails={contentful.purchaseDetails}
|
||||
|
@ -182,6 +190,7 @@ export default async function Page({ params }: { params: CheckoutParams }) {
|
|||
</section>
|
||||
|
||||
<TermsAndPrivacy
|
||||
locale={locale}
|
||||
{...cart}
|
||||
{...contentful.commonContent}
|
||||
{...contentful.purchaseDetails}
|
|
@ -0,0 +1,20 @@
|
|||
// TODO - Replace these placeholders as part of FXA-8227
|
||||
export const metadata = {
|
||||
title: 'Mozilla accounts',
|
||||
description: 'Mozilla accounts',
|
||||
};
|
||||
|
||||
export interface CheckoutSearchParams {
|
||||
interval?: string;
|
||||
promotion_code?: string;
|
||||
experiment?: string;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <>{children}</>;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default async function CheckoutNew() {
|
||||
return <></>;
|
||||
}
|
|
@ -50,7 +50,7 @@ export async function fetchFromContentful() {
|
|||
|
||||
export async function fetchCartData(cartId: string) {
|
||||
return {
|
||||
id: '',
|
||||
id: 'b6115e72-2a3f-4de8-a58a-e231bfeea85d',
|
||||
// state: CartState.START,
|
||||
state: CartState.FAIL,
|
||||
// errorReasonId: CartErrorReasonId.BASIC_ERROR,
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function Index() {
|
|||
<h2>123Done - Monthly</h2>
|
||||
<Link
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
href="/123done/checkout?interval=monthly"
|
||||
href="/123done/checkout/b6115e72-2a3f-4de8-a58a-e231bfeea85d?interval=monthly"
|
||||
>
|
||||
Redirect
|
||||
</Link>
|
||||
|
@ -23,7 +23,7 @@ export default function Index() {
|
|||
<h2>123Done - Yearly</h2>
|
||||
<Link
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
href="/123done/checkout?interval=yearly"
|
||||
href="/123done/checkout/b6115e72-2a3f-4de8-a58a-e231bfeea85d?interval=yearly"
|
||||
>
|
||||
Redirect
|
||||
</Link>
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
getLocalizedCurrencyString,
|
||||
} from '@fxa/shared/l10n';
|
||||
import { FluentBundle } from '@fluent/bundle';
|
||||
import { headers } from 'next/headers';
|
||||
import Image from 'next/image';
|
||||
import { formatPlanPricing } from '../utils/helpers';
|
||||
import '../../styles/index.css';
|
||||
|
@ -57,6 +56,7 @@ export const ListLabelItem = ({
|
|||
|
||||
type PurchaseDetailsProps = {
|
||||
interval: string;
|
||||
locale: string;
|
||||
invoice: Invoice;
|
||||
purchaseDetails: {
|
||||
details: string[];
|
||||
|
@ -75,20 +75,12 @@ export async function PurchaseDetails(props: PurchaseDetailsProps) {
|
|||
(taxAmount) => !taxAmount.inclusive
|
||||
);
|
||||
|
||||
// TODO - Temporary
|
||||
// Identify an approach to ensure we don't have to perform this logic
|
||||
// in every component/page that requires localization.
|
||||
const languages = headers()
|
||||
.get('Accept-Language')
|
||||
?.split(',')
|
||||
.map((language) => language.split(';')[0]);
|
||||
|
||||
// TODO
|
||||
// Move to instantiation on start up. Ideally getBundle's, generateBundle, is only called once at startup,
|
||||
// and then that instance is used for all requests.
|
||||
// Approach 1 (Experimental): https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
|
||||
// Approach 2 (Node global): https://github.com/vercel/next.js/blob/canary/examples/with-knex/knex/index.js#L13
|
||||
const l10n = await getBundle(languages);
|
||||
const l10n = await getBundle([props.locale]);
|
||||
|
||||
return (
|
||||
<div className="component-card text-sm px-4 rounded-t-none tablet:rounded-t-lg">
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { FluentBundle } from '@fluent/bundle';
|
||||
import { getBundle } from '@fxa/shared/l10n';
|
||||
import { headers } from 'next/headers';
|
||||
import {
|
||||
GenericTermItem,
|
||||
GenericTermsListItem,
|
||||
|
@ -59,6 +58,7 @@ function GenericTerms({
|
|||
}
|
||||
|
||||
export interface TermsAndPrivacyProps {
|
||||
locale: string;
|
||||
paymentProvider?: PaymentProvider;
|
||||
productName: string;
|
||||
termsOfServiceUrl: string;
|
||||
|
@ -68,6 +68,7 @@ export interface TermsAndPrivacyProps {
|
|||
}
|
||||
|
||||
export async function TermsAndPrivacy({
|
||||
locale,
|
||||
paymentProvider,
|
||||
productName,
|
||||
termsOfServiceUrl,
|
||||
|
@ -88,20 +89,12 @@ export async function TermsAndPrivacy({
|
|||
),
|
||||
];
|
||||
|
||||
// TODO - Temporary
|
||||
// Identify an approach to ensure we don't have to perform this logic
|
||||
// in every component/page that requires localization.
|
||||
const languages = headers()
|
||||
.get('Accept-Language')
|
||||
?.split(',')
|
||||
.map((language) => language.split(';')[0]);
|
||||
|
||||
// TODO
|
||||
// Move to instantiation on start up. Ideally getBundle's, generateBundle, is only called once at startup,
|
||||
// and then that instance is used for all requests.
|
||||
// Approach 1 (Experimental): https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
|
||||
// Approach 2 (Node global): https://github.com/vercel/next.js/blob/canary/examples/with-knex/knex/index.js#L13
|
||||
const l10n = await getBundle(languages);
|
||||
const l10n = await getBundle([locale]);
|
||||
|
||||
return (
|
||||
<aside className="pt-14" aria-label="Terms and Privacy Notices">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* 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 { determineLocale } from './determine-locale';
|
||||
import { determineLocale, getLocaleFromRequest } from './determine-locale';
|
||||
|
||||
describe('l10n/determineLocale:', () => {
|
||||
it('finds a locale', () => {
|
||||
|
@ -58,4 +58,26 @@ describe('l10n/determineLocale:', () => {
|
|||
expect(determineLocale('en;q=0.5, fr;q=1.1')).toEqual('fr');
|
||||
expect(determineLocale('en;q=0.5, fr;q=-.1')).toEqual('en');
|
||||
});
|
||||
|
||||
describe('getLocaleFromRequest', () => {
|
||||
it('return searchParams', () => {
|
||||
expect(getLocaleFromRequest({ locale: 'fr-FR' }, null)).toEqual('fr');
|
||||
});
|
||||
|
||||
it('return searchParams in supportedLanguages', () => {
|
||||
expect(
|
||||
getLocaleFromRequest({ locale: 'ra-ND' }, null, ['ra-ND'])
|
||||
).toEqual('ra-ND');
|
||||
});
|
||||
|
||||
it('return accept language', () => {
|
||||
expect(getLocaleFromRequest({}, 'en-US;q=0.1, es-MX;q=0.8')).toEqual(
|
||||
'es-MX'
|
||||
);
|
||||
});
|
||||
|
||||
it('return default locale', () => {
|
||||
expect(getLocaleFromRequest({}, null)).toEqual('en-US');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,31 @@
|
|||
/* 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 { DEFAULT_LOCALE } from './l10n';
|
||||
import { parseAcceptLanguage } from './parse-accept-language';
|
||||
|
||||
/**
|
||||
* Get the best fitting locale, prioritizing request search params, followed by request header AcceptLanguage and DEFAULT_LOCALE as default
|
||||
* @param searchParams - Search parameters of the request
|
||||
* @param acceptLanguage - Accept language from request header
|
||||
* @returns The best fitting locale
|
||||
*/
|
||||
export function getLocaleFromRequest(
|
||||
searchParams: { locale?: string },
|
||||
acceptLanguage: string | null,
|
||||
supportedLanguages?: string[]
|
||||
) {
|
||||
if (searchParams.locale) {
|
||||
return determineLocale(searchParams?.locale, supportedLanguages);
|
||||
}
|
||||
|
||||
if (acceptLanguage) {
|
||||
return determineLocale(acceptLanguage, supportedLanguages);
|
||||
}
|
||||
|
||||
return DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of supported languages and an accept-language http header value, this resolves language that fits best.
|
||||
* @param acceptLanguage - The accept-language http header value
|
||||
|
|
|
@ -8,7 +8,7 @@ import * as path from 'path';
|
|||
// fr: new URL('./fr.ftl', import.meta.url),
|
||||
// };
|
||||
|
||||
const DEFAULT_LOCALE = 'en-US';
|
||||
export const DEFAULT_LOCALE = 'en-US';
|
||||
// const AVAILABLE_LOCALES = {
|
||||
// 'en-US': 'English',
|
||||
// fr: 'French',
|
||||
|
|
Загрузка…
Ссылка в новой задаче