зеркало из https://github.com/mozilla/fxa.git
Merge pull request #18275 from mozilla/FXA-7605
feat(payments,shared): Update cart with saved tax location
This commit is contained in:
Коммит
bc62e1f479
|
@ -56,6 +56,9 @@ export default async function RootLayout({
|
|||
cartDataPromise,
|
||||
sessionPromise,
|
||||
]);
|
||||
const purchaseDetails =
|
||||
cms.defaultPurchase.purchaseDetails.localizations.at(0) ||
|
||||
cms.defaultPurchase.purchaseDetails;
|
||||
return (
|
||||
<MetricsWrapper cart={cart}>
|
||||
{session?.user?.email && (
|
||||
|
@ -79,33 +82,31 @@ export default async function RootLayout({
|
|||
listAmount={cart.upcomingInvoicePreview.listAmount}
|
||||
/>
|
||||
}
|
||||
purchaseDetails={
|
||||
cms.defaultPurchase.purchaseDetails.localizations.at(0) ||
|
||||
cms.defaultPurchase.purchaseDetails
|
||||
}
|
||||
purchaseDetails={purchaseDetails}
|
||||
>
|
||||
<Details
|
||||
l10n={l10n}
|
||||
interval={cart.interval}
|
||||
invoice={cart.upcomingInvoicePreview}
|
||||
purchaseDetails={
|
||||
cms.defaultPurchase.purchaseDetails.localizations.at(0) ||
|
||||
cms.defaultPurchase.purchaseDetails
|
||||
}
|
||||
purchaseDetails={purchaseDetails}
|
||||
/>
|
||||
</PurchaseDetails>
|
||||
<SelectTaxLocation
|
||||
cartId={cart.id}
|
||||
cartVersion={cart.version}
|
||||
cmsCountries={cms.countries}
|
||||
locale={locale.substring(0, 2)}
|
||||
productName={purchaseDetails.productName}
|
||||
unsupportedLocations={config.subscriptionsUnsupportedLocations}
|
||||
countryCode={cart.taxAddress?.countryCode}
|
||||
postalCode={cart.taxAddress?.postalCode}
|
||||
/>
|
||||
<CouponForm
|
||||
cartId={cart.id}
|
||||
cartVersion={cart.version}
|
||||
promoCode={cart.couponCode}
|
||||
readOnly={false}
|
||||
/>
|
||||
<SelectTaxLocation
|
||||
locale={locale.substring(0, 2)}
|
||||
unsupportedLocations={config.subscriptionsUnsupportedLocations}
|
||||
countryCode={cart.taxAddress?.countryCode}
|
||||
postalCode={cart.taxAddress?.postalCode}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className="bg-white rounded-b-lg shadow-sm shadow-grey-300 border-t-0 mb-6 pt-4 px-4 pb-14 rounded-t-lg text-grey-600 tablet:clip-shadow tablet:rounded-t-none desktop:px-12 desktop:pb-12">
|
||||
|
@ -113,9 +114,8 @@ export default async function RootLayout({
|
|||
<TermsAndPrivacy
|
||||
l10n={l10n}
|
||||
{...cart}
|
||||
{...purchaseDetails}
|
||||
{...(cms.commonContent.localizations.at(0) || cms.commonContent)}
|
||||
{...(cms.defaultPurchase.purchaseDetails.localizations.at(0) ||
|
||||
cms.defaultPurchase.purchaseDetails)}
|
||||
contentServerUrl={config.contentServerUrl}
|
||||
showFXALinks={true}
|
||||
/>
|
||||
|
|
|
@ -9,6 +9,9 @@ select-tax-location-country-code-label = Country
|
|||
select-tax-location-country-code-placeholder = Select your country
|
||||
select-tax-location-error-missing-country-code = Please select your country
|
||||
|
||||
# $productName (String) - The name of the product to be downloaded, e.g. Mozilla VPN
|
||||
select-tax-location-product-not-available = { $productName } is not available in this location.
|
||||
|
||||
select-tax-location-postal-code-label = Postal Code
|
||||
select-tax-location-postal-code =
|
||||
.placeholder = Enter your postal code
|
||||
|
|
|
@ -9,7 +9,7 @@ import * as Form from '@radix-ui/react-form';
|
|||
import countries from 'i18n-iso-countries';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ButtonVariant } from '@fxa/payments/ui';
|
||||
import { validatePostalCode } from '@fxa/payments/ui/actions';
|
||||
import { updateCartAction, validatePostalCode } from '@fxa/payments/ui/actions';
|
||||
import { SubmitButton } from '../SubmitButton';
|
||||
|
||||
interface CollapsedProps {
|
||||
|
@ -37,7 +37,9 @@ const Collapsed = ({
|
|||
variant={ButtonVariant.Secondary}
|
||||
data-testid="tax-location-edit-button"
|
||||
>
|
||||
<Localized id="select-tax-location-edit-button">Edit</Localized>
|
||||
<Localized id="select-tax-location-edit-button">
|
||||
<p>Edit</p>
|
||||
</Localized>
|
||||
</SubmitButton>
|
||||
</Form.Submit>
|
||||
</span>
|
||||
|
@ -55,7 +57,9 @@ const Collapsed = ({
|
|||
};
|
||||
|
||||
interface ExpandedProps {
|
||||
cmsCountryCodes: string[];
|
||||
locale: string;
|
||||
productName: string;
|
||||
unsupportedLocations: string;
|
||||
countryCode: string | undefined;
|
||||
postalCode: string | undefined;
|
||||
|
@ -63,7 +67,9 @@ interface ExpandedProps {
|
|||
}
|
||||
|
||||
const Expanded = ({
|
||||
cmsCountryCodes,
|
||||
locale,
|
||||
productName,
|
||||
unsupportedLocations,
|
||||
countryCode,
|
||||
postalCode,
|
||||
|
@ -82,6 +88,7 @@ const Expanded = ({
|
|||
[key: string]: boolean;
|
||||
}>({
|
||||
missingCountryCode: false,
|
||||
productNotAvailable: false,
|
||||
unsupportedCountry: false,
|
||||
invalidPostalCode: false,
|
||||
locationNotUpdated: false,
|
||||
|
@ -113,6 +120,7 @@ const Expanded = ({
|
|||
setServerErrors((prev) => ({
|
||||
...prev,
|
||||
missingCountryCode: false,
|
||||
productNotAvailable: false,
|
||||
unsupportedCountries: false,
|
||||
}));
|
||||
|
||||
|
@ -122,10 +130,35 @@ const Expanded = ({
|
|||
|
||||
setSelectedCountryCode(countryCode);
|
||||
|
||||
if (unsupportedLocations.includes(countryCode)) {
|
||||
setServerErrors((prev) => ({ ...prev, unsupportedCountry: true }));
|
||||
// If the selected location is not supported per TOS, it is not necessary to
|
||||
// also inform the customer that the product is not available in their location.
|
||||
if (
|
||||
unsupportedLocations.includes(countryCode) &&
|
||||
!cmsCountryCodes.includes(countryCode)
|
||||
) {
|
||||
setServerErrors((prev) => ({
|
||||
...prev,
|
||||
productNotAvailable: false,
|
||||
unsupportedCountry: true,
|
||||
}));
|
||||
} else if (unsupportedLocations.includes(countryCode)) {
|
||||
setServerErrors((prev) => ({
|
||||
...prev,
|
||||
productNotAvailable: false,
|
||||
unsupportedCountry: true,
|
||||
}));
|
||||
} else if (!cmsCountryCodes.includes(countryCode)) {
|
||||
setServerErrors((prev) => ({
|
||||
...prev,
|
||||
productNotAvailable: true,
|
||||
unsupportedCountry: false,
|
||||
}));
|
||||
} else {
|
||||
setServerErrors((prev) => ({ ...prev, unsupportedCountry: false }));
|
||||
setServerErrors((prev) => ({
|
||||
...prev,
|
||||
productNotAvailable: false,
|
||||
unsupportedCountry: false,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -206,6 +239,15 @@ const Expanded = ({
|
|||
</p>
|
||||
</Localized>
|
||||
</Form.Message>
|
||||
{serverErrors.productNotAvailable && (
|
||||
<Form.Message>
|
||||
<Localized id="select-tax-location-product-not-available">
|
||||
<p className="mt-1 text-alert-red" role="alert">
|
||||
{productName} is not available in this location.
|
||||
</p>
|
||||
</Localized>
|
||||
</Form.Message>
|
||||
)}
|
||||
{serverErrors.unsupportedCountry && (
|
||||
<Form.Message>
|
||||
<Localized id="next-location-unsupported">
|
||||
|
@ -282,7 +324,9 @@ const Expanded = ({
|
|||
data-testid="tax-location-save-button"
|
||||
variant={ButtonVariant.Secondary}
|
||||
>
|
||||
<Localized id="select-tax-location-save-button">Save</Localized>
|
||||
<Localized id="select-tax-location-save-button">
|
||||
<p>Save</p>
|
||||
</Localized>
|
||||
</SubmitButton>
|
||||
</Form.Submit>
|
||||
</Form.Root>
|
||||
|
@ -290,14 +334,22 @@ const Expanded = ({
|
|||
};
|
||||
|
||||
interface SelectTaxLocationProps {
|
||||
cartId: string;
|
||||
cartVersion: number;
|
||||
cmsCountries: string[];
|
||||
locale: string;
|
||||
productName: string;
|
||||
unsupportedLocations: string;
|
||||
countryCode: string | undefined;
|
||||
postalCode: string | undefined;
|
||||
}
|
||||
|
||||
export function SelectTaxLocation({
|
||||
cartId,
|
||||
cartVersion,
|
||||
cmsCountries,
|
||||
locale,
|
||||
productName,
|
||||
unsupportedLocations,
|
||||
countryCode,
|
||||
postalCode,
|
||||
|
@ -306,6 +358,7 @@ export function SelectTaxLocation({
|
|||
!countryCode || !postalCode
|
||||
);
|
||||
const [alertStatus, setAlertStatus] = useState<boolean>(false);
|
||||
const cmsCountryCodes = cmsCountries.map((country) => country.slice(0, 2));
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -318,15 +371,19 @@ export function SelectTaxLocation({
|
|||
|
||||
{expanded ? (
|
||||
<Expanded
|
||||
cmsCountryCodes={cmsCountryCodes}
|
||||
locale={locale}
|
||||
productName={productName}
|
||||
unsupportedLocations={unsupportedLocations}
|
||||
countryCode={countryCode}
|
||||
postalCode={postalCode}
|
||||
saveAction={(countryCode: string, postalCode: string) => {
|
||||
saveAction={async (countryCode: string, postalCode: string) => {
|
||||
setExpanded(false);
|
||||
|
||||
// Call function to save to Cart
|
||||
// await saveTaxLocationAction(countryCode, postalCode);
|
||||
await updateCartAction(cartId, cartVersion, {
|
||||
taxAddress: { countryCode, postalCode },
|
||||
});
|
||||
setAlertStatus(true);
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { CodegenConfig } from '@graphql-codegen/cli';
|
||||
|
||||
const STRAPI_GRAPHQL_API_URL = process.env.STRAPI_GRAPHQL_API_URL;
|
||||
const STRAPI_API_KEY = process.env.STRAPI_API_KEY;
|
||||
const STRAPI_GRAPHQL_API_URL =
|
||||
process.env.STRAPI_CLIENT_CONFIG__GRAPHQL_API_URI;
|
||||
const STRAPI_API_KEY = process.env.STRAPI_CLIENT_CONFIG__API_KEY;
|
||||
|
||||
if (!STRAPI_GRAPHQL_API_URL || !STRAPI_API_KEY) {
|
||||
throw new Error('Please provide a valid Strapi API URL and API key');
|
||||
|
|
|
@ -18,7 +18,7 @@ const documents = {
|
|||
"\n query EligibilityContentByPlanIds($stripePlanIds: [String]!) {\n purchases(\n filters: {\n or: [\n { stripePlanChoices: { stripePlanChoice: { in: $stripePlanIds } } }\n {\n offering: {\n stripeLegacyPlans: { stripeLegacyPlan: { in: $stripePlanIds } }\n }\n }\n ]\n }\n pagination: { limit: 200 }\n ) {\n stripePlanChoices {\n stripePlanChoice\n }\n offering {\n apiIdentifier\n stripeProductId\n stripeLegacyPlans(pagination: { limit: 200 }) {\n stripeLegacyPlan\n }\n countries\n subGroups {\n groupName\n offerings {\n apiIdentifier\n stripeProductId\n stripeLegacyPlans(pagination: { limit: 200 }) {\n stripeLegacyPlan\n }\n countries\n }\n }\n }\n }\n }\n": types.EligibilityContentByPlanIdsDocument,
|
||||
"\n query Locales {\n i18NLocales(pagination: { limit: 100 }) {\n code\n }\n }\n": types.LocalesDocument,
|
||||
"\n query Offering($id: ID!, $locale: String!) {\n offering(documentId: $id) {\n stripeProductId\n countries\n defaultPurchase {\n purchaseDetails {\n productName\n details\n subtitle\n webIcon\n localizations(filters: { locale: { eq: $locale } }) {\n productName\n details\n subtitle\n webIcon\n }\n }\n }\n }\n }\n": types.OfferingDocument,
|
||||
"\n query PageContentForOffering($locale: String!, $apiIdentifier: String!) {\n offerings(\n filters: { apiIdentifier: { eq: $apiIdentifier } }\n pagination: { limit: 200 }\n ) {\n apiIdentifier\n stripeProductId\n defaultPurchase {\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n localizations(filters: { locale: { eq: $locale } }) {\n details\n productName\n subtitle\n webIcon\n }\n }\n }\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n localizations(filters: { locale: { eq: $locale } }) {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n }\n }\n }\n }\n": types.PageContentForOfferingDocument,
|
||||
"\n query PageContentForOffering($locale: String!, $apiIdentifier: String!) {\n offerings(\n filters: { apiIdentifier: { eq: $apiIdentifier } }\n pagination: { limit: 200 }\n ) {\n apiIdentifier\n countries\n stripeProductId\n defaultPurchase {\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n localizations(filters: { locale: { eq: $locale } }) {\n details\n productName\n subtitle\n webIcon\n }\n }\n }\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n localizations(filters: { locale: { eq: $locale } }) {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n }\n }\n }\n }\n": types.PageContentForOfferingDocument,
|
||||
"\n query PurchaseWithDetailsOfferingContent(\n $locale: String!\n $stripePlanIds: [String]!\n ) {\n purchases(\n filters: {\n or: [\n { stripePlanChoices: { stripePlanChoice: { in: $stripePlanIds } } }\n {\n offering: {\n stripeLegacyPlans: { stripeLegacyPlan: { in: $stripePlanIds } }\n }\n }\n ]\n }\n pagination: { limit: 500 }\n ) {\n stripePlanChoices {\n stripePlanChoice\n }\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n localizations(filters: { locale: { eq: $locale } }) {\n details\n productName\n subtitle\n webIcon\n }\n }\n offering {\n stripeProductId\n stripeLegacyPlans(pagination: { limit: 200 }) {\n stripeLegacyPlan\n }\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n localizations(filters: { locale: { eq: $locale } }) {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n }\n }\n }\n }\n }\n": types.PurchaseWithDetailsOfferingContentDocument,
|
||||
"\n query ServicesWithCapabilities {\n services(pagination: { limit: 500 }) {\n oauthClientId\n capabilities {\n slug\n }\n }\n }\n": types.ServicesWithCapabilitiesDocument,
|
||||
};
|
||||
|
@ -60,7 +60,7 @@ export function graphql(source: "\n query Offering($id: ID!, $locale: String!)
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query PageContentForOffering($locale: String!, $apiIdentifier: String!) {\n offerings(\n filters: { apiIdentifier: { eq: $apiIdentifier } }\n pagination: { limit: 200 }\n ) {\n apiIdentifier\n stripeProductId\n defaultPurchase {\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n localizations(filters: { locale: { eq: $locale } }) {\n details\n productName\n subtitle\n webIcon\n }\n }\n }\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n localizations(filters: { locale: { eq: $locale } }) {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n }\n }\n }\n }\n"): (typeof documents)["\n query PageContentForOffering($locale: String!, $apiIdentifier: String!) {\n offerings(\n filters: { apiIdentifier: { eq: $apiIdentifier } }\n pagination: { limit: 200 }\n ) {\n apiIdentifier\n stripeProductId\n defaultPurchase {\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n localizations(filters: { locale: { eq: $locale } }) {\n details\n productName\n subtitle\n webIcon\n }\n }\n }\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n localizations(filters: { locale: { eq: $locale } }) {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n }\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query PageContentForOffering($locale: String!, $apiIdentifier: String!) {\n offerings(\n filters: { apiIdentifier: { eq: $apiIdentifier } }\n pagination: { limit: 200 }\n ) {\n apiIdentifier\n countries\n stripeProductId\n defaultPurchase {\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n localizations(filters: { locale: { eq: $locale } }) {\n details\n productName\n subtitle\n webIcon\n }\n }\n }\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n localizations(filters: { locale: { eq: $locale } }) {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n }\n }\n }\n }\n"): (typeof documents)["\n query PageContentForOffering($locale: String!, $apiIdentifier: String!) {\n offerings(\n filters: { apiIdentifier: { eq: $apiIdentifier } }\n pagination: { limit: 200 }\n ) {\n apiIdentifier\n countries\n stripeProductId\n defaultPurchase {\n purchaseDetails {\n details\n productName\n subtitle\n webIcon\n localizations(filters: { locale: { eq: $locale } }) {\n details\n productName\n subtitle\n webIcon\n }\n }\n }\n commonContent {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n localizations(filters: { locale: { eq: $locale } }) {\n privacyNoticeUrl\n privacyNoticeDownloadUrl\n termsOfServiceUrl\n termsOfServiceDownloadUrl\n cancellationUrl\n emailIcon\n successActionButtonUrl\n successActionButtonLabel\n newsletterLabelTextCode\n newsletterSlug\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -47,6 +47,7 @@ export const PageContentOfferingResultFactory = (
|
|||
override?: Partial<PageContentOfferingResult>
|
||||
): PageContentOfferingResult => ({
|
||||
apiIdentifier: faker.string.sample(),
|
||||
countries: [faker.string.sample()],
|
||||
stripeProductId: faker.string.sample(),
|
||||
defaultPurchase: PageContentOfferingDefaultPurchaseResultFactory(),
|
||||
commonContent: {
|
||||
|
|
|
@ -11,6 +11,7 @@ export const pageContentForOfferingQuery = graphql(`
|
|||
pagination: { limit: 200 }
|
||||
) {
|
||||
apiIdentifier
|
||||
countries
|
||||
stripeProductId
|
||||
defaultPurchase {
|
||||
purchaseDetails {
|
||||
|
|
|
@ -46,6 +46,7 @@ export interface PageContentOfferingTransformed
|
|||
|
||||
export interface PageContentOfferingResult {
|
||||
apiIdentifier: string;
|
||||
countries: string[];
|
||||
stripeProductId: string;
|
||||
defaultPurchase: PageContentOfferingDefaultPurchaseResult;
|
||||
commonContent: PageContentCommonContentResult & {
|
||||
|
|
Загрузка…
Ссылка в новой задаче