mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-22 18:42:48 +03:00
feat: add open app route (#3899)
This commit is contained in:
parent
71b195d9a9
commit
800f3c3cb6
BIN
apps/core/public/imgs/app-icon-beta.ico
Normal file
BIN
apps/core/public/imgs/app-icon-beta.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
BIN
apps/core/public/imgs/app-icon-canary.ico
Normal file
BIN
apps/core/public/imgs/app-icon-canary.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
BIN
apps/core/public/imgs/app-icon-internal.ico
Normal file
BIN
apps/core/public/imgs/app-icon-internal.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
BIN
apps/core/public/imgs/app-icon-stable.ico
Normal file
BIN
apps/core/public/imgs/app-icon-stable.ico
Normal file
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,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();
|
||||
|
58
apps/core/src/pages/open-app.css.ts
Normal file
58
apps/core/src/pages/open-app.css.ts
Normal file
@ -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,
|
||||
});
|
151
apps/core/src/pages/open-app.tsx
Normal file
151
apps/core/src/pages/open-app.tsx
Normal file
@ -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',
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
@ -426,21 +426,30 @@
|
||||
"com.affine.settings.profile.placeholder": "Input account name",
|
||||
"com.affine.auth.sign.in": "Sign in",
|
||||
"com.affine.auth.sign.up": "Sign up",
|
||||
"com.affine.auth.desktop.signing.in": "Signing in...",
|
||||
"com.affine.auth.desktop.signing.in.message": "Signing in with account <1></1>",
|
||||
"com.affine.auth.sign.up.sent.email.subtitle": "Create your account",
|
||||
"com.affine.auth.sign.sent.email.message.start": "An email with a magic link has been sent to ",
|
||||
"com.affine.auth.sign.sent.email.message.end": " You can click the link to create an account automatically.",
|
||||
"com.affine.auth.sign.up.success.title": "Your account has been created and you’re now signed in!",
|
||||
"com.affine.auth.sign.up.success.subtitle": "The app will automatically open or redirect to the web version. if you encounter any issues, you can also click the button below to manually open the AFFiNE app.",
|
||||
"com.affine.auth.sign.up.success.subtitle": "The app will automatically open or redirect to the web version. If you encounter any issues, you can also click the button below to manually open the AFFiNE app.",
|
||||
"com.affine.auth.page.sent.email.title": "Welcome to AFFiNE Cloud, you are almost there!",
|
||||
"com.affine.auth.page.sent.email.subtitle": "Please set a password of 8-20 characters with both letters and numbers to continue signing up with ",
|
||||
"com.affine.auth.later": "Later",
|
||||
"com.affine.auth.open.affine": "Open AFFiNE",
|
||||
"com.affine.auth.open.affine.prompt": "Opening <1>AFFiNE</1> app now",
|
||||
"com.affine.auth.open.affine.try-again": "Try again",
|
||||
"com.affine.auth.open.affine.download-app": "Download App",
|
||||
"com.affine.auth.sign.in.sent.email.subtitle": "Confirm your email",
|
||||
"com.affine.auth.sign.auth.code.message": "If you haven't received the email, please check your spam folder.",
|
||||
"com.affine.auth.sign.auth.code.message.password": "If you haven't received the email, please check your spam folder. Or <1>sign in with password</1> instead.",
|
||||
"com.affine.auth.password.error": "Invalid password",
|
||||
"com.affine.auth.forget": "Forgot password",
|
||||
"com.affine.auth.has.signed": " has signed in!",
|
||||
"com.affine.auth.toast.title.signed-in": "Signed in",
|
||||
"com.affine.auth.toast.message.signed-in": "You have been signed in, start to sync your data with AFFiNE Cloud!",
|
||||
"com.affine.auth.toast.title.failed": "Unable to sign in",
|
||||
"com.affine.auth.toast.message.failed": "Server error, please try again later.",
|
||||
"com.affine.auth.signed.success.title": "You’re almost there!",
|
||||
"com.affine.auth.signed.success.subtitle": "You have successfully signed in. The app will automatically open or redirect to the web version. if you encounter any issues, you can also click the button below to manually open the AFFiNE app.",
|
||||
"com.affine.auth.reset.password": "Reset Password",
|
||||
|
@ -173,7 +173,6 @@ export type UIHandlers = {
|
||||
handleMinimizeApp: () => Promise<any>;
|
||||
handleMaximizeApp: () => Promise<any>;
|
||||
handleCloseApp: () => Promise<any>;
|
||||
handleFinishLogin: () => Promise<void>;
|
||||
getGoogleOauthCode: () => Promise<any>;
|
||||
};
|
||||
|
||||
@ -267,7 +266,10 @@ export interface WorkspaceEvents {
|
||||
}
|
||||
|
||||
export interface UIEvents {
|
||||
onFinishLogin: (fn: () => void) => () => void;
|
||||
onStartLogin: (fn: (options: { email: string }) => void) => () => void;
|
||||
onFinishLogin: (
|
||||
fn: (result: { success: boolean; email: string }) => void
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
export interface EventMap {
|
||||
|
Loading…
Reference in New Issue
Block a user