mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-24 22:09:08 +08:00
feat: add open app route (#3899)
This commit is contained in:
@@ -5,7 +5,6 @@ import {
|
||||
SignInSuccessPage,
|
||||
SignUpPage,
|
||||
} from '@affine/component/auth-components';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { changeEmailMutation, changePasswordMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import type { ReactElement } from 'react';
|
||||
@@ -13,7 +12,7 @@ import { useCallback } from 'react';
|
||||
import { type LoaderFunction, redirect, useParams } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
import { useCurrentUser } from '../hooks/affine/use-current-user';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
|
||||
@@ -58,11 +57,7 @@ export const AuthPage = (): ReactElement | null => {
|
||||
[changePassword, user.id]
|
||||
);
|
||||
const onOpenAffine = useCallback(() => {
|
||||
if (isDesktop) {
|
||||
window.apis.ui.handleFinishLogin();
|
||||
} else {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}, [jumpToIndex]);
|
||||
|
||||
switch (authType) {
|
||||
@@ -119,7 +114,7 @@ export const loader: LoaderFunction = async args => {
|
||||
return null;
|
||||
};
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const { jumpToExpired } = useNavigateHelper();
|
||||
|
||||
if (loginStatus === 'unauthenticated') {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useCallback, useEffect } from 'react';
|
||||
import { type LoaderFunction, redirect, useLoaderData } from 'react-router-dom';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { useAppHelper } from '../hooks/use-workspaces';
|
||||
|
||||
@@ -45,7 +45,7 @@ export const loader: LoaderFunction = async args => {
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const { jumpToSignIn } = useNavigateHelper();
|
||||
const { addCloudWorkspace } = useAppHelper();
|
||||
const { jumpToSubPath } = useNavigateHelper();
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const affineLogo = style({
|
||||
color: 'inherit',
|
||||
});
|
||||
|
||||
export const topNav = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '16px 120px',
|
||||
});
|
||||
|
||||
export const topNavLinks = style({
|
||||
display: 'flex',
|
||||
columnGap: 4,
|
||||
});
|
||||
|
||||
export const topNavLink = style({
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
padding: '4px 18px',
|
||||
});
|
||||
|
||||
export const tryAgainLink = style({
|
||||
color: 'var(--affine-link-color)',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
});
|
||||
|
||||
export const centerContent = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginTop: 40,
|
||||
});
|
||||
|
||||
export const prompt = style({
|
||||
marginTop: 20,
|
||||
marginBottom: 12,
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import * as styles from './open-app.css';
|
||||
|
||||
let lastOpened = '';
|
||||
|
||||
const appSchemas = z.enum([
|
||||
'affine',
|
||||
'affine-canary',
|
||||
'affine-beta',
|
||||
'affine-internal',
|
||||
'affine-dev',
|
||||
]);
|
||||
|
||||
const appChannelSchema = z.enum(['stable', 'canary', 'beta', 'internal']);
|
||||
|
||||
type Schema = z.infer<typeof appSchemas>;
|
||||
type Channel = z.infer<typeof appChannelSchema>;
|
||||
|
||||
const schemaToChanel = {
|
||||
affine: 'stable',
|
||||
'affine-canary': 'canary',
|
||||
'affine-beta': 'beta',
|
||||
'affine-internal': 'internal',
|
||||
'affine-dev': 'canary', // dev does not have a dedicated app. use canary as the placeholder.
|
||||
} as Record<Schema, Channel>;
|
||||
|
||||
const appIconMap = {
|
||||
stable: '/imgs/app-icon-stable.ico',
|
||||
canary: '/imgs/app-icon-canary.ico',
|
||||
beta: '/imgs/app-icon-beta.ico',
|
||||
internal: '/imgs/app-icon-internal.ico',
|
||||
} satisfies Record<Channel, string>;
|
||||
|
||||
const appNames = {
|
||||
stable: 'AFFiNE',
|
||||
canary: 'AFFiNE Canary',
|
||||
beta: 'AFFiNE Beta',
|
||||
internal: 'AFFiNE Internal',
|
||||
} satisfies Record<Channel, string>;
|
||||
|
||||
export const Component = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [params] = useSearchParams();
|
||||
const urlToOpen = useMemo(() => params.get('url'), [params]);
|
||||
const autoOpen = useMemo(() => params.get('open') !== 'false', [params]);
|
||||
const channel = useMemo(() => {
|
||||
const urlObj = new URL(urlToOpen || '');
|
||||
const maybeSchema = appSchemas.safeParse(urlObj.protocol.replace(':', ''));
|
||||
return schemaToChanel[maybeSchema.success ? maybeSchema.data : 'affine'];
|
||||
}, [urlToOpen]);
|
||||
|
||||
const appIcon = appIconMap[channel];
|
||||
const appName = appNames[channel];
|
||||
|
||||
const openDownloadLink = useCallback(() => {
|
||||
const url = `https://affine.pro/download?channel=${channel}`;
|
||||
open(url, '_blank');
|
||||
}, [channel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!urlToOpen || lastOpened === urlToOpen || !autoOpen) {
|
||||
return;
|
||||
}
|
||||
lastOpened = urlToOpen;
|
||||
open(urlToOpen, '_blank');
|
||||
}, [urlToOpen, autoOpen]);
|
||||
|
||||
if (urlToOpen) {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.topNav}>
|
||||
<a
|
||||
href="https://affine.pro"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.affineLogo}
|
||||
>
|
||||
<Logo1Icon width={24} height={24} />
|
||||
</a>
|
||||
|
||||
<div className={styles.topNavLinks}>
|
||||
<a
|
||||
href="https://affine.pro"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Official Website
|
||||
</a>
|
||||
<a
|
||||
href="https://community.affine.pro/home"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
AFFiNE Community
|
||||
</a>
|
||||
<a
|
||||
href="https://affine.pro/blog"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="https://affine.pro/about-us"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
Contact us
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Button onClick={openDownloadLink}>
|
||||
{t['com.affine.auth.open.affine.download-app']()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.centerContent}>
|
||||
<img src={appIcon} alt={appName} width={120} height={120} />
|
||||
|
||||
<div className={styles.prompt}>
|
||||
<Trans i18nKey="com.affine.auth.open.affine.prompt">
|
||||
Open {appName} app now
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<a
|
||||
className={styles.tryAgainLink}
|
||||
href={urlToOpen}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t['com.affine.auth.open.affine.try-again']()}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import { AuthPanel } from '../components/affine/auth';
|
||||
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
|
||||
interface LocationState {
|
||||
state: {
|
||||
@@ -18,7 +18,7 @@ export const Component = () => {
|
||||
{ state, email = '', emailType = 'changePassword', onceSignedIn },
|
||||
setAuthAtom,
|
||||
] = useAtom(authAtom);
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const location = useLocation() as LocationState;
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user