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

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