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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 128 additions and 22 deletions

View File

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

View File

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

View File

@ -2,13 +2,14 @@ import { SubscriptionPlan } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import Tooltip from '@toeverything/components/tooltip';
import { useSetAtom } from 'jotai';
import { useCallback } from 'react';
import React, { useCallback } from 'react';
import { withErrorBoundary } from 'react-error-boundary';
import { openSettingModalAtom } from '../../../atoms';
import { useUserSubscription } from '../../../hooks/use-subscription';
import * as styles from './style.css';
export const UserPlanButton = () => {
const UserPlanButtonWithData = () => {
const [subscription] = useUserSubscription();
const plan = subscription?.plan ?? SubscriptionPlan.Free;
@ -35,3 +36,8 @@ export const UserPlanButton = () => {
</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,
useUserSubscription,
} from '../../../../../hooks/use-subscription';
import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary';
import { CancelAction, ResumeAction } from '../plans/actions';
import * as styles from './style.css';
@ -66,20 +67,24 @@ export const BillingSettings = () => {
title={t['com.affine.payment.billing-setting.title']()}
subtitle={t['com.affine.payment.billing-setting.subtitle']()}
/>
<Suspense fallback={<SubscriptionSettingSkeleton />}>
<SettingWrapper
title={t['com.affine.payment.billing-setting.information']()}
>
<SubscriptionSettings />
</SettingWrapper>
</Suspense>
<Suspense fallback={<BillingHistorySkeleton />}>
<SettingWrapper
title={t['com.affine.payment.billing-setting.history']()}
>
<BillingHistory />
</SettingWrapper>
</Suspense>
<SWRErrorBoundary FallbackComponent={SubscriptionSettingSkeleton}>
<Suspense fallback={<SubscriptionSettingSkeleton />}>
<SettingWrapper
title={t['com.affine.payment.billing-setting.information']()}
>
<SubscriptionSettings />
</SettingWrapper>
</Suspense>
</SWRErrorBoundary>
<SWRErrorBoundary FallbackComponent={BillingHistorySkeleton}>
<Suspense fallback={<BillingHistorySkeleton />}>
<SettingWrapper
title={t['com.affine.payment.billing-setting.history']()}
>
<BillingHistory />
</SettingWrapper>
</Suspense>
</SWRErrorBoundary>
</>
);
};

View File

@ -9,8 +9,10 @@ import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useQuery } from '@affine/workspace/affine/gql';
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 { useUserSubscription } from '../../../../../hooks/use-subscription';
import { PlanLayout } from './layout';
@ -192,9 +194,30 @@ const Settings = () => {
export const AFFiNECloudPlans = () => {
return (
// TODO: Error Boundary
<Suspense fallback={<PlansSkeleton />}>
<Settings />
</Suspense>
<SWRErrorBoundary FallbackComponent={PlansErrorBoundary}>
<Suspense fallback={<PlansSkeleton />}>
<Settings />
</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({
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-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.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.payment.tag-tooltips": "See all plans",
"com.affine.payment.billing-setting.title": "Billing",