feat: add open app route (#3899)

This commit is contained in:
Peng Xiao 2023-08-30 06:40:25 +08:00 committed by GitHub
parent 71b195d9a9
commit 800f3c3cb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 486 additions and 104 deletions

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

View File

@ -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]);

View File

@ -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>
);
};

View File

@ -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,
});

View File

@ -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();

View File

@ -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>

View File

@ -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 />;
}

View File

@ -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 (

View File

@ -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' {

View File

@ -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') {

View File

@ -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();

View 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,
});

View 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;
}
};

View File

@ -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();

View File

@ -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 />}
</>
);
};

View File

@ -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'),

View File

@ -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);
}
}
}

View File

@ -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
*/

View File

@ -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() {

View File

@ -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>;

View File

@ -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();
},

View File

@ -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 }>(),
};

View File

@ -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,

View File

@ -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',
},
},
}),
};

View File

@ -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 youre 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": "Youre 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",

View File

@ -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 {