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

@@ -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' {
+3 -8
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') {
+2 -2
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();
+58
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,
});
+151
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;
}
};
+2 -2
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();
+61 -1
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 />}
</>
);
};
+4
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'),
+76 -6
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);
}
}
}
-6
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
*/
+31 -45
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() {
+9 -1
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>;
-6
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();
},
+2 -1
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 }>(),
};
@@ -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',
},
},
}),
};