client/components/mma/accountoverview/AccountOverview.tsx (318 lines of code) (raw):

import { css } from '@emotion/react'; import { from, headlineBold28, palette, space, textSans17, until, } from '@guardian/source/foundations'; import { Stack } from '@guardian/source/react-components'; import { capitalize } from 'lodash'; import { Fragment } from 'react'; import { featureSwitches } from '../../../../shared/featureSwitches'; import type { MPAPIResponse } from '../../../../shared/mpapiResponse'; import { isValidAppSubscription } from '../../../../shared/mpapiResponse'; import type { CancelledProductDetail, MembersDataApiResponse, ProductDetail, SingleProductDetail, } from '../../../../shared/productResponse'; import { getSpecificProductType, isProduct, isSpecificProductType, sortByJoinDate, } from '../../../../shared/productResponse'; import type { GroupedProductTypeKeys } from '../../../../shared/productTypes'; import { GROUPED_PRODUCT_TYPES, PRODUCT_TYPES, } from '../../../../shared/productTypes'; import { fetchWithDefaultParameters } from '../../../utilities/fetch'; import { LoadingState, useAsyncLoader, } from '../../../utilities/hooks/useAsyncLoader'; import { allRecurringProductsDetailFetcher, allSingleProductsDetailFetcher, } from '../../../utilities/productUtils'; import { GenericErrorScreen } from '../../shared/GenericErrorScreen'; import { NAV_LINKS } from '../../shared/nav/NavConfig'; import { SupportTheGuardianButton } from '../../shared/SupportTheGuardianButton'; import { isCancelled } from '../cancel/CancellationSummary'; import { PageContainer } from '../Page'; import { JsonResponseHandler } from '../shared/asyncComponents/DefaultApiResponseHandler'; import { DefaultLoadingView } from '../shared/asyncComponents/DefaultLoadingView'; import { DownloadAppCtaVariation1 } from '../shared/DownloadAppCtaVariation1'; import { DownloadFeastAppCtaWithImage } from '../shared/DownloadFeastAppCtaWithImage'; import type { IsFromAppProps } from '../shared/IsFromAppProps'; import { NewspaperArchiveCta } from '../shared/NewspaperArchiveCta'; import { nonServiceableCountries } from '../shared/NonServiceableCountries'; import { PaymentFailureAlertIfApplicable } from '../shared/PaymentFailureAlertIfApplicable'; import { CancelledProductCard } from './CancelledProductCard'; import { EmptyAccountOverview } from './EmptyAccountOverview'; import { InAppPurchaseCard } from './InAppPurchaseCard'; import { PersonalisedHeader } from './PersonalisedHeader'; import { ProductCard } from './ProductCard'; import { SingleContributionCard } from './SingleContributionCard'; type AccountOverviewResponse = [ MembersDataApiResponse, CancelledProductDetail[], MPAPIResponse, SingleProductDetail[], ]; const subHeadingCss = css` margin: ${space[6]}px 0 ${space[6]}px; border-top: 1px solid ${palette.neutral['86']}; ${headlineBold28}; ${until.tablet} { font-size: 1.25rem; line-height: 1.6; } ${from.tablet} { margin-top: ${space[8]}px; } `; const AccountOverviewPage = ({ isFromApp }: IsFromAppProps) => { const { data: accountOverviewResponse, loadingState, }: { data: AccountOverviewResponse | null; loadingState: LoadingState; } = useAsyncLoader(accountOverviewFetcher, JsonResponseHandler); if (loadingState == LoadingState.HasError) { return <GenericErrorScreen />; } if (loadingState == LoadingState.IsLoading) { return ( <DefaultLoadingView loadingMessage="Loading your account details..." /> ); } if (accountOverviewResponse === null) { return <GenericErrorScreen />; } const [ mdapiResponse, cancelledProductsResponse, mpapiResponse, singleContributions, ] = accountOverviewResponse; const allActiveProductDetails = mdapiResponse.products .filter(isProduct) .sort(sortByJoinDate); const activeProductsNotPendingCancellation = allActiveProductDetails.filter( (product: ProductDetail) => !product.subscription.cancelledAt, ); const allCancelledProductDetails = cancelledProductsResponse.sort( (a: CancelledProductDetail, b: CancelledProductDetail) => b.subscription.start.localeCompare(a.subscription.start), ); const allProductCategories = [ ...allActiveProductDetails, ...allCancelledProductDetails, ].map((product: ProductDetail | CancelledProductDetail) => { const specificProductType = getSpecificProductType(product.tier); if ( specificProductType.groupedProductType === 'recurringSupportWithBenefits' ) { return 'subscriptions'; // we want to override the display text in MMA for RC/S+ but not affect functionality } return specificProductType.groupedProductType; }); const uniqueProductCategories = [...new Set(allProductCategories)]; const appSubscriptions = mpapiResponse.subscriptions.filter( isValidAppSubscription, ); if ( featureSwitches.appSubscriptions && appSubscriptions.length > 0 && !uniqueProductCategories.includes('subscriptions') ) { uniqueProductCategories.push('subscriptions'); } if ( singleContributions.length > 0 && !uniqueProductCategories.includes('subscriptions') ) { uniqueProductCategories.push('subscriptions'); } if ( allActiveProductDetails.length === 0 && allCancelledProductDetails.length === 0 && appSubscriptions.length === 0 && singleContributions.length === 0 ) { return <EmptyAccountOverview />; } const maybeFirstPaymentFailure = allActiveProductDetails.find( (product) => product.alertText, ); const hasDigiSubAndContribution = allActiveProductDetails.some((productDetail) => isSpecificProductType(productDetail, PRODUCT_TYPES.contributions), ) && allActiveProductDetails.some((productDetail) => isSpecificProductType(productDetail, PRODUCT_TYPES.digipack), ); const hasDigitalPlusPrint = allActiveProductDetails.some((productDetail) => isSpecificProductType(productDetail, PRODUCT_TYPES.tierthree), ); const hasNonServiceableCountry = nonServiceableCountries.includes( allActiveProductDetails.find(isProduct)?.billingCountry as string, ); const isEligibleToSwitch = !maybeFirstPaymentFailure && !hasDigiSubAndContribution && !hasNonServiceableCountry; const visualProductGroupingCategory = ( product: ProductDetail | CancelledProductDetail, ): GroupedProductTypeKeys => { const specificProductType = getSpecificProductType(product.tier); if ( specificProductType.groupedProductType === 'recurringSupportWithBenefits' ) { return 'subscriptions'; } return specificProductType.groupedProductType; }; return ( <> <PersonalisedHeader mdapiResponse={mdapiResponse} mpapiResponse={mpapiResponse} /> <PaymentFailureAlertIfApplicable productDetails={allActiveProductDetails} isFromApp={isFromApp} /> {uniqueProductCategories.map((category) => { const groupedProductType = GROUPED_PRODUCT_TYPES[category]; const activeProductsInCategory = allActiveProductDetails.filter( (activeProduct) => visualProductGroupingCategory(activeProduct) === category, ); const cancelledProductsInCategory = allCancelledProductDetails.filter( (cancelledProduct) => visualProductGroupingCategory(cancelledProduct) === category, ); return ( <Fragment key={category}> <h2 css={subHeadingCss}> {capitalize(groupedProductType.groupFriendlyName)} </h2> <Stack space={6}> {activeProductsInCategory.map((productDetail) => ( <ProductCard key={ productDetail.subscription .subscriptionId } productDetail={productDetail} isEligibleToSwitch={isEligibleToSwitch} user={mdapiResponse.user} /> ))} {cancelledProductsInCategory.map( (cancelledProductDetail) => ( <CancelledProductCard key={ cancelledProductDetail.subscription .subscriptionId } productDetail={cancelledProductDetail} hasOtherActiveSubs={ !!activeProductsNotPendingCancellation.length } /> ), )} {groupedProductType.supportTheGuardianSectionProps && (cancelledProductsInCategory.length > 0 || activeProductsInCategory.some( (productDetail) => isCancelled( productDetail.subscription, ), )) && ( <div> <p css={css` ${textSans17}; `} > { groupedProductType .supportTheGuardianSectionProps .message } </p> <SupportTheGuardianButton {...groupedProductType.supportTheGuardianSectionProps} size="small" /> </div> )} {featureSwitches.appSubscriptions && appSubscriptions.length > 0 && category === 'subscriptions' && appSubscriptions.map((subscription) => ( <InAppPurchaseCard key={subscription.subscriptionId} subscription={subscription} /> ))} {category === 'subscriptions' && singleContributions.length > 0 && ( <SingleContributionCard singleContributions={ singleContributions } /> )} </Stack> </Fragment> ); })} {hasDigitalPlusPrint && ( <> <h2 css={subHeadingCss}> Get the most out of your benefits </h2> <Stack space={6}> {featureSwitches.digitalArchiveCta && ( <NewspaperArchiveCta /> )} <DownloadAppCtaVariation1 /> <DownloadFeastAppCtaWithImage /> </Stack> </> )} </> ); }; const accountOverviewFetcher = () => Promise.all([ allRecurringProductsDetailFetcher(), fetchWithDefaultParameters('/api/cancelled/'), fetchWithDefaultParameters('/mpapi/user/mobile-subscriptions'), allSingleProductsDetailFetcher(), ]); export const AccountOverview = ({ isFromApp }: IsFromAppProps) => { return ( <PageContainer selectedNavItem={NAV_LINKS.accountOverview} pageTitle="Account overview" > <AccountOverviewPage isFromApp={isFromApp} /> </PageContainer> ); };