mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-24 05:07:06 +08:00
fix(core): handle the getSession network error properly (#4909)
If network offline or API error happens, the `session` returned by the `useSession` hook will be null, so we can't assume it is not null. There should be following changes: 1. create a page in ErrorBoundary to let the user refetch the session. 2. The `SessionProvider` stop to pull the new session once the session is null, we need to figure out a way to pull the new session when the network is back or the user click the refetch button.
This commit is contained in:
@@ -7,12 +7,7 @@ import { useCallback, useState } from 'react';
|
|||||||
import { pushNotificationAtom } from '../notification-center';
|
import { pushNotificationAtom } from '../notification-center';
|
||||||
import { AuthPageContainer } from './auth-page-container';
|
import { AuthPageContainer } from './auth-page-container';
|
||||||
import { SetPassword } from './set-password';
|
import { SetPassword } from './set-password';
|
||||||
type User = {
|
import type { User } from './type';
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
image: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ChangePasswordPage: FC<{
|
export const ChangePasswordPage: FC<{
|
||||||
user: User;
|
user: User;
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ export * from './set-password-page';
|
|||||||
export * from './sign-in-page-container';
|
export * from './sign-in-page-container';
|
||||||
export * from './sign-in-success-page';
|
export * from './sign-in-success-page';
|
||||||
export * from './sign-up-page';
|
export * from './sign-up-page';
|
||||||
|
export type { User } from './type';
|
||||||
|
|||||||
@@ -7,13 +7,7 @@ import { useCallback, useState } from 'react';
|
|||||||
import { pushNotificationAtom } from '../notification-center';
|
import { pushNotificationAtom } from '../notification-center';
|
||||||
import { AuthPageContainer } from './auth-page-container';
|
import { AuthPageContainer } from './auth-page-container';
|
||||||
import { SetPassword } from './set-password';
|
import { SetPassword } from './set-password';
|
||||||
|
import type { User } from './type';
|
||||||
type User = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
image: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SetPasswordPage: FC<{
|
export const SetPasswordPage: FC<{
|
||||||
user: User;
|
user: User;
|
||||||
|
|||||||
@@ -7,12 +7,7 @@ import { useCallback, useState } from 'react';
|
|||||||
import { pushNotificationAtom } from '../notification-center';
|
import { pushNotificationAtom } from '../notification-center';
|
||||||
import { AuthPageContainer } from './auth-page-container';
|
import { AuthPageContainer } from './auth-page-container';
|
||||||
import { SetPassword } from './set-password';
|
import { SetPassword } from './set-password';
|
||||||
type User = {
|
import type { User } from './type';
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
image: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SignUpPage: FC<{
|
export const SignUpPage: FC<{
|
||||||
user: User;
|
user: User;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
image?: string | null;
|
||||||
|
}
|
||||||
@@ -114,7 +114,10 @@ export const createConfiguration: (
|
|||||||
buildFlags.mode === 'production'
|
buildFlags.mode === 'production'
|
||||||
? 'js/chunk.[name]-[contenthash:8].js'
|
? 'js/chunk.[name]-[contenthash:8].js'
|
||||||
: 'js/chunk.[name].js',
|
: 'js/chunk.[name].js',
|
||||||
assetModuleFilename: 'assets/[name]-[contenthash:8][ext][query]',
|
assetModuleFilename:
|
||||||
|
buildFlags.mode === 'production'
|
||||||
|
? 'assets/[name]-[contenthash:8][ext][query]'
|
||||||
|
: '[name][ext]',
|
||||||
devtoolModuleFilenameTemplate: 'webpack://[namespace]/[resource-path]',
|
devtoolModuleFilenameTemplate: 'webpack://[namespace]/[resource-path]',
|
||||||
hotUpdateChunkFilename: 'hot/[id].[fullhash].js',
|
hotUpdateChunkFilename: 'hot/[id].[fullhash].js',
|
||||||
hotUpdateMainFilename: 'hot/[runtime].[fullhash].json',
|
hotUpdateMainFilename: 'hot/[runtime].[fullhash].json',
|
||||||
@@ -292,7 +295,7 @@ export const createConfiguration: (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
exclude: [/node_modules/],
|
exclude: [/node_modules/, /\.assets\.svg$/],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(png|jpg|gif|svg|webp|mp4)$/,
|
test: /\.(png|jpg|gif|svg|webp|mp4)$/,
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const errorLayout = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const errorDetailStyle = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
maxWidth: '420px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const errorTitle = style({
|
||||||
|
fontSize: '36px',
|
||||||
|
lineHeight: '44px',
|
||||||
|
fontWeight: 700,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const errorImage = style({
|
||||||
|
height: '178px',
|
||||||
|
maxWidth: '400px',
|
||||||
|
flexGrow: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const errorDescription = style({
|
||||||
|
marginTop: '24px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const errorRetryButton = style({
|
||||||
|
marginTop: '24px',
|
||||||
|
width: '94px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const errorDivider = style({
|
||||||
|
width: '20px',
|
||||||
|
height: '100%',
|
||||||
|
});
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import type {
|
||||||
|
QueryParamError,
|
||||||
|
Unreachable,
|
||||||
|
WorkspaceNotFoundError,
|
||||||
|
} from '@affine/env/constant';
|
||||||
|
import { PageNotFoundError } from '@affine/env/constant';
|
||||||
|
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||||
|
import { Button } from '@toeverything/components/button';
|
||||||
|
import {
|
||||||
|
currentPageIdAtom,
|
||||||
|
currentWorkspaceIdAtom,
|
||||||
|
getCurrentStore,
|
||||||
|
} from '@toeverything/infra/atom';
|
||||||
|
import { useAtomValue } from 'jotai/react';
|
||||||
|
import { Provider } from 'jotai/react';
|
||||||
|
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
|
||||||
|
import type React from 'react';
|
||||||
|
import { Component, useEffect } from 'react';
|
||||||
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
RecoverableError,
|
||||||
|
SessionFetchErrorRightAfterLoginOrSignUp,
|
||||||
|
} from '../../unexpected-application-state/errors';
|
||||||
|
import {
|
||||||
|
errorDescription,
|
||||||
|
errorDetailStyle,
|
||||||
|
errorDivider,
|
||||||
|
errorImage,
|
||||||
|
errorLayout,
|
||||||
|
errorRetryButton,
|
||||||
|
errorTitle,
|
||||||
|
} from './affine-error-boundary.css';
|
||||||
|
import errorBackground from './error-status.assets.svg';
|
||||||
|
|
||||||
|
export type AffineErrorBoundaryProps = React.PropsWithChildren;
|
||||||
|
|
||||||
|
type AffineError =
|
||||||
|
| QueryParamError
|
||||||
|
| Unreachable
|
||||||
|
| WorkspaceNotFoundError
|
||||||
|
| PageNotFoundError
|
||||||
|
| Error
|
||||||
|
| SessionFetchErrorRightAfterLoginOrSignUp;
|
||||||
|
|
||||||
|
interface AffineErrorBoundaryState {
|
||||||
|
error: AffineError | null;
|
||||||
|
canRetryRecoveredError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DumpInfo = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
|
||||||
|
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||||
|
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||||
|
const path = location.pathname;
|
||||||
|
const query = useParams();
|
||||||
|
useEffect(() => {
|
||||||
|
console.info('DumpInfo', {
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
currentWorkspaceId,
|
||||||
|
currentPageId,
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
}, [path, query, currentWorkspaceId, currentPageId, metadata]);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AffineErrorBoundary extends Component<
|
||||||
|
AffineErrorBoundaryProps,
|
||||||
|
AffineErrorBoundaryState
|
||||||
|
> {
|
||||||
|
override state: AffineErrorBoundaryState = {
|
||||||
|
error: null,
|
||||||
|
canRetryRecoveredError: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly handleRecoverableRetry = () => {
|
||||||
|
if (this.state.error instanceof RecoverableError) {
|
||||||
|
if (this.state.error.canRetry()) {
|
||||||
|
this.state.error.retry();
|
||||||
|
this.setState({
|
||||||
|
error: this.state.error,
|
||||||
|
canRetryRecoveredError: this.state.error.canRetry(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static getDerivedStateFromError(
|
||||||
|
error: AffineError
|
||||||
|
): AffineErrorBoundaryState {
|
||||||
|
return {
|
||||||
|
error,
|
||||||
|
canRetryRecoveredError:
|
||||||
|
error instanceof RecoverableError ? error.canRetry() : true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
override componentDidCatch(error: AffineError, errorInfo: ErrorInfo) {
|
||||||
|
console.error('Uncaught error:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
override render(): ReactNode {
|
||||||
|
if (this.state.error) {
|
||||||
|
let errorDetail: ReactElement | null = null;
|
||||||
|
const error = this.state.error;
|
||||||
|
if (error instanceof PageNotFoundError) {
|
||||||
|
errorDetail = (
|
||||||
|
<>
|
||||||
|
<h1>Sorry.. there was an error</h1>
|
||||||
|
<>
|
||||||
|
<span> Page error </span>
|
||||||
|
<span>
|
||||||
|
Cannot find page {error.pageId} in workspace{' '}
|
||||||
|
{error.workspace.id}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (error instanceof SessionFetchErrorRightAfterLoginOrSignUp) {
|
||||||
|
const retryButtonDesc = this.state.canRetryRecoveredError
|
||||||
|
? 'Refetch'
|
||||||
|
: 'Reload';
|
||||||
|
errorDetail = (
|
||||||
|
<>
|
||||||
|
<h1 className={errorTitle}>Sorry.. there was an error</h1>
|
||||||
|
<span className={errorDescription}> Fetching session failed </span>
|
||||||
|
<span className={errorDescription}>
|
||||||
|
If you are still experiencing this issue, please{' '}
|
||||||
|
<a
|
||||||
|
style={{ color: 'var(--affine-primary-color)' }}
|
||||||
|
href="https://community.affine.pro"
|
||||||
|
target="__blank"
|
||||||
|
>
|
||||||
|
contact us through the community.
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
className={errorRetryButton}
|
||||||
|
onClick={this.handleRecoverableRetry}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
{retryButtonDesc}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
errorDetail = (
|
||||||
|
<>
|
||||||
|
<h1>Sorry.. there was an error</h1>
|
||||||
|
{error.message ?? error.toString()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={errorLayout}>
|
||||||
|
<div className={errorDetailStyle}>{errorDetail}</div>
|
||||||
|
<span className={errorDivider} />
|
||||||
|
<div
|
||||||
|
className={errorImage}
|
||||||
|
style={{ backgroundImage: `url(${errorBackground})` }}
|
||||||
|
/>
|
||||||
|
<Provider key="JotaiProvider" store={getCurrentStore()}>
|
||||||
|
<DumpInfo />
|
||||||
|
</Provider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import type {
|
|
||||||
QueryParamError,
|
|
||||||
Unreachable,
|
|
||||||
WorkspaceNotFoundError,
|
|
||||||
} from '@affine/env/constant';
|
|
||||||
import { PageNotFoundError } from '@affine/env/constant';
|
|
||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
|
||||||
import {
|
|
||||||
currentPageIdAtom,
|
|
||||||
currentWorkspaceIdAtom,
|
|
||||||
getCurrentStore,
|
|
||||||
} from '@toeverything/infra/atom';
|
|
||||||
import { useAtomValue } from 'jotai/react';
|
|
||||||
import { Provider } from 'jotai/react';
|
|
||||||
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
|
|
||||||
import type React from 'react';
|
|
||||||
import { Component } from 'react';
|
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
|
||||||
export type AffineErrorBoundaryProps = React.PropsWithChildren;
|
|
||||||
|
|
||||||
type AffineError =
|
|
||||||
| QueryParamError
|
|
||||||
| Unreachable
|
|
||||||
| WorkspaceNotFoundError
|
|
||||||
| PageNotFoundError
|
|
||||||
| Error;
|
|
||||||
|
|
||||||
interface AffineErrorBoundaryState {
|
|
||||||
error: AffineError | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DumpInfo = () => {
|
|
||||||
const location = useLocation();
|
|
||||||
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
|
|
||||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
|
||||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
|
||||||
const path = location.pathname;
|
|
||||||
const query = useParams();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
Please copy the following information and send it to the developer.
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
border: '1px solid red',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>path: {path}</div>
|
|
||||||
<div>query: {JSON.stringify(query)}</div>
|
|
||||||
<div>currentWorkspaceId: {currentWorkspaceId}</div>
|
|
||||||
<div>currentPageId: {currentPageId}</div>
|
|
||||||
<div>metadata: {JSON.stringify(metadata)}</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export class AffineErrorBoundary extends Component<
|
|
||||||
AffineErrorBoundaryProps,
|
|
||||||
AffineErrorBoundaryState
|
|
||||||
> {
|
|
||||||
public override state: AffineErrorBoundaryState = {
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static getDerivedStateFromError(
|
|
||||||
error: AffineError
|
|
||||||
): AffineErrorBoundaryState {
|
|
||||||
return { error };
|
|
||||||
}
|
|
||||||
|
|
||||||
public override componentDidCatch(error: AffineError, errorInfo: ErrorInfo) {
|
|
||||||
console.error('Uncaught error:', error, errorInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override render(): ReactNode {
|
|
||||||
if (this.state.error) {
|
|
||||||
let errorDetail: ReactElement | null = null;
|
|
||||||
const error = this.state.error;
|
|
||||||
if (error instanceof PageNotFoundError) {
|
|
||||||
errorDetail = (
|
|
||||||
<>
|
|
||||||
<h1>Sorry.. there was an error</h1>
|
|
||||||
<>
|
|
||||||
<span> Page error </span>
|
|
||||||
<span>
|
|
||||||
Cannot find page {error.pageId} in workspace{' '}
|
|
||||||
{error.workspace.id}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
errorDetail = (
|
|
||||||
<>
|
|
||||||
<h1>Sorry.. there was an error</h1>
|
|
||||||
{error.message ?? error.toString()}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{errorDetail}
|
|
||||||
<Provider key="JotaiProvider" store={getCurrentStore()}>
|
|
||||||
<DumpInfo />
|
|
||||||
</Provider>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<svg width="402" height="178" viewBox="0 0 402 178" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M93.7434 129.308H1L71.7965 15.1021V167.142" stroke="#121212" stroke-width="2" stroke-linecap="square" stroke-linejoin="bevel" />
|
||||||
|
<ellipse cx="71.4426" cy="14.7483" rx="3.89381" ry="3.88938" fill="#121212" />
|
||||||
|
<ellipse cx="93.3894" cy="129.308" rx="3.89381" ry="3.88938" fill="#121212" />
|
||||||
|
<path d="M140.357 27.1235L264.717 151.342" stroke="#E3E2E4" />
|
||||||
|
<rect x="140.392" y="27.1582" width="124.291" height="124.149" stroke="#E3E2E4" />
|
||||||
|
<path d="M264.339 89.4652C264.895 85.8242 265.183 82.0954 265.183 78.2995C265.183 37.7024 232.235 4.79199 191.592 4.79199C150.948 4.79199 118 37.7024 118 78.2995C118 118.897 150.948 151.807 191.592 151.807C195.23 151.807 198.807 151.543 202.304 151.034" stroke="#E3E2E4" />
|
||||||
|
<path d="M202.304 150.964C205.949 151.519 209.682 151.807 213.483 151.807C254.126 151.807 287.074 118.897 287.074 78.2995C287.074 37.7024 254.126 4.79199 213.483 4.79199C172.839 4.79199 139.892 37.7024 139.892 78.2995C139.892 82.0955 140.18 85.8242 140.735 89.4652" stroke="#E3E2E4" />
|
||||||
|
<path d="M140.2 89.4652C139.69 92.9584 139.426 96.5312 139.426 100.166C139.426 140.763 172.374 173.673 213.017 173.673C253.66 173.673 286.608 140.763 286.608 100.166C286.608 59.5686 253.66 26.6582 213.017 26.6582C209.378 26.6582 205.801 26.922 202.304 27.4314" stroke="#E3E2E4" />
|
||||||
|
<path d="M202.304 27.4314C198.807 26.922 195.23 26.6582 191.592 26.6582C150.948 26.6582 118 59.5686 118 100.166C118 140.763 150.948 173.673 191.592 173.673C232.235 173.673 265.183 140.763 265.183 100.166C265.183 96.5312 264.919 92.9584 264.409 89.4652" stroke="#E3E2E4" />
|
||||||
|
<path d="M264.717 27.1235L140.357 151.342" stroke="#E3E2E4" />
|
||||||
|
<path d="M139.892 89.4653H264.717" stroke="#E3E2E4" />
|
||||||
|
<path d="M202.304 26.6582V151.807" stroke="#E3E2E4" />
|
||||||
|
<ellipse cx="264.717" cy="89.4651" rx="3.72614" ry="3.7219" fill="#121212" />
|
||||||
|
<ellipse cx="264.717" cy="27.1233" rx="3.72614" ry="3.7219" fill="#121212" />
|
||||||
|
<ellipse cx="264.717" cy="151.807" rx="3.72614" ry="3.7219" fill="#121212" />
|
||||||
|
<ellipse cx="140.357" cy="151.807" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||||
|
<ellipse cx="202.304" cy="89.4651" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||||
|
<ellipse cx="202.304" cy="27.1233" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||||
|
<ellipse cx="140.357" cy="89.4651" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||||
|
<ellipse cx="140.357" cy="27.1233" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||||
|
<ellipse cx="202.304" cy="151.807" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||||
|
<path d="M401 127.187H308.257L379.053 12.9805V165.02" stroke="#121212" stroke-width="2" stroke-linecap="square" stroke-linejoin="bevel" />
|
||||||
|
<ellipse cx="379.407" cy="127.187" rx="3.89381" ry="3.88938" fill="#121212" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -1,44 +1,107 @@
|
|||||||
import type { DefaultSession } from 'next-auth';
|
import { type User } from '@affine/component/auth-components';
|
||||||
|
import type { DefaultSession, Session } from 'next-auth';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
import { useSession } from 'next-auth/react';
|
import { getSession, useSession } from 'next-auth/react';
|
||||||
export type CheckedUser = {
|
import { useEffect, useReducer } from 'react';
|
||||||
id: string;
|
|
||||||
name: string;
|
import { SessionFetchErrorRightAfterLoginOrSignUp } from '../../unexpected-application-state/errors';
|
||||||
email: string;
|
|
||||||
image: string;
|
export type CheckedUser = User & {
|
||||||
hasPassword: boolean;
|
hasPassword: boolean;
|
||||||
update: ReturnType<typeof useSession>['update'];
|
update: ReturnType<typeof useSession>['update'];
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: Should this namespace be here?
|
|
||||||
declare module 'next-auth' {
|
declare module 'next-auth' {
|
||||||
interface Session {
|
interface Session {
|
||||||
user: {
|
user: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
id: string;
|
id: string;
|
||||||
hasPassword: boolean;
|
hasPassword: boolean;
|
||||||
} & DefaultSession['user'];
|
} & Omit<NonNullable<DefaultSession['user']>, 'name' | 'email'>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateSessionAction =
|
||||||
|
| {
|
||||||
|
type: 'update';
|
||||||
|
payload: Session;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'fetchError';
|
||||||
|
payload: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateSessionReducer(prevState: Session, action: UpdateSessionAction) {
|
||||||
|
const { type, payload } = action;
|
||||||
|
switch (type) {
|
||||||
|
case 'update':
|
||||||
|
return payload;
|
||||||
|
case 'fetchError':
|
||||||
|
return prevState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This hook checks if the user is logged in.
|
* This hook checks if the user is logged in.
|
||||||
* If not, it will throw an error.
|
* If so, the user object will be cached and returned.
|
||||||
|
* If not, and there is no cache, it will throw an error.
|
||||||
|
* If network error or API response error, it will use the cached value.
|
||||||
*/
|
*/
|
||||||
export function useCurrentUser(): CheckedUser {
|
export function useCurrentUser(): CheckedUser {
|
||||||
const { data: session, status, update } = useSession();
|
const { data, update } = useSession();
|
||||||
// If you are seeing this error, it means that you are not logged in.
|
|
||||||
// This should be prohibited in the development environment, please re-write your component logic.
|
|
||||||
if (status === 'unauthenticated') {
|
|
||||||
throw new Error('session.status should be authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = session?.user;
|
const [session, dispatcher] = useReducer(
|
||||||
|
updateSessionReducer,
|
||||||
|
data,
|
||||||
|
firstSession => {
|
||||||
|
if (!firstSession) {
|
||||||
|
// barely possible.
|
||||||
|
// login succeed but the session request failed then.
|
||||||
|
// also need a error boundary to handle this error.
|
||||||
|
throw new SessionFetchErrorRightAfterLoginOrSignUp(
|
||||||
|
'First session should not be null',
|
||||||
|
() => {
|
||||||
|
getSession()
|
||||||
|
.then(session => {
|
||||||
|
if (session) {
|
||||||
|
dispatcher({
|
||||||
|
type: 'update',
|
||||||
|
payload: session,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return firstSession;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
dispatcher({
|
||||||
|
type: 'update',
|
||||||
|
payload: data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatcher({
|
||||||
|
type: 'fetchError',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, update]);
|
||||||
|
|
||||||
|
const user = session.user;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user?.id ?? 'REPLACE_ME_DEFAULT_ID',
|
id: user.id,
|
||||||
name: user?.name ?? 'REPLACE_ME_DEFAULT_NAME',
|
name: user.name,
|
||||||
email: user?.email ?? 'REPLACE_ME_DEFAULT_EMAIL',
|
email: user.email,
|
||||||
image: user?.image ?? 'REPLACE_ME_DEFAULT_URL',
|
image: user.image,
|
||||||
hasPassword: user?.hasPassword ?? false,
|
hasPassword: user?.hasPassword ?? false,
|
||||||
update,
|
update,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { getUIAdapter } from '../../adapters/workspace';
|
|||||||
import { setPageModeAtom } from '../../atoms';
|
import { setPageModeAtom } from '../../atoms';
|
||||||
import { collectionsCRUDAtom } from '../../atoms/collections';
|
import { collectionsCRUDAtom } from '../../atoms/collections';
|
||||||
import { currentModeAtom } from '../../atoms/mode';
|
import { currentModeAtom } from '../../atoms/mode';
|
||||||
|
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||||
import { WorkspaceHeader } from '../../components/workspace-header';
|
import { WorkspaceHeader } from '../../components/workspace-header';
|
||||||
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands';
|
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands';
|
||||||
import { useCurrentSyncEngineStatus } from '../../hooks/current/use-current-sync-engine';
|
import { useCurrentSyncEngineStatus } from '../../hooks/current/use-current-sync-engine';
|
||||||
@@ -214,5 +215,9 @@ export const Component = () => {
|
|||||||
}
|
}
|
||||||
}, [params, setContentLayout, setCurrentPageId, setCurrentWorkspaceId]);
|
}, [params, setContentLayout, setCurrentPageId, setCurrentWorkspaceId]);
|
||||||
|
|
||||||
return <DetailPage />;
|
return (
|
||||||
|
<AffineErrorBoundary>
|
||||||
|
<DetailPage />
|
||||||
|
</AffineErrorBoundary>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+5
@@ -8,3 +8,8 @@ declare module '*.md' {
|
|||||||
const text: string;
|
const text: string;
|
||||||
export default text;
|
export default text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '*.assets.svg' {
|
||||||
|
const url: string;
|
||||||
|
export default url;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export abstract class RecoverableError extends Error {
|
||||||
|
protected ttl = 3;
|
||||||
|
|
||||||
|
canRetry(): boolean {
|
||||||
|
return this.ttl > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract retry(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the first session request failed after login or signup succeed.
|
||||||
|
// should give a hint to the user to refetch the session.
|
||||||
|
export class SessionFetchErrorRightAfterLoginOrSignUp extends RecoverableError {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
private readonly onRetry: () => void
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
retry(): void {
|
||||||
|
if (this.ttl <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.onRetry();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Retry error', e);
|
||||||
|
} finally {
|
||||||
|
this.ttl--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user