feat(core): payment plans error boundary (#4744)

This commit is contained in:
Cats Juice
2023-11-06 10:42:21 +08:00
committed by GitHub
parent 3c4dbed16b
commit 7a8150398c
8 changed files with 128 additions and 22 deletions

View File

@@ -382,7 +382,9 @@ export const createConfiguration: (
devServer: { devServer: {
hot: 'only', hot: 'only',
liveReload: true, liveReload: true,
client: undefined, client: {
overlay: process.env.DISABLE_DEV_OVERLAY === 'true' ? false : undefined,
},
historyApiFallback: true, historyApiFallback: true,
static: { static: {
directory: resolve(rootPath, 'public'), directory: resolve(rootPath, 'public'),

View File

@@ -45,6 +45,9 @@
}, },
{ {
"env": "COVERAGE" "env": "COVERAGE"
},
{
"env": "DISABLE_DEV_OVERLAY"
} }
], ],
"options": { "options": {

View File

@@ -2,13 +2,14 @@ import { SubscriptionPlan } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import Tooltip from '@toeverything/components/tooltip'; import Tooltip from '@toeverything/components/tooltip';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useCallback } from 'react'; import React, { useCallback } from 'react';
import { withErrorBoundary } from 'react-error-boundary';
import { openSettingModalAtom } from '../../../atoms'; import { openSettingModalAtom } from '../../../atoms';
import { useUserSubscription } from '../../../hooks/use-subscription'; import { useUserSubscription } from '../../../hooks/use-subscription';
import * as styles from './style.css'; import * as styles from './style.css';
export const UserPlanButton = () => { const UserPlanButtonWithData = () => {
const [subscription] = useUserSubscription(); const [subscription] = useUserSubscription();
const plan = subscription?.plan ?? SubscriptionPlan.Free; const plan = subscription?.plan ?? SubscriptionPlan.Free;
@@ -35,3 +36,8 @@ export const UserPlanButton = () => {
</Tooltip> </Tooltip>
); );
}; };
// If fetch user data failed, just render empty.
export const UserPlanButton = withErrorBoundary(UserPlanButtonWithData, {
fallbackRender: () => <React.Fragment />,
});

View File

@@ -31,6 +31,7 @@ import {
type SubscriptionMutator, type SubscriptionMutator,
useUserSubscription, useUserSubscription,
} from '../../../../../hooks/use-subscription'; } from '../../../../../hooks/use-subscription';
import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary';
import { CancelAction, ResumeAction } from '../plans/actions'; import { CancelAction, ResumeAction } from '../plans/actions';
import * as styles from './style.css'; import * as styles from './style.css';
@@ -66,20 +67,24 @@ export const BillingSettings = () => {
title={t['com.affine.payment.billing-setting.title']()} title={t['com.affine.payment.billing-setting.title']()}
subtitle={t['com.affine.payment.billing-setting.subtitle']()} subtitle={t['com.affine.payment.billing-setting.subtitle']()}
/> />
<Suspense fallback={<SubscriptionSettingSkeleton />}> <SWRErrorBoundary FallbackComponent={SubscriptionSettingSkeleton}>
<SettingWrapper <Suspense fallback={<SubscriptionSettingSkeleton />}>
title={t['com.affine.payment.billing-setting.information']()} <SettingWrapper
> title={t['com.affine.payment.billing-setting.information']()}
<SubscriptionSettings /> >
</SettingWrapper> <SubscriptionSettings />
</Suspense> </SettingWrapper>
<Suspense fallback={<BillingHistorySkeleton />}> </Suspense>
<SettingWrapper </SWRErrorBoundary>
title={t['com.affine.payment.billing-setting.history']()} <SWRErrorBoundary FallbackComponent={BillingHistorySkeleton}>
> <Suspense fallback={<BillingHistorySkeleton />}>
<BillingHistory /> <SettingWrapper
</SettingWrapper> title={t['com.affine.payment.billing-setting.history']()}
</Suspense> >
<BillingHistory />
</SettingWrapper>
</Suspense>
</SWRErrorBoundary>
</> </>
); );
}; };

View File

