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 = ( +
+ {t['com.affine.payment.plans-error-tip']()} + + {t['com.affine.payment.plans-error-retry']()} + +
+ ); + + 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",