mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-29 16:19:43 +08:00
feat: add open app route (#3899)
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
@@ -2,7 +2,6 @@ import {
|
||||
AuthModal as AuthModalBase,
|
||||
type AuthModalProps as AuthModalBaseProps,
|
||||
} from '@affine/component/auth-components';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import { type FC, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
@@ -80,15 +79,6 @@ export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
|
||||
}
|
||||
}, [open, setAuthEmail, setAuthStore]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDesktop) {
|
||||
return window.events?.ui.onFinishLogin(() => {
|
||||
setOpen(false);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}, [setOpen]);
|
||||
|
||||
const onSignedIn = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Modal, ModalWrapper } from '@affine/component';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const DesktopLoginModal = ({
|
||||
signingEmail,
|
||||
}: {
|
||||
signingEmail?: string;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<Modal open={!!signingEmail}>
|
||||
<ModalWrapper className={styles.root}>
|
||||
<div className={styles.title}>
|
||||
{t['com.affine.auth.desktop.signing.in']()}
|
||||
</div>
|
||||
|
||||
<Trans i18nKey="com.affine.auth.desktop.signing.in.message">
|
||||
Signing in with account {signingEmail}
|
||||
</Trans>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
padding: '20px 24px',
|
||||
});
|
||||
|
||||
export const title = style({
|
||||
fontSize: 'var(--affine-font-h-6)',
|
||||
fontWeight: 600,
|
||||
marginBottom: 12,
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ContactWithUsIcon } from '@blocksuite/icons';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
|
||||
import { useCurrenLoginStatus } from '../../../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import { AccountSetting } from './account-setting';
|
||||
import {
|
||||
GeneralSetting,
|
||||
@@ -39,7 +39,7 @@ export const SettingModal = ({
|
||||
onSettingClick,
|
||||
}: SettingModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
|
||||
const generalSettingList = useGeneralSettingList();
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from 'react';
|
||||
|
||||
import { authAtom } from '../../../../atoms';
|
||||
import { useCurrenLoginStatus } from '../../../../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status';
|
||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import type {
|
||||
@@ -113,7 +113,7 @@ export const SettingSidebar = ({
|
||||
onAccountSettingClick: () => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
return (
|
||||
<div className={settingSlideBar} data-testid="settings-sidebar">
|
||||
<div className={sidebarTitle}>{t['Settings']()}</div>
|
||||
|
||||
@@ -3,13 +3,13 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||
import { signIn } from 'next-auth/react';
|
||||
|
||||
import { useCurrenLoginStatus } from '../../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentLoginStatus } from '../../hooks/affine/use-current-login-status';
|
||||
import { useCurrentUser } from '../../hooks/affine/use-current-user';
|
||||
import { StyledSignInButton } from '../pure/footer/styles';
|
||||
|
||||
export const LoginCard = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
if (loginStatus === 'authenticated') {
|
||||
return <UserCard />;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { type CSSProperties, type FC, forwardRef, useCallback } from 'react';
|
||||
|
||||
import { useCurrenLoginStatus } from '../../../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
// import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||
import { stringToColour } from '../../../utils';
|
||||
import { StyledFooter, StyledSignInButton } from './styles';
|
||||
export const Footer: FC = () => {
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
|
||||
// const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
return (
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
export function useCurrenLoginStatus():
|
||||
export function useCurrentLoginStatus():
|
||||
| 'authenticated'
|
||||
| 'unauthenticated'
|
||||
| 'loading' {
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
SignInSuccessPage,
|
||||
SignUpPage,
|
||||
} from '@affine/component/auth-components';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { changeEmailMutation, changePasswordMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import type { ReactElement } from 'react';
|
||||
@@ -13,7 +12,7 @@ import { useCallback } from 'react';
|
||||
import { type LoaderFunction, redirect, useParams } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
import { useCurrentUser } from '../hooks/affine/use-current-user';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
|
||||
@@ -58,11 +57,7 @@ export const AuthPage = (): ReactElement | null => {
|
||||
[changePassword, user.id]
|
||||
);
|
||||
const onOpenAffine = useCallback(() => {
|
||||
if (isDesktop) {
|
||||
window.apis.ui.handleFinishLogin();
|
||||
} else {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}, [jumpToIndex]);
|
||||
|
||||
switch (authType) {
|
||||
@@ -119,7 +114,7 @@ export const loader: LoaderFunction = async args => {
|
||||
return null;
|
||||
};
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const { jumpToExpired } = useNavigateHelper();
|
||||
|
||||
if (loginStatus === 'unauthenticated') {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useCallback, useEffect } from 'react';
|
||||
import { type LoaderFunction, redirect, useLoaderData } from 'react-router-dom';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { useAppHelper } from '../hooks/use-workspaces';
|
||||
|
||||
@@ -45,7 +45,7 @@ export const loader: LoaderFunction = async args => {
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const { jumpToSignIn } = useNavigateHelper();
|
||||
const { addCloudWorkspace } = useAppHelper();
|
||||
const { jumpToSubPath } = useNavigateHelper();
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const affineLogo = style({
|
||||
color: 'inherit',
|
||||
});
|
||||
|
||||
export const topNav = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '16px 120px',
|
||||
});
|
||||
|
||||
export const topNavLinks = style({
|
||||
display: 'flex',
|
||||
columnGap: 4,
|
||||
});
|
||||
|
||||
export const topNavLink = style({
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
padding: '4px 18px',
|
||||
});
|
||||
|
||||
export const tryAgainLink = style({
|
||||
color: 'var(--affine-link-color)',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
});
|
||||
|
||||
export const centerContent = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginTop: 40,
|
||||
});
|
||||
|
||||
export const prompt = style({
|
||||
marginTop: 20,
|
||||
marginBottom: 12,
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import * as styles from './open-app.css';
|
||||
|
||||
let lastOpened = '';
|
||||
|
||||
const appSchemas = z.enum([
|
||||
'affine',
|
||||
'affine-canary',
|
||||
'affine-beta',
|
||||
'affine-internal',
|
||||
'affine-dev',
|
||||
]);
|
||||
|
||||
const appChannelSchema = z.enum(['stable', 'canary', 'beta', 'internal']);
|
||||
|
||||
type Schema = z.infer<typeof appSchemas>;
|
||||
type Channel = z.infer<typeof appChannelSchema>;
|
||||
|
||||
const schemaToChanel = {
|
||||
affine: 'stable',
|
||||
'affine-canary': 'canary',
|
||||
'affine-beta': 'beta',
|
||||
'affine-internal': 'internal',
|
||||
'affine-dev': 'canary', // dev does not have a dedicated app. use canary as the placeholder.
|
||||
} as Record<Schema, Channel>;
|
||||
|
||||
const appIconMap = {
|
||||
stable: '/imgs/app-icon-stable.ico',
|
||||
canary: '/imgs/app-icon-canary.ico',
|
||||
beta: '/imgs/app-icon-beta.ico',
|
||||
internal: '/imgs/app-icon-internal.ico',
|
||||
} satisfies Record<Channel, string>;
|
||||
|
||||
const appNames = {
|
||||
stable: 'AFFiNE',
|
||||
canary: 'AFFiNE Canary',
|
||||
beta: 'AFFiNE Beta',
|
||||
internal: 'AFFiNE Internal',
|
||||
} satisfies Record<Channel, string>;
|
||||
|
||||
export const Component = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [params] = useSearchParams();
|
||||
const urlToOpen = useMemo(() => params.get('url'), [params]);
|
||||
const autoOpen = useMemo(() => params.get('open') !== 'false', [params]);
|
||||
const channel = useMemo(() => {
|
||||
const urlObj = new URL(urlToOpen || '');
|
||||
const maybeSchema = appSchemas.safeParse(urlObj.protocol.replace(':', ''));
|
||||
return schemaToChanel[maybeSchema.success ? maybeSchema.data : 'affine'];
|
||||
}, [urlToOpen]);
|
||||
|
||||
const appIcon = appIconMap[channel];
|
||||
const appName = appNames[channel];
|
||||
|
||||
const openDownloadLink = useCallback(() => {
|
||||
const url = `https://affine.pro/download?channel=${channel}`;
|
||||
open(url, '_blank');
|
||||
}, [channel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!urlToOpen || lastOpened === urlToOpen || !autoOpen) {
|
||||
return;
|
||||
}
|
||||
lastOpened = urlToOpen;
|
||||
open(urlToOpen, '_blank');
|
||||
}, [urlToOpen, autoOpen]);
|
||||
|
||||
if (urlToOpen) {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.topNav}>
|
||||
<a
|
||||
href="https://affine.pro"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.affineLogo}
|
||||
>
|
||||
<Logo1Icon width={24} height={24} />
|
||||
</a>
|
||||
|
||||
<div className={styles.topNavLinks}>
|
||||
<a
|
||||
href="https://affine.pro"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Official Website
|
||||
</a>
|
||||
<a
|
||||
href="https://community.affine.pro/home"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
AFFiNE Community
|
||||
</a>
|
||||
<a
|
||||
href="https://affine.pro/blog"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="https://affine.pro/about-us"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Contact us
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Button onClick={openDownloadLink}>
|
||||
{t['com.affine.auth.open.affine.download-app']()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.centerContent}>
|
||||
<img src={appIcon} alt={appName} width={120} height={120} />
|
||||
|
||||
<div className={styles.prompt}>
|
||||
<Trans i18nKey="com.affine.auth.open.affine.prompt">
|
||||
Open {appName} app now
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<a
|
||||
className={styles.tryAgainLink}
|
||||
href={urlToOpen}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t['com.affine.auth.open.affine.try-again']()}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import { AuthPanel } from '../components/affine/auth';
|
||||
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
|
||||
interface LocationState {
|
||||
state: {
|
||||
@@ -18,7 +18,7 @@ export const Component = () => {
|
||||
{ state, email = '', emailType = 'changePassword', onceSignedIn },
|
||||
setAuthAtom,
|
||||
] = useAtom(authAtom);
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const location = useLocation() as LocationState;
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
@@ -8,7 +11,14 @@ import {
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import { lazy, Suspense, useCallback, useTransition } from 'react';
|
||||
import {
|
||||
lazy,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
useTransition,
|
||||
} from 'react';
|
||||
|
||||
import type { SettingAtom } from '../atoms';
|
||||
import {
|
||||
@@ -32,6 +42,12 @@ const Auth = lazy(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
const DesktopLogin = lazy(() =>
|
||||
import('../components/affine/desktop-login-modal').then(module => ({
|
||||
default: module.DesktopLoginModal,
|
||||
}))
|
||||
);
|
||||
|
||||
const WorkspaceListModal = lazy(() =>
|
||||
import('../components/pure/workspace-list-modal').then(module => ({
|
||||
default: module.WorkspaceListModal,
|
||||
@@ -129,6 +145,49 @@ export const AuthModal = (): ReactElement => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DesktopLoginModal = (): ReactElement => {
|
||||
const [signingEmail, setSigningEmail] = useState<string>();
|
||||
const setAuthAtom = useSetAtom(authAtom);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
// hack for closing the potentially opened auth modal
|
||||
const closeAuthModal = useCallback(() => {
|
||||
setAuthAtom(prev => ({ ...prev, openModal: false }));
|
||||
}, [setAuthAtom]);
|
||||
|
||||
useEffect(() => {
|
||||
return window.events?.ui.onStartLogin(opts => {
|
||||
setSigningEmail(opts.email);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return window.events?.ui.onFinishLogin(({ success, email }) => {
|
||||
if (email !== signingEmail) {
|
||||
return;
|
||||
}
|
||||
setSigningEmail(undefined);
|
||||
closeAuthModal();
|
||||
if (success) {
|
||||
pushNotification({
|
||||
title: t['com.affine.auth.toast.title.signed-in'](),
|
||||
message: t['com.affine.auth.toast.message.signed-in'](),
|
||||
type: 'success',
|
||||
});
|
||||
} else {
|
||||
pushNotification({
|
||||
title: t['com.affine.auth.toast.title.failed'](),
|
||||
message: t['com.affine.auth.toast.message.failed'](),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [closeAuthModal, pushNotification, signingEmail, t]);
|
||||
|
||||
return <DesktopLogin signingEmail={signingEmail} />;
|
||||
};
|
||||
|
||||
export function CurrentWorkspaceModals() {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const [openDisableCloudAlertModal, setOpenDisableCloudAlertModal] = useAtom(
|
||||
@@ -263,6 +322,7 @@ export const AllWorkspaceModals = (): ReactElement => {
|
||||
<Suspense>
|
||||
<AuthModal />
|
||||
</Suspense>
|
||||
{isDesktop && <DesktopLoginModal />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -48,6 +48,10 @@ export const routes = [
|
||||
path: '/signIn',
|
||||
lazy: () => import('./pages/sign-in'),
|
||||
},
|
||||
{
|
||||
path: '/open-app',
|
||||
lazy: () => import('./pages/open-app'),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
lazy: () => import('./pages/404'),
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import type { App } from 'electron';
|
||||
|
||||
import { buildType, isDev } from './config';
|
||||
import { logger } from './logger';
|
||||
import { handleOpenUrlInPopup } from './main-window';
|
||||
import {
|
||||
handleOpenUrlInHiddenWindow,
|
||||
restoreOrCreateWindow,
|
||||
} from './main-window';
|
||||
import { uiSubjects } from './ui';
|
||||
|
||||
let protocol = buildType === 'stable' ? 'affine' : `affine-${buildType}`;
|
||||
if (isDev) {
|
||||
@@ -10,7 +16,16 @@ if (isDev) {
|
||||
}
|
||||
|
||||
export function setupDeepLink(app: App) {
|
||||
app.setAsDefaultProtocolClient(protocol);
|
||||
if (process.defaultApp) {
|
||||
if (process.argv.length >= 2) {
|
||||
app.setAsDefaultProtocolClient(protocol, process.execPath, [
|
||||
path.resolve(process.argv[1]),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
app.setAsDefaultProtocolClient(protocol);
|
||||
}
|
||||
|
||||
app.on('open-url', (event, url) => {
|
||||
if (url.startsWith(`${protocol}://`)) {
|
||||
event.preventDefault();
|
||||
@@ -19,17 +34,72 @@ export function setupDeepLink(app: App) {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// on windows & linux, we need to listen for the second-instance event
|
||||
app.on('second-instance', (event, commandLine) => {
|
||||
restoreOrCreateWindow()
|
||||
.then(() => {
|
||||
const url = commandLine.pop();
|
||||
if (url?.startsWith(`${protocol}://`)) {
|
||||
event.preventDefault();
|
||||
handleAffineUrl(url).catch(e => {
|
||||
logger.error('failed to handle affine url', e);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(e => console.error('Failed to restore or create window:', e));
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAffineUrl(url: string) {
|
||||
logger.info('open affine url', url);
|
||||
const urlObj = new URL(url);
|
||||
if (urlObj.hostname === 'open-url') {
|
||||
logger.info('handle affine schema action', urlObj.hostname);
|
||||
// handle more actions here
|
||||
// hostname is the action name
|
||||
if (urlObj.hostname === 'sign-in') {
|
||||
const urlToOpen = urlObj.search.slice(1);
|
||||
if (urlToOpen) {
|
||||
handleOpenUrlInPopup(urlToOpen).catch(e => {
|
||||
logger.error('failed to open url in popup', e);
|
||||
});
|
||||
await handleSignIn(urlToOpen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: move to another place?
|
||||
async function handleSignIn(url: string) {
|
||||
if (url) {
|
||||
try {
|
||||
const mainWindow = await restoreOrCreateWindow();
|
||||
mainWindow.show();
|
||||
const urlObj = new URL(url);
|
||||
const email = urlObj.searchParams.get('email');
|
||||
|
||||
if (!email) {
|
||||
logger.error('no email in url', url);
|
||||
return;
|
||||
}
|
||||
|
||||
uiSubjects.onStartLogin.next({
|
||||
email,
|
||||
});
|
||||
const window = await handleOpenUrlInHiddenWindow(url);
|
||||
logger.info('opened url in hidden window', window.webContents.getURL());
|
||||
// check path
|
||||
// - if path === /auth/{signIn,signUp}, we know sign in succeeded
|
||||
// - if path === expired, we know sign in failed
|
||||
const finalUrl = new URL(window.webContents.getURL());
|
||||
console.log('final url', finalUrl);
|
||||
// hack: wait for the hidden window to send broadcast message to the main window
|
||||
// that's how next-auth works for cross-tab communication
|
||||
setTimeout(() => {
|
||||
window.destroy();
|
||||
}, 3000);
|
||||
uiSubjects.onFinishLogin.next({
|
||||
success: ['/auth/signIn', '/auth/signUp'].includes(finalUrl.pathname),
|
||||
email,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('failed to open url in popup', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,12 +33,6 @@ if (!isSingleInstance) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
app.on('second-instance', () => {
|
||||
restoreOrCreateWindow().catch(e =>
|
||||
console.error('Failed to restore or create window:', e)
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Shout down background process if all windows was closed
|
||||
*/
|
||||
|
||||
@@ -95,7 +95,13 @@ async function createWindow() {
|
||||
|
||||
browserWindow.on('close', e => {
|
||||
e.preventDefault();
|
||||
browserWindow.destroy();
|
||||
// close and destroy all windows
|
||||
BrowserWindow.getAllWindows().forEach(w => {
|
||||
if (!w.isDestroyed()) {
|
||||
w.close();
|
||||
w.destroy();
|
||||
}
|
||||
});
|
||||
helperConnectionUnsub?.();
|
||||
// TODO: gracefully close the app, for example, ask user to save unsaved changes
|
||||
});
|
||||
@@ -123,44 +129,12 @@ async function createWindow() {
|
||||
|
||||
// singleton
|
||||
let browserWindow: BrowserWindow | undefined;
|
||||
let popup: BrowserWindow | undefined;
|
||||
|
||||
function createPopupWindow() {
|
||||
if (!popup || popup?.isDestroyed()) {
|
||||
const mainExposedMeta = getExposedMeta();
|
||||
popup = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 600,
|
||||
alwaysOnTop: true,
|
||||
resizable: false,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, './preload.js'),
|
||||
additionalArguments: [
|
||||
`--main-exposed-meta=` + JSON.stringify(mainExposedMeta),
|
||||
// popup window does not need helper process, right?
|
||||
],
|
||||
},
|
||||
});
|
||||
popup.on('close', e => {
|
||||
e.preventDefault();
|
||||
popup?.destroy();
|
||||
popup = undefined;
|
||||
});
|
||||
browserWindow?.webContents.once('did-finish-load', () => {
|
||||
closePopup();
|
||||
});
|
||||
}
|
||||
return popup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore existing BrowserWindow or Create new BrowserWindow
|
||||
*/
|
||||
export async function restoreOrCreateWindow() {
|
||||
browserWindow =
|
||||
browserWindow || BrowserWindow.getAllWindows().find(w => !w.isDestroyed());
|
||||
|
||||
if (browserWindow === undefined) {
|
||||
if (!browserWindow || browserWindow.isDestroyed()) {
|
||||
browserWindow = await createWindow();
|
||||
}
|
||||
|
||||
@@ -172,17 +146,29 @@ export async function restoreOrCreateWindow() {
|
||||
return browserWindow;
|
||||
}
|
||||
|
||||
export async function handleOpenUrlInPopup(url: string) {
|
||||
const popup = createPopupWindow();
|
||||
await popup.loadURL(url);
|
||||
}
|
||||
|
||||
export function closePopup() {
|
||||
if (!popup?.isDestroyed()) {
|
||||
popup?.close();
|
||||
popup?.destroy();
|
||||
popup = undefined;
|
||||
}
|
||||
export async function handleOpenUrlInHiddenWindow(url: string) {
|
||||
const mainExposedMeta = getExposedMeta();
|
||||
const win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, './preload.js'),
|
||||
additionalArguments: [
|
||||
`--main-exposed-meta=` + JSON.stringify(mainExposedMeta),
|
||||
// popup window does not need helper process, right?
|
||||
],
|
||||
},
|
||||
show: false,
|
||||
});
|
||||
win.on('close', e => {
|
||||
e.preventDefault();
|
||||
if (!win.isDestroyed()) {
|
||||
win.destroy();
|
||||
}
|
||||
});
|
||||
logger.info('loading page at', url);
|
||||
await win.loadURL(url);
|
||||
return win;
|
||||
}
|
||||
|
||||
export function reloadApp() {
|
||||
|
||||
@@ -5,10 +5,18 @@ import { uiSubjects } from './subject';
|
||||
* Events triggered by application menu
|
||||
*/
|
||||
export const uiEvents = {
|
||||
onFinishLogin: (fn: () => void) => {
|
||||
onFinishLogin: (
|
||||
fn: (result: { success: boolean; email: string }) => void
|
||||
) => {
|
||||
const sub = uiSubjects.onFinishLogin.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
onStartLogin: (fn: (opts: { email: string }) => void) => {
|
||||
const sub = uiSubjects.onStartLogin.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
} satisfies Record<string, MainEventRegister>;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { app, BrowserWindow, nativeTheme } from 'electron';
|
||||
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { closePopup } from '../main-window';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import { getGoogleOauthCode } from './google-auth';
|
||||
import { uiSubjects } from './subject';
|
||||
|
||||
export const uiHandlers = {
|
||||
handleThemeChange: async (_, theme: (typeof nativeTheme)['themeSource']) => {
|
||||
@@ -38,10 +36,6 @@ export const uiHandlers = {
|
||||
handleCloseApp: async () => {
|
||||
app.quit();
|
||||
},
|
||||
handleFinishLogin: async () => {
|
||||
closePopup();
|
||||
uiSubjects.onFinishLogin.next();
|
||||
},
|
||||
getGoogleOauthCode: async () => {
|
||||
return getGoogleOauthCode();
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export const uiSubjects = {
|
||||
onFinishLogin: new Subject<void>(),
|
||||
onStartLogin: new Subject<{ email: string }>(),
|
||||
onFinishLogin: new Subject<{ success: boolean; email: string }>(),
|
||||
};
|
||||
|
||||
@@ -25,9 +25,14 @@ function getSchemaFromCallbackUrl(origin: string, callbackUrl: string) {
|
||||
return searchParams.has('schema') ? searchParams.get('schema') : null;
|
||||
}
|
||||
|
||||
function wrapUrlWithSchema(url: string, schema: string | null) {
|
||||
function wrapUrlWithOpenApp(
|
||||
origin: string,
|
||||
url: string,
|
||||
schema: string | null
|
||||
) {
|
||||
if (schema) {
|
||||
return `${schema}://open-url?${url}`;
|
||||
const urlWithSchema = `${schema}://sign-in?${url}`;
|
||||
return `${origin}/open-app?url=${encodeURIComponent(urlWithSchema)}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
@@ -88,9 +93,10 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
||||
if (!callbackUrl) {
|
||||
throw new Error('callbackUrl is not set');
|
||||
}
|
||||
const schema = getSchemaFromCallbackUrl(origin, callbackUrl);
|
||||
const wrappedUrl = wrapUrlWithSchema(url, schema);
|
||||
// hack: check if link is opened via desktop
|
||||
const schema = getSchemaFromCallbackUrl(origin, callbackUrl);
|
||||
const wrappedUrl = wrapUrlWithOpenApp(origin, url, schema);
|
||||
|
||||
const result = await mailer.sendMail({
|
||||
to: identifier,
|
||||
from: provider.from,
|
||||
|
||||
@@ -156,3 +156,20 @@ ImportPage.parameters = {
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const OpenAppPage: StoryFn = () => {
|
||||
return <FakeApp />;
|
||||
};
|
||||
OpenAppPage.decorators = [withRouter];
|
||||
OpenAppPage.parameters = {
|
||||
reactRouter: reactRouterParameters({
|
||||
routing: reactRouterOutlets(routes),
|
||||
location: {
|
||||
path: '/open-app',
|
||||
searchParams: {
|
||||
url: 'affine-beta://foo-bar.com',
|
||||
open: 'false',
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user