@@ -9,8 +9,10 @@ import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useQuery } from '@affine/workspace/affine/gql'; import { useQuery } from '@affine/workspace/affine/gql';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { Suspense, useEffect, useRef, useState } from 'react'; import React, { Suspense, useEffect, useRef, useState } from 'react';
import type { FallbackProps } from 'react-error-boundary';
import { SWRErrorBoundary } from '../../../../../components/pure/swr-error-bundary';
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
import { useUserSubscription } from '../../../../../hooks/use-subscription'; import { useUserSubscription } from '../../../../../hooks/use-subscription';
import { PlanLayout } from './layout'; import { PlanLayout } from './layout';
@@ -192,9 +194,30 @@ const Settings = () => {
export const AFFiNECloudPlans = () => { export const AFFiNECloudPlans = () => {
return ( return (
// TODO: Error Boundary <SWRErrorBoundary FallbackComponent={PlansErrorBoundary}>
<Suspense fallback={<PlansSkeleton />}> <Suspense fallback={<PlansSkeleton />}>
<Settings /> <Settings />
</Suspense> </Suspense>
</SWRErrorBoundary>
); );
}; };
const PlansErrorBoundary = ({ resetErrorBoundary }: FallbackProps) => {
const t = useAFFiNEI18N();
const title = t['com.affine.payment.title']();
const subtitle = <React.Fragment />;
const tabs = <React.Fragment />;
const footer = <React.Fragment />;
const scroll = (
<div className={styles.errorTip}>
<span>{t['com.affine.payment.plans-error-tip']()}</span>
<a onClick={resetErrorBoundary} className={styles.errorTipRetry}>
{t['com.affine.payment.plans-error-retry']()}
</a>
</div>
);
return <PlanLayout {...{ title, subtitle, tabs, scroll, footer }} />;
};

View File

@@ -155,3 +155,12 @@ export const downgradeFooter = style({
export const textEmphasis = style({ export const textEmphasis = style({
color: 'var(--affine-text-emphasis-color)', color: 'var(--affine-text-emphasis-color)',
}); });
export const errorTip = style({
color: 'var(--affine-text-secondary-color)',
fontSize: '12px',
lineHeight: '20px',
});
export const errorTipRetry = style({
textDecoration: 'underline',
});

View File

@@ -0,0 +1,56 @@
import type { ErrorInfo } from 'react';
import React, { useRef } from 'react';
import type { ErrorBoundaryProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';
import { useSWRConfig } from 'swr';
/**
* If we use suspense mode in SWR, we need to preload or delete cache to retry request.
* Or the error will be cached and the request will not be retried.
*
* Reference:
* https://github.com/vercel/swr/issues/2740
* https://github.com/vercel/swr/blob/main/core/src/use-swr.ts#L690
* https://github.com/vercel/swr/tree/main/examples/suspense-retry
*/
export const SWRErrorBoundary = (props: ErrorBoundaryProps) => {
const { onReset, onError } = props;
const errorsRef = useRef<Error[]>([]);
const { cache } = useSWRConfig();
const clearErrorCache = React.useCallback(() => {
const errors = errorsRef.current;
errorsRef.current = [];
for (const key of cache.keys()) {
const item = cache.get(key);
if (errors.includes(item?.error)) {
cache.delete(key);
}
}
}, [cache]);
const onResetWithSWR = React.useCallback(
(details: any) => {
clearErrorCache();
onReset?.(details);
},
[clearErrorCache, onReset]
);
const onErrorWithSWR = React.useCallback(
(error: Error, info: ErrorInfo) => {
errorsRef.current.push(error);
onError?.(error, info);
},
[onError]
);
React.useEffect(() => clearErrorCache, [clearErrorCache]);
return (
<ErrorBoundary {...props} onReset={onResetWithSWR} onError={onErrorWithSWR}>
{props.children}
</ErrorBoundary>
);
};

View File

@@ -734,6 +734,8 @@
"com.affine.payment.updated-notify-title": "Subscription updated", "com.affine.payment.updated-notify-title": "Subscription updated",
"com.affine.payment.updated-notify-msg": "You have changed your plan to {{plan}} billing.", "com.affine.payment.updated-notify-msg": "You have changed your plan to {{plan}} billing.",
"com.affine.payment.updated-notify-msg.cancel-subscription": "No further charges will be made starting from the next billing cycle.", "com.affine.payment.updated-notify-msg.cancel-subscription": "No further charges will be made starting from the next billing cycle.",
"com.affine.payment.plans-error-tip": "Unable to load Pricing plans, please check your network. ",
"com.affine.payment.plans-error-retry": "Refresh",
"com.affine.storage.maximum-tips": "You have reached the maximum capacity limit for your current account", "com.affine.storage.maximum-tips": "You have reached the maximum capacity limit for your current account",
"com.affine.payment.tag-tooltips": "See all plans", "com.affine.payment.tag-tooltips": "See all plans",
"com.affine.payment.billing-setting.title": "Billing", "com.affine.payment.billing-setting.title": "Billing",