diff --git a/packages/frontend/core/.webpack/config.ts b/packages/frontend/core/.webpack/config.ts
index 25da82636a..d55865bf06 100644
--- a/packages/frontend/core/.webpack/config.ts
+++ b/packages/frontend/core/.webpack/config.ts
@@ -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'),
diff --git a/packages/frontend/core/project.json b/packages/frontend/core/project.json
index 1b74c0a09e..ee354d7c39 100644
--- a/packages/frontend/core/project.json
+++ b/packages/frontend/core/project.json
@@ -45,6 +45,9 @@
},
{
"env": "COVERAGE"
+ },
+ {
+ "env": "DISABLE_DEV_OVERLAY"
}
],
"options": {
diff --git a/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx b/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx
index 580db2ea11..b9b9ab1f16 100644
--- a/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx
+++ b/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx
@@ -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 = () => {
);
};
+
+// If fetch user data failed, just render empty.
+export const UserPlanButton = withErrorBoundary(UserPlanButtonWithData, {
+ fallbackRender: () => ,
+});
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx
index bca216a527..8f44116fca 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx
@@ -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']()}
/>
- }>
-
-
-
-
- }>
-
-
-
-
+
+ }>
+
+
+
+
+
+
+ }>
+
+
+
+
+
>
);
};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx
index 58c5d6dfc6..4990b17bb8 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx
@@ -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
- }>
-
-
+
+ }>
+
+
+
);
};
+
+const PlansErrorBoundary = ({ resetErrorBoundary }: FallbackProps) => {
+ const t = useAFFiNEI18N();
+
+ const title = t['com.affine.payment.title']();
+ const subtitle = ;
+ const tabs = ;
+ const footer = ;
+
+ const scroll = (
+
+ );
+
+ return ;
+};
diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts
index 0540ed746a..6e13c4d423 100644
--- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts
+++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/style.css.ts
@@ -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',
+});
diff --git a/packages/frontend/core/src/components/pure/swr-error-bundary.tsx b/packages/frontend/core/src/components/pure/swr-error-bundary.tsx
new file mode 100644
index 0000000000..ba40e69756
--- /dev/null
+++ b/packages/frontend/core/src/components/pure/swr-error-bundary.tsx
@@ -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([]);
+ 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 (
+
+ {props.children}
+
+ );
+};
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index c1a9b52ce0..d8678c6040 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -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",