fix: sign in issues (#4047)

Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
Qi
2023-08-31 21:07:05 +08:00
committed by GitHub
parent 13857d59dc
commit 3f5e649295
17 changed files with 430 additions and 472 deletions

View File

@@ -1,23 +1,39 @@
import {
AuthContent,
BackButton,
CountDownRender,
ModalHeader,
ResendButton,
} from '@affine/component/auth-components';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { type FC, useCallback } from 'react';
import { Button } from '@toeverything/components/button';
import { useCallback } from 'react';
import { signInCloud } from '../../../utils/cloud-utils';
import { buildCallbackUrl } from './callback-url';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import type { AuthPanelProps } from './index';
import * as style from './style.css';
import { useAuth } from './use-auth';
export const AfterSignInSendEmail: FC<AuthPanelProps> = ({
export const AfterSignInSendEmail = ({
setAuthState,
email,
}) => {
onSignedIn,
}: AuthPanelProps) => {
const t = useAFFiNEI18N();
const loginStatus = useCurrentLoginStatus();
const { resendCountDown, allowSendEmail, signIn } = useAuth({
onNoAccess: useCallback(() => {
setAuthState('noAccess');
}, [setAuthState]),
});
if (loginStatus === 'authenticated') {
onSignedIn?.();
}
const onResendClick = useCallback(async () => {
await signIn(email);
}, [email, signIn]);
return (
<>
@@ -31,15 +47,23 @@ export const AfterSignInSendEmail: FC<AuthPanelProps> = ({
{t['com.affine.auth.sign.sent.email.message.end']()}
</AuthContent>
<ResendButton
onClick={useCallback(() => {
signInCloud('email', {
email,
callbackUrl: buildCallbackUrl('/auth/signIn'),
redirect: true,
}).catch(console.error);
}, [email])}
/>
<div className={style.resendWrapper}>
{allowSendEmail ? (
<Button type="plain" size="large" onClick={onResendClick}>
{t['com.affine.auth.sign.auth.code.resend.hint']()}
</Button>
) : (
<>
<span className="resend-code-hint">
{t['com.affine.auth.sign.auth.code.on.resend.hint']()}
</span>
<CountDownRender
className={style.resendCountdown}
timeLeft={resendCountDown}
/>
</>
)}
</div>
<div className={style.authMessage} style={{ marginTop: 20 }}>
{/*prettier-ignore*/}

View File

@@ -1,22 +1,39 @@
import {
AuthContent,
BackButton,
CountDownRender,
ModalHeader,
ResendButton,
} from '@affine/component/auth-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { type FC, useCallback } from 'react';
import { signInCloud } from '../../../utils/cloud-utils';
import { buildCallbackUrl } from './callback-url';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import type { AuthPanelProps } from './index';
import * as style from './style.css';
import { useAuth } from './use-auth';
export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
setAuthState,
email,
onSignedIn,
}) => {
const t = useAFFiNEI18N();
const loginStatus = useCurrentLoginStatus();
const { resendCountDown, allowSendEmail, signUp } = useAuth({
onNoAccess: useCallback(() => {
setAuthState('noAccess');
}, [setAuthState]),
});
if (loginStatus === 'authenticated') {
onSignedIn?.();
}
const onResendClick = useCallback(async () => {
await signUp(email);
}, [email, signUp]);
return (
<>
@@ -30,15 +47,23 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
{t['com.affine.auth.sign.sent.email.message.end']()}
</AuthContent>
<ResendButton
onClick={useCallback(() => {
signInCloud('email', {
email: email,
callbackUrl: buildCallbackUrl('/auth/signUp'),
redirect: true,
}).catch(console.error);
}, [email])}
/>
<div className={style.resendWrapper}>
{allowSendEmail ? (
<Button type="plain" size="large" onClick={onResendClick}>
{t['com.affine.auth.sign.auth.code.resend.hint']()}
</Button>
) : (
<>
<span className="resend-code-hint">
{t['com.affine.auth.sign.auth.code.on.resend.hint']()}
</span>
<CountDownRender
className={style.resendCountdown}
timeLeft={resendCountDown}
/>
</>
)}
</div>
<div className={style.authMessage} style={{ marginTop: 20 }}>
{t['com.affine.auth.sign.auth.code.message']()}

View File

@@ -3,17 +3,12 @@ import {
type AuthModalProps as AuthModalBaseProps,
} from '@affine/component/auth-components';
import { refreshRootMetadataAtom } from '@affine/workspace/atom';
import { atom, useAtom, useSetAtom } from 'jotai';
import {
type FC,
startTransition,
useCallback,
useEffect,
useMemo,
} from 'react';
import { useSetAtom } from 'jotai';
import { type FC, startTransition, useCallback, useMemo } from 'react';
import { AfterSignInSendEmail } from './after-sign-in-send-email';
import { AfterSignUpSendEmail } from './after-sign-up-send-email';
import { NoAccess } from './no-access';
import { SendEmail } from './send-email';
import { SignIn } from './sign-in';
import { SignInWithPassword } from './sign-in-with-password';
@@ -25,7 +20,8 @@ export type AuthProps = {
| 'afterSignInSendEmail'
// throw away
| 'signInWithPassword'
| 'sendEmail';
| 'sendEmail'
| 'noAccess';
setAuthState: (state: AuthProps['state']) => void;
setAuthEmail: (state: AuthProps['email']) => void;
setEmailType: (state: AuthProps['emailType']) => void;
@@ -41,8 +37,6 @@ export type AuthPanelProps = {
setEmailType: AuthProps['setEmailType'];
emailType: AuthProps['emailType'];
onSignedIn?: () => void;
authStore: AuthStoreAtom;
setAuthStore: (data: Partial<AuthStoreAtom>) => void;
};
const config: {
@@ -53,17 +47,9 @@ const config: {
afterSignInSendEmail: AfterSignInSendEmail,
signInWithPassword: SignInWithPassword,
sendEmail: SendEmail,
noAccess: NoAccess,
};
type AuthStoreAtom = {
hasSentEmail: boolean;
resendCountDown: number;
};
export const authStoreAtom = atom<AuthStoreAtom>({
hasSentEmail: false,
resendCountDown: 60,
});
export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
open,
state,
@@ -74,18 +60,6 @@ export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
setEmailType,
emailType,
}) => {
const [, setAuthStore] = useAtom(authStoreAtom);
useEffect(() => {
if (!open) {
setAuthStore({
hasSentEmail: false,
resendCountDown: 60,
});
setAuthEmail('');
}
}, [open, setAuthEmail, setAuthStore]);
const refreshMetadata = useSetAtom(refreshRootMetadataAtom);
const onSignedIn = useCallback(() => {
@@ -119,39 +93,18 @@ export const AuthPanel: FC<AuthProps> = ({
emailType,
onSignedIn,
}) => {
const [authStore, setAuthStore] = useAtom(authStoreAtom);
const CurrentPanel = useMemo(() => {
return config[state];
}, [state]);
useEffect(() => {
return () => {
setAuthStore({
hasSentEmail: false,
resendCountDown: 60,
});
};
}, [setAuthEmail, setAuthStore]);
return (
<CurrentPanel
email={email}
setAuthState={setAuthState}
setAuthEmail={setAuthEmail}
setEmailType={setEmailType}
authStore={authStore}
emailType={emailType}
onSignedIn={onSignedIn}
setAuthStore={useCallback(
(data: Partial<AuthStoreAtom>) => {
setAuthStore(prev => ({
...prev,
...data,
}));
},
[setAuthStore]
)}
/>
);
};

View File

@@ -0,0 +1,53 @@
import {
AuthContent,
BackButton,
ModalHeader,
} from '@affine/component/auth-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { NewIcon } from '@blocksuite/icons';
import { type FC, useCallback } from 'react';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import type { AuthPanelProps } from './index';
import * as style from './style.css';
export const NoAccess: FC<AuthPanelProps> = ({ setAuthState, onSignedIn }) => {
const t = useAFFiNEI18N();
const loginStatus = useCurrentLoginStatus();
if (loginStatus === 'authenticated') {
onSignedIn?.();
}
return (
<>
<ModalHeader
title={t['AFFiNE Cloud']()}
subTitle={t['Early Access Stage']()}
/>
<AuthContent style={{ height: 162 }}>
{t['com.affine.auth.sign.no.access.hint']()}
<a href="https://community.affine.pro/c/insider-general/">
{t['com.affine.auth.sign.no.access.link']()}
</a>
</AuthContent>
<div className={style.accessMessage}>
<NewIcon
style={{
fontSize: 16,
marginRight: 4,
color: 'var(--affine-icon-color)',
}}
/>
{t['com.affine.auth.sign.no.access.wait']()}
</div>
<BackButton
onClick={useCallback(() => {
setAuthState('signIn');
}, [setAuthState])}
/>
</>
);
};

View File

@@ -16,7 +16,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation } from '@affine/workspace/affine/gql';
import { Button } from '@toeverything/components/button';
import { useSetAtom } from 'jotai/react';
import { type FC, useCallback } from 'react';
import { useCallback, useState } from 'react';
import type { AuthPanelProps } from './index';
@@ -118,14 +118,13 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
};
};
export const SendEmail: FC<AuthPanelProps> = ({
export const SendEmail = ({
setAuthState,
setAuthStore,
email,
authStore: { hasSentEmail },
emailType,
}) => {
}: AuthPanelProps) => {
const t = useAFFiNEI18N();
const [hasSentEmail, setHasSentEmail] = useState(false);
const pushNotification = useSetAtom(pushNotificationAtom);
const title = useEmailTitle(emailType);
@@ -143,8 +142,8 @@ export const SendEmail: FC<AuthPanelProps> = ({
key: Date.now().toString(),
type: 'success',
});
setAuthStore({ hasSentEmail: true });
}, [email, hint, pushNotification, sendEmail, setAuthStore]);
setHasSentEmail(true);
}, [email, hint, pushNotification, sendEmail]);
return (
<>

View File

@@ -1,62 +1,52 @@
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
import { pushNotificationAtom } from '@affine/component/notification-center';
import type { Notification } from '@affine/component/notification-center/index.jotai';
import { isDesktop } from '@affine/env/constant';
import {
AuthInput,
CountDownRender,
ModalHeader,
} from '@affine/component/auth-components';
import { getUserQuery } from '@affine/graphql';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation } from '@affine/workspace/affine/gql';
import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { useSetAtom } from 'jotai';
import { type SignInResponse } from 'next-auth/react';
import { type FC, useState } from 'react';
import { useCallback } from 'react';
import { signInCloud } from '../../../utils/cloud-utils';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import { emailRegex } from '../../../utils/email-regex';
import { buildCallbackUrl } from './callback-url';
import type { AuthPanelProps } from './index';
import * as style from './style.css';
import { useAuth } from './use-auth';
function validateEmail(email: string) {
return emailRegex.test(email);
}
const INTERNAL_BETA_URL = `https://community.affine.pro/c/insider-general/`;
function handleSendEmailError(
res: SignInResponse | undefined,
pushNotification: (notification: Notification) => void
) {
if (res?.error) {
pushNotification({
title: 'Send email error',
message: 'Please back to home and try again',
type: 'error',
});
}
if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) {
pushNotification({
title: 'Sign up error',
message: `You don't have early access permission\nVisit ${INTERNAL_BETA_URL} for more information`,
type: 'error',
});
}
}
export const SignIn: FC<AuthPanelProps> = ({
setAuthState,
setAuthEmail,
email,
onSignedIn,
}) => {
const t = useAFFiNEI18N();
const loginStatus = useCurrentLoginStatus();
const { resendCountDown, allowSendEmail, signIn, signUp, signInWithGoogle } =
useAuth({
onNoAccess: useCallback(() => {
setAuthState('noAccess');
}, [setAuthState]),
});
const { trigger: verifyUser, isMutating } = useMutation({
mutation: getUserQuery,
});
const [isValidEmail, setIsValidEmail] = useState(true);
const pushNotification = useSetAtom(pushNotificationAtom);
if (loginStatus === 'authenticated') {
onSignedIn?.();
}
const onContinue = useCallback(async () => {
if (!validateEmail(email)) {
setIsValidEmail(false);
@@ -68,26 +58,16 @@ export const SignIn: FC<AuthPanelProps> = ({
setAuthEmail(email);
if (user) {
signInCloud('email', {
email: email,
callbackUrl: buildCallbackUrl('/auth/signIn'),
redirect: false,
})
.then(res => handleSendEmailError(res, pushNotification))
.catch(console.error);
setAuthState('afterSignInSendEmail');
} else {
signInCloud('email', {
email: email,
callbackUrl: buildCallbackUrl('/auth/signUp'),
redirect: false,
})
.then(res => handleSendEmailError(res, pushNotification))
.catch(console.error);
await signIn(email);
} else {
setAuthState('afterSignUpSendEmail');
await signUp(email);
}
}, [email, setAuthEmail, setAuthState, verifyUser, pushNotification]);
}, [email, setAuthEmail, setAuthState, signIn, signUp, verifyUser]);
return (
<>
<ModalHeader
@@ -103,18 +83,9 @@ export const SignIn: FC<AuthPanelProps> = ({
marginTop: 30,
}}
icon={<GoogleDuotoneIcon />}
onClick={useCallback(() => {
if (isDesktop) {
open(
`/desktop-signin?provider=google&callback_url=${buildCallbackUrl(
'/open-app/oauth-jwt'
)}`,
'_target'
);
} else {
signInCloud('google').catch(console.error);
}
}, [])}
onClick={useCallback(async () => {
await signInWithGoogle();
}, [signInWithGoogle])}
>
{t['Continue with Google']()}
</Button>
@@ -142,15 +113,23 @@ export const SignIn: FC<AuthPanelProps> = ({
data-testid="continue-login-button"
block
loading={isMutating}
disabled={!allowSendEmail}
icon={
<ArrowDownBigIcon
width={20}
height={20}
style={{
transform: 'rotate(-90deg)',
color: 'var(--affine-blue)',
}}
/>
allowSendEmail || isMutating ? (
<ArrowDownBigIcon
width={20}
height={20}
style={{
transform: 'rotate(-90deg)',
color: 'var(--affine-blue)',
}}
/>
) : (
<CountDownRender
className={style.resendCountdownInButton}
timeLeft={resendCountDown}
/>
)
}
iconPosition="end"
onClick={onContinue}

View File

@@ -26,3 +26,32 @@ export const forgetPasswordButton = style({
bottom: 0,
display: 'none',
});
export const resendWrapper = style({
height: 32,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: 30,
});
export const resendCountdown = style({ width: 45, textAlign: 'center' });
export const resendCountdownInButton = style({
width: 40,
textAlign: 'center',
fontSize: 'var(--affine-font-sm)',
marginLeft: 16,
color: 'var(--affine-blue)',
fontWeight: 400,
});
export const accessMessage = style({
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: 'var(--affine-font-xs)',
fontWeight: 500,
marginTop: 65,
marginBottom: 40,
});

View File

@@ -0,0 +1,136 @@
import { pushNotificationAtom } from '@affine/component/notification-center';
import type { Notification } from '@affine/component/notification-center/index.jotai';
import { isDesktop } from '@affine/env/constant';
import { atom, useAtom, useSetAtom } from 'jotai';
import { type SignInResponse } from 'next-auth/react';
import { useCallback } from 'react';
import { signInCloud } from '../../../utils/cloud-utils';
import { buildCallbackUrl } from './callback-url';
const COUNT_DOWN_TIME = 60;
const INTERNAL_BETA_URL = `https://community.affine.pro/c/insider-general/`;
function handleSendEmailError(
res: SignInResponse | undefined | void,
pushNotification: (notification: Notification) => void
) {
if (res?.error) {
pushNotification({
title: 'Send email error',
message: 'Please back to home and try again',
type: 'error',
});
}
// if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) {
// pushNotification({
// title: 'Sign up error',
// message: `You don't have early access permission\nVisit ${INTERNAL_BETA_URL} for more information`,
// type: 'error',
// });
// }
}
type AuthStoreAtom = {
allowSendEmail: boolean;
resendCountDown: number;
};
export const authStoreAtom = atom<AuthStoreAtom>({
allowSendEmail: true,
resendCountDown: COUNT_DOWN_TIME,
});
const countDownAtom = atom(
null, // it's a convention to pass `null` for the first argument
(get, set) => {
const clearId = window.setInterval(() => {
const countDown = get(authStoreAtom).resendCountDown;
if (countDown === 0) {
set(authStoreAtom, {
allowSendEmail: true,
resendCountDown: COUNT_DOWN_TIME,
});
window.clearInterval(clearId);
return;
}
set(authStoreAtom, {
resendCountDown: countDown - 1,
allowSendEmail: false,
});
}, 1000);
}
);
export const useAuth = ({ onNoAccess }: { onNoAccess: () => void }) => {
const pushNotification = useSetAtom(pushNotificationAtom);
const [authStore, setAuthStore] = useAtom(authStoreAtom);
const startResendCountDown = useSetAtom(countDownAtom);
const signIn = useCallback(
async (email: string) => {
setAuthStore(() => ({
allowSendEmail: false,
resendCountDown: COUNT_DOWN_TIME,
}));
startResendCountDown();
const res = await signInCloud('email', {
email: email,
callbackUrl: buildCallbackUrl('signIn'),
redirect: false,
}).catch(console.error);
handleSendEmailError(res, pushNotification);
if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) {
onNoAccess();
}
},
[onNoAccess, pushNotification, setAuthStore, startResendCountDown]
);
const signUp = useCallback(
async (email: string) => {
setAuthStore({
allowSendEmail: false,
resendCountDown: COUNT_DOWN_TIME,
});
startResendCountDown();
const res = await signInCloud('email', {
email: email,
callbackUrl: buildCallbackUrl('signUp'),
redirect: false,
}).catch(console.error);
handleSendEmailError(res, pushNotification);
if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) {
onNoAccess();
}
},
[onNoAccess, pushNotification, setAuthStore, startResendCountDown]
);
const signInWithGoogle = useCallback(() => {
if (isDesktop) {
open(
`/desktop-signin?provider=google&callback_url=${buildCallbackUrl(
'/open-app/oauth-jwt'
)}`,
'_target'
);
} else {
signInCloud('google').catch(console.error);
}
}, []);
return {
allowSendEmail: authStore.allowSendEmail,
resendCountDown: authStore.resendCountDown,
signUp,
signIn,
signInWithGoogle,
};
};

View File

@@ -10,7 +10,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
import { ArrowRightSmallIcon, CameraIcon, DoneIcon } from '@blocksuite/icons';
import { Button, IconButton } from '@toeverything/components/button';
import { useAtom } from 'jotai';
import { useSetAtom } from 'jotai';
import { type FC, Suspense, useCallback, useState } from 'react';
import { authAtom } from '../../../../atoms';
@@ -137,7 +137,7 @@ const StoragePanel = () => {
export const AccountSetting: FC = () => {
const t = useAFFiNEI18N();
const user = useCurrentUser();
const [, setAuthModal] = useAtom(authAtom);
const setAuthModal = useSetAtom(authAtom);
const onChangeEmail = useCallback(() => {
setAuthModal({
@@ -147,14 +147,15 @@ export const AccountSetting: FC = () => {
emailType: 'changeEmail',
});
}, [setAuthModal, user.email]);
const onChangePassword = useCallback(() => {
const onPasswordButtonClick = useCallback(() => {
setAuthModal({
openModal: true,
state: 'sendEmail',
email: user.email,
emailType: 'changePassword',
emailType: user.hasPassword ? 'changePassword' : 'setPassword',
});
}, [setAuthModal, user.email]);
}, [setAuthModal, user.email, user.hasPassword]);
return (
<>
@@ -173,7 +174,7 @@ export const AccountSetting: FC = () => {
name={t['com.affine.settings.password']()}
desc={t['com.affine.settings.password.message']()}
>
<Button onClick={onChangePassword}>
<Button onClick={onPasswordButtonClick}>
{user.hasPassword
? t['com.affine.settings.password.action.change']()
: t['com.affine.settings.password.action.set']()}

View File

@@ -8,10 +8,9 @@ export const settingContent = style({
});
globalStyle(`${settingContent} .wrapper`, {
width: '60%',
padding: '0 15px',
height: '100%',
minWidth: '560px',
maxWidth: '560px',
margin: '0 auto',
overflowY: 'auto',
});