feat(core): payment plans error boundary (#4744)
This commit is contained in:
@@ -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'),
|
||||||
|
|||||||
@@ -45,6 +45,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"env": "COVERAGE"
|
"env": "COVERAGE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"env": "DISABLE_DEV_OVERLAY"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"options": {
|
"options": {
|
||||||
|
|||||||
@@ -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 />,
|
||||||
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }} />;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user