diff --git a/apps/core/src/components/affine/auth/after-sign-in-send-email.tsx b/apps/core/src/components/affine/auth/after-sign-in-send-email.tsx index 646c3b6863..5db7f13fe8 100644 --- a/apps/core/src/components/affine/auth/after-sign-in-send-email.tsx +++ b/apps/core/src/components/affine/auth/after-sign-in-send-email.tsx @@ -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 = ({ +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 = ({ {t['com.affine.auth.sign.sent.email.message.end']()} - { - signInCloud('email', { - email, - callbackUrl: buildCallbackUrl('/auth/signIn'), - redirect: true, - }).catch(console.error); - }, [email])} - /> +
+ {allowSendEmail ? ( + + ) : ( + <> + + {t['com.affine.auth.sign.auth.code.on.resend.hint']()} + + + + )} +
{/*prettier-ignore*/} diff --git a/apps/core/src/components/affine/auth/after-sign-up-send-email.tsx b/apps/core/src/components/affine/auth/after-sign-up-send-email.tsx index 68ae46cbb5..36a0ff7acb 100644 --- a/apps/core/src/components/affine/auth/after-sign-up-send-email.tsx +++ b/apps/core/src/components/affine/auth/after-sign-up-send-email.tsx @@ -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 = ({ 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 = ({ {t['com.affine.auth.sign.sent.email.message.end']()} - { - signInCloud('email', { - email: email, - callbackUrl: buildCallbackUrl('/auth/signUp'), - redirect: true, - }).catch(console.error); - }, [email])} - /> +
+ {allowSendEmail ? ( + + ) : ( + <> + + {t['com.affine.auth.sign.auth.code.on.resend.hint']()} + + + + )} +
{t['com.affine.auth.sign.auth.code.message']()} diff --git a/apps/core/src/components/affine/auth/index.tsx b/apps/core/src/components/affine/auth/index.tsx index b34baf279b..128480b3e7 100644 --- a/apps/core/src/components/affine/auth/index.tsx +++ b/apps/core/src/components/affine/auth/index.tsx @@ -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) => 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({ - hasSentEmail: false, - resendCountDown: 60, -}); - export const AuthModal: FC = ({ open, state, @@ -74,18 +60,6 @@ export const AuthModal: FC = ({ 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 = ({ emailType, onSignedIn, }) => { - const [authStore, setAuthStore] = useAtom(authStoreAtom); - const CurrentPanel = useMemo(() => { return config[state]; }, [state]); - useEffect(() => { - return () => { - setAuthStore({ - hasSentEmail: false, - resendCountDown: 60, - }); - }; - }, [setAuthEmail, setAuthStore]); - return ( ) => { - setAuthStore(prev => ({ - ...prev, - ...data, - })); - }, - [setAuthStore] - )} /> ); }; diff --git a/apps/core/src/components/affine/auth/no-access.tsx b/apps/core/src/components/affine/auth/no-access.tsx new file mode 100644 index 0000000000..961b6e4b98 --- /dev/null +++ b/apps/core/src/components/affine/auth/no-access.tsx @@ -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 = ({ setAuthState, onSignedIn }) => { + const t = useAFFiNEI18N(); + const loginStatus = useCurrentLoginStatus(); + + if (loginStatus === 'authenticated') { + onSignedIn?.(); + } + + return ( + <> + + + {t['com.affine.auth.sign.no.access.hint']()} + + {t['com.affine.auth.sign.no.access.link']()} + + + +
+ + {t['com.affine.auth.sign.no.access.wait']()} +
+ + { + setAuthState('signIn'); + }, [setAuthState])} + /> + + ); +}; diff --git a/apps/core/src/components/affine/auth/send-email.tsx b/apps/core/src/components/affine/auth/send-email.tsx index 8d13421528..228252118b 100644 --- a/apps/core/src/components/affine/auth/send-email.tsx +++ b/apps/core/src/components/affine/auth/send-email.tsx @@ -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 = ({ +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 = ({ key: Date.now().toString(), type: 'success', }); - setAuthStore({ hasSentEmail: true }); - }, [email, hint, pushNotification, sendEmail, setAuthStore]); + setHasSentEmail(true); + }, [email, hint, pushNotification, sendEmail]); return ( <> diff --git a/apps/core/src/components/affine/auth/sign-in.tsx b/apps/core/src/components/affine/auth/sign-in.tsx index 3fc8db1510..cfc89233a3 100644 --- a/apps/core/src/components/affine/auth/sign-in.tsx +++ b/apps/core/src/components/affine/auth/sign-in.tsx @@ -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 = ({ 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 = ({ 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 ( <> = ({ marginTop: 30, }} icon={} - 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']()} @@ -142,15 +113,23 @@ export const SignIn: FC = ({ data-testid="continue-login-button" block loading={isMutating} + disabled={!allowSendEmail} icon={ - + allowSendEmail || isMutating ? ( + + ) : ( + + ) } iconPosition="end" onClick={onContinue} diff --git a/apps/core/src/components/affine/auth/style.css.ts b/apps/core/src/components/affine/auth/style.css.ts index 3ee63abd4f..ba6f5df032 100644 --- a/apps/core/src/components/affine/auth/style.css.ts +++ b/apps/core/src/components/affine/auth/style.css.ts @@ -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, +}); diff --git a/apps/core/src/components/affine/auth/use-auth.ts b/apps/core/src/components/affine/auth/use-auth.ts new file mode 100644 index 0000000000..5c201d8e17 --- /dev/null +++ b/apps/core/src/components/affine/auth/use-auth.ts @@ -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({ + 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, + }; +}; diff --git a/apps/core/src/components/affine/setting-modal/account-setting/index.tsx b/apps/core/src/components/affine/setting-modal/account-setting/index.tsx index 54286a7753..55c5ffc16a 100644 --- a/apps/core/src/components/affine/setting-modal/account-setting/index.tsx +++ b/apps/core/src/components/affine/setting-modal/account-setting/index.tsx @@ -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']()} > - - ) : ( - <> - - {t['com.affine.auth.sign.auth.code.on.resend.hint']()} - - - - )} -
- ); -}; diff --git a/packages/component/src/components/auth-components/set-password-page.tsx b/packages/component/src/components/auth-components/set-password-page.tsx index 7eea1f4e0a..4caf97c8cb 100644 --- a/packages/component/src/components/auth-components/set-password-page.tsx +++ b/packages/component/src/components/auth-components/set-password-page.tsx @@ -47,7 +47,6 @@ export const SetPasswordPage: FC<{ ) } > -

This is set page

{hasSetUp ? (