Skip to content

Commit

Permalink
feat(core): open in app for self-hosted
Browse files Browse the repository at this point in the history
  • Loading branch information
pengx17 committed Dec 22, 2024
1 parent ea6a056 commit 74d71b7
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 39 deletions.
18 changes: 13 additions & 5 deletions packages/frontend/core/src/components/sign-in/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AddSelfhostedStep } from './add-selfhosted';
import { SignInStep } from './sign-in';
import { SignInWithEmailStep } from './sign-in-with-email';
import { SignInWithPasswordStep } from './sign-in-with-password';
import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session';

export type SignInStep =
| 'signIn'
Expand All @@ -22,11 +23,13 @@ export interface SignInState {
}

export const SignInPanel = ({
onClose,
onSkip,
server: initialServerBaseUrl,
initStep,
onAuthenticated,
}: {
onClose: () => void;
onAuthenticated?: (status: AuthSessionStatus) => void;
onSkip: () => void;
server?: string;
initStep?: SignInStep | undefined;
}) => {
Expand All @@ -47,18 +50,23 @@ export const SignInPanel = ({
return (
<FrameworkScope scope={server.scope}>
{step === 'signIn' ? (
<SignInStep state={state} changeState={setState} close={onClose} />
<SignInStep
state={state}
changeState={setState}
onSkip={onSkip}
onAuthenticated={onAuthenticated}
/>
) : step === 'signInWithEmail' ? (
<SignInWithEmailStep
state={state}
changeState={setState}
close={onClose}
onAuthenticated={onAuthenticated}
/>
) : step === 'signInWithPassword' ? (
<SignInWithPasswordStep
state={state}
changeState={setState}
close={onClose}
onAuthenticated={onAuthenticated}
/>
) : step === 'addSelfhosted' ? (
<AddSelfhostedStep state={state} changeState={setState} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ import {
import type { SignInState } from '.';
import { Captcha } from './captcha';
import * as style from './style.css';
import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session';

export const SignInWithEmailStep = ({
state,
changeState,
close,
onAuthenticated,
}: {
state: SignInState;
changeState: Dispatch<SetStateAction<SignInState>>;
close: () => void;
onAuthenticated?: (status: AuthSessionStatus) => void;
}) => {
const initialSent = useRef(false);
const [resendCountDown, setResendCountDown] = useState(0);
Expand Down Expand Up @@ -66,13 +67,13 @@ export const SignInWithEmailStep = ({

useEffect(() => {
if (loginStatus === 'authenticated') {
close();
notify.success({
title: t['com.affine.auth.toast.title.signed-in'](),
message: t['com.affine.auth.toast.message.signed-in'](),
});
}
}, [close, loginStatus, t]);
onAuthenticated?.(loginStatus);
}, [loginStatus, onAuthenticated, t]);

const sendEmail = useAsyncCallback(async () => {
if (isSending || (!verifyToken && needCaptcha)) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ import { useCallback, useEffect, useState } from 'react';
import type { SignInState } from '.';
import { Captcha } from './captcha';
import * as styles from './style.css';
import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session';

export const SignInWithPasswordStep = ({
state,
changeState,
close,
onAuthenticated,
}: {
state: SignInState;
changeState: Dispatch<SetStateAction<SignInState>>;
close: () => void;
onAuthenticated?: (status: AuthSessionStatus) => void;
}) => {
const t = useI18n();
const authService = useService(AuthService);
Expand Down Expand Up @@ -62,13 +63,13 @@ export const SignInWithPasswordStep = ({

useEffect(() => {
if (loginStatus === 'authenticated') {
close();
notify.success({
title: t['com.affine.auth.toast.title.signed-in'](),
message: t['com.affine.auth.toast.message.signed-in'](),
});
}
}, [close, loginStatus, t]);
onAuthenticated?.(loginStatus);
}, [loginStatus, onAuthenticated, t]);

const onSignIn = useAsyncCallback(async () => {
if (isLoading || (!verifyToken && needCaptcha)) return;
Expand Down
13 changes: 8 additions & 5 deletions packages/frontend/core/src/components/sign-in/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {

import type { SignInState } from '.';
import * as style from './style.css';
import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session';

const emailRegex =
/^(?:(?:[^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(?:(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|((?:[a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
Expand All @@ -33,11 +34,13 @@ function validateEmail(email: string) {
export const SignInStep = ({
state,
changeState,
close,
onSkip,
onAuthenticated,
}: {
state: SignInState;
changeState: Dispatch<SetStateAction<SignInState>>;
close: () => void;
onSkip: () => void;
onAuthenticated?: (status: AuthSessionStatus) => void;
}) => {
const t = useI18n();
const serverService = useService(ServerService);
Expand All @@ -64,13 +67,13 @@ export const SignInStep = ({

useEffect(() => {
if (loginStatus === 'authenticated') {
close();
notify.success({
title: t['com.affine.auth.toast.title.signed-in'](),
message: t['com.affine.auth.toast.message.signed-in'](),
});
}
}, [close, loginStatus, t]);
onAuthenticated?.(loginStatus);
}, [loginStatus, onAuthenticated, t]);

const onContinue = useAsyncCallback(async () => {
if (!validateEmail(email)) {
Expand Down Expand Up @@ -208,7 +211,7 @@ export const SignInStep = ({
</div>
<Button
variant="plain"
onClick={() => close()}
onClick={onSkip}
className={style.skipLink}
suffix={<ArrowRightBigIcon className={style.skipLinkIcon} />}
>
Expand Down
13 changes: 12 additions & 1 deletion packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { Modal } from '@affine/component';
import { SignInPanel, type SignInStep } from '@affine/core/components/sign-in';
import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session';
import type {
DialogComponentProps,
GLOBAL_DIALOG_SCHEMA,
} from '@affine/core/modules/dialogs';
import { useCallback } from 'react';
export const SignInDialog = ({
close,
server: initialServerBaseUrl,
step,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['sign-in']>) => {
const onAuthenticated = useCallback(
(status: AuthSessionStatus) => {
if (status === 'authenticated') {
close();
}
},
[close]
);
return (
<Modal
open
Expand All @@ -21,7 +31,8 @@ export const SignInDialog = ({
}}
>
<SignInPanel
onClose={close}
onSkip={close}
onAuthenticated={onAuthenticated}
server={initialServerBaseUrl}
initStep={step as SignInStep}
/>
Expand Down
48 changes: 32 additions & 16 deletions packages/frontend/core/src/desktop/pages/auth/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,29 @@ import { notify } from '@affine/component';
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
import { SignInPageContainer } from '@affine/component/auth-components';
import { SignInPanel } from '@affine/core/components/sign-in';
import { AuthService } from '@affine/core/modules/cloud';
import { useI18n } from '@affine/i18n';
import { useService } from '@toeverything/infra';
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useNavigate, useSearchParams } from 'react-router-dom';

import {
RouteLogic,
useNavigateHelper,
} from '../../../components/hooks/use-navigate-helper';
import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session';

export const SignIn = ({
redirectUrl: redirectUrlFromProps,
}: {
redirectUrl?: string;
}) => {
const t = useI18n();
const session = useService(AuthService).session;
const navigate = useNavigate();
const { jumpToIndex } = useNavigateHelper();
const [searchParams] = useSearchParams();
const redirectUrl = redirectUrlFromProps ?? searchParams.get('redirect_uri');

const server = searchParams.get('server') ?? undefined;
const error = searchParams.get('error');

useEffect(() => {
Expand All @@ -36,22 +36,38 @@ export const SignIn = ({
}
}, [error, t]);

const handleClose = () => {
if (session.status$.value === 'authenticated' && redirectUrl) {
navigate(redirectUrl, {
replace: true,
});
} else {
jumpToIndex(RouteLogic.REPLACE, {
search: searchParams.toString(),
});
}
};
const handleClose = useCallback(() => {
jumpToIndex(RouteLogic.REPLACE, {
search: searchParams.toString(),
});
}, [jumpToIndex, searchParams]);

const handleAuthenticated = useCallback(
(status: AuthSessionStatus) => {
if (status === 'authenticated') {
if (redirectUrl) {
navigate(redirectUrl, {
replace: true,
});
} else {
handleClose();
}
}
},
[handleClose, navigate, redirectUrl]
);

const initStep = server ? 'addSelfhosted' : 'signIn';

return (
<SignInPageContainer>
<div style={{ maxWidth: '400px', width: '100%' }}>
<SignInPanel onClose={handleClose} />
<SignInPanel
onSkip={handleClose}
onAuthenticated={handleAuthenticated}
initStep={initStep}
server={server}
/>
</div>
</SignInPageContainer>
);
Expand Down
20 changes: 19 additions & 1 deletion packages/frontend/core/src/desktop/pages/workspace/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@ import {
} from '@toeverything/infra';
import type { PropsWithChildren, ReactElement } from 'react';
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { matchPath, useLocation, useParams } from 'react-router-dom';
import {
matchPath,
useLocation,
useParams,
useSearchParams,
} from 'react-router-dom';

import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
import { WorkbenchRoot } from '../../../modules/workbench';
import { AppContainer } from '../../components/app-container';
import { PageNotFound } from '../404';
import { WorkspaceLayout } from './layouts/workspace-layout';
import { SharePage } from './share/share-page';
import { SignIn } from '../auth/sign-in';

declare global {
/**
Expand All @@ -49,6 +55,7 @@ export const Component = (): ReactElement => {

const params = useParams();
const location = useLocation();
const [searchParams] = useSearchParams();

// check if we are in detail doc route, if so, maybe render share page
const detailDocRoute = useMemo(() => {
Expand Down Expand Up @@ -110,6 +117,17 @@ export const Component = (): ReactElement => {
}, [listLoading, meta, workspaceNotFound, workspacesService]);

if (workspaceNotFound) {
if (BUILD_CONFIG.isElectron && searchParams.get('server')) {
const redirectUrl = window.location.href.replace(
window.location.origin,
''
);
return (
<AffineOtherPageLayout>
<SignIn redirectUrl={redirectUrl} />
</AffineOtherPageLayout>
);
}
if (detailDocRoute) {
return (
<SharePage
Expand Down
17 changes: 16 additions & 1 deletion packages/frontend/core/src/mobile/components/sign-in/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { SignInPanel } from '@affine/core/components/sign-in';

Check failure on line 1 in packages/frontend/core/src/mobile/components/sign-in/index.tsx

View workflow job for this annotation

GitHub Actions / Lint

Run autofix to sort these imports!

import { MobileSignInLayout } from './layout';
import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session';
import { useCallback } from 'react';

export const MobileSignInPanel = ({
onClose,
Expand All @@ -9,9 +11,22 @@ export const MobileSignInPanel = ({
onClose: () => void;
server?: string;
}) => {
const onAuthenticated = useCallback(
(status: AuthSessionStatus) => {
if (status === 'authenticated') {
onClose();
}
},
[onClose]
);

return (
<MobileSignInLayout>
<SignInPanel onClose={onClose} server={server} />
<SignInPanel
onSkip={onClose}
onAuthenticated={onAuthenticated}
server={server}
/>
</MobileSignInLayout>
);
};
5 changes: 5 additions & 0 deletions packages/frontend/core/src/modules/cloud/entities/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export interface AuthSessionAuthenticated {
session: AuthSessionInfo;
}

export type AuthSessionStatus = (
| AuthSessionUnauthenticated
| AuthSessionAuthenticated
)['status'];

export class AuthSession extends Entity {
session$: LiveData<AuthSessionUnauthenticated | AuthSessionAuthenticated> =
LiveData.from(this.store.watchCachedAuthSession(), null).map(session =>
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend/core/src/modules/open-in-app/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export const getOpenUrlInDesktopAppLink = (
if (newTab) {
params.set('new-tab', '1');
}
if (environment.isSelfHosted) {
// assume self-hosted server is the current origin
params.set('server', location.origin);
}
return new URL(
`${scheme}://${urlObject.host}${urlObject.pathname}?${params.toString()}#${urlObject.hash}`
).toString();
Expand Down
Loading

0 comments on commit 74d71b7

Please sign in to comment.