mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
fix: sign in issues (#4047)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
@@ -1,23 +1,39 @@
|
|||||||
import {
|
import {
|
||||||
AuthContent,
|
AuthContent,
|
||||||
BackButton,
|
BackButton,
|
||||||
|
CountDownRender,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ResendButton,
|
|
||||||
} from '@affine/component/auth-components';
|
} from '@affine/component/auth-components';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
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 { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||||
import { buildCallbackUrl } from './callback-url';
|
|
||||||
import type { AuthPanelProps } from './index';
|
import type { AuthPanelProps } from './index';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
|
import { useAuth } from './use-auth';
|
||||||
|
|
||||||
export const AfterSignInSendEmail: FC<AuthPanelProps> = ({
|
export const AfterSignInSendEmail = ({
|
||||||
setAuthState,
|
setAuthState,
|
||||||
email,
|
email,
|
||||||
}) => {
|
onSignedIn,
|
||||||
|
}: AuthPanelProps) => {
|
||||||
const t = useAFFiNEI18N();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -31,15 +47,23 @@ export const AfterSignInSendEmail: FC<AuthPanelProps> = ({
|
|||||||
{t['com.affine.auth.sign.sent.email.message.end']()}
|
{t['com.affine.auth.sign.sent.email.message.end']()}
|
||||||
</AuthContent>
|
</AuthContent>
|
||||||
|
|
||||||
<ResendButton
|
<div className={style.resendWrapper}>
|
||||||
onClick={useCallback(() => {
|
{allowSendEmail ? (
|
||||||
signInCloud('email', {
|
<Button type="plain" size="large" onClick={onResendClick}>
|
||||||
email,
|
{t['com.affine.auth.sign.auth.code.resend.hint']()}
|
||||||
callbackUrl: buildCallbackUrl('/auth/signIn'),
|
</Button>
|
||||||
redirect: true,
|
) : (
|
||||||
}).catch(console.error);
|
<>
|
||||||
}, [email])}
|
<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 }}>
|
<div className={style.authMessage} style={{ marginTop: 20 }}>
|
||||||
{/*prettier-ignore*/}
|
{/*prettier-ignore*/}
|
||||||
|
|||||||
@@ -1,22 +1,39 @@
|
|||||||
import {
|
import {
|
||||||
AuthContent,
|
AuthContent,
|
||||||
BackButton,
|
BackButton,
|
||||||
|
CountDownRender,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ResendButton,
|
|
||||||
} from '@affine/component/auth-components';
|
} from '@affine/component/auth-components';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { Button } from '@toeverything/components/button';
|
||||||
import { type FC, useCallback } from 'react';
|
import { type FC, useCallback } from 'react';
|
||||||
|
|
||||||
import { signInCloud } from '../../../utils/cloud-utils';
|
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||||
import { buildCallbackUrl } from './callback-url';
|
|
||||||
import type { AuthPanelProps } from './index';
|
import type { AuthPanelProps } from './index';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
|
import { useAuth } from './use-auth';
|
||||||
|
|
||||||
export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
||||||
setAuthState,
|
setAuthState,
|
||||||
email,
|
email,
|
||||||
|
onSignedIn,
|
||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -30,15 +47,23 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
|||||||
{t['com.affine.auth.sign.sent.email.message.end']()}
|
{t['com.affine.auth.sign.sent.email.message.end']()}
|
||||||
</AuthContent>
|
</AuthContent>
|
||||||
|
|
||||||
<ResendButton
|
<div className={style.resendWrapper}>
|
||||||
onClick={useCallback(() => {
|
{allowSendEmail ? (
|
||||||
signInCloud('email', {
|
<Button type="plain" size="large" onClick={onResendClick}>
|
||||||
email: email,
|
{t['com.affine.auth.sign.auth.code.resend.hint']()}
|
||||||
callbackUrl: buildCallbackUrl('/auth/signUp'),
|
</Button>
|
||||||
redirect: true,
|
) : (
|
||||||
}).catch(console.error);
|
<>
|
||||||
}, [email])}
|
<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 }}>
|
<div className={style.authMessage} style={{ marginTop: 20 }}>
|
||||||
{t['com.affine.auth.sign.auth.code.message']()}
|
{t['com.affine.auth.sign.auth.code.message']()}
|
||||||
|
|||||||
@@ -3,17 +3,12 @@ import {
|
|||||||
type AuthModalProps as AuthModalBaseProps,
|
type AuthModalProps as AuthModalBaseProps,
|
||||||
} from '@affine/component/auth-components';
|
} from '@affine/component/auth-components';
|
||||||
import { refreshRootMetadataAtom } from '@affine/workspace/atom';
|
import { refreshRootMetadataAtom } from '@affine/workspace/atom';
|
||||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import {
|
import { type FC, startTransition, useCallback, useMemo } from 'react';
|
||||||
type FC,
|
|
||||||
startTransition,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { AfterSignInSendEmail } from './after-sign-in-send-email';
|
import { AfterSignInSendEmail } from './after-sign-in-send-email';
|
||||||
import { AfterSignUpSendEmail } from './after-sign-up-send-email';
|
import { AfterSignUpSendEmail } from './after-sign-up-send-email';
|
||||||
|
import { NoAccess } from './no-access';
|
||||||
import { SendEmail } from './send-email';
|
import { SendEmail } from './send-email';
|
||||||
import { SignIn } from './sign-in';
|
import { SignIn } from './sign-in';
|
||||||
import { SignInWithPassword } from './sign-in-with-password';
|
import { SignInWithPassword } from './sign-in-with-password';
|
||||||
@@ -25,7 +20,8 @@ export type AuthProps = {
|
|||||||
| 'afterSignInSendEmail'
|
| 'afterSignInSendEmail'
|
||||||
// throw away
|
// throw away
|
||||||
| 'signInWithPassword'
|
| 'signInWithPassword'
|
||||||
| 'sendEmail';
|
| 'sendEmail'
|
||||||
|
| 'noAccess';
|
||||||
setAuthState: (state: AuthProps['state']) => void;
|
setAuthState: (state: AuthProps['state']) => void;
|
||||||
setAuthEmail: (state: AuthProps['email']) => void;
|
setAuthEmail: (state: AuthProps['email']) => void;
|
||||||
setEmailType: (state: AuthProps['emailType']) => void;
|
setEmailType: (state: AuthProps['emailType']) => void;
|
||||||
@@ -41,8 +37,6 @@ export type AuthPanelProps = {
|
|||||||
setEmailType: AuthProps['setEmailType'];
|
setEmailType: AuthProps['setEmailType'];
|
||||||
emailType: AuthProps['emailType'];
|
emailType: AuthProps['emailType'];
|
||||||
onSignedIn?: () => void;
|
onSignedIn?: () => void;
|
||||||
authStore: AuthStoreAtom;
|
|
||||||
setAuthStore: (data: Partial<AuthStoreAtom>) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const config: {
|
const config: {
|
||||||
@@ -53,17 +47,9 @@ const config: {
|
|||||||
afterSignInSendEmail: AfterSignInSendEmail,
|
afterSignInSendEmail: AfterSignInSendEmail,
|
||||||
signInWithPassword: SignInWithPassword,
|
signInWithPassword: SignInWithPassword,
|
||||||
sendEmail: SendEmail,
|
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> = ({
|
export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
|
||||||
open,
|
open,
|
||||||
state,
|
state,
|
||||||
@@ -74,18 +60,6 @@ export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
|
|||||||
setEmailType,
|
setEmailType,
|
||||||
emailType,
|
emailType,
|
||||||
}) => {
|
}) => {
|
||||||
const [, setAuthStore] = useAtom(authStoreAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
setAuthStore({
|
|
||||||
hasSentEmail: false,
|
|
||||||
resendCountDown: 60,
|
|
||||||
});
|
|
||||||
setAuthEmail('');
|
|
||||||
}
|
|
||||||
}, [open, setAuthEmail, setAuthStore]);
|
|
||||||
|
|
||||||
const refreshMetadata = useSetAtom(refreshRootMetadataAtom);
|
const refreshMetadata = useSetAtom(refreshRootMetadataAtom);
|
||||||
|
|
||||||
const onSignedIn = useCallback(() => {
|
const onSignedIn = useCallback(() => {
|
||||||
@@ -119,39 +93,18 @@ export const AuthPanel: FC<AuthProps> = ({
|
|||||||
emailType,
|
emailType,
|
||||||
onSignedIn,
|
onSignedIn,
|
||||||
}) => {
|
}) => {
|
||||||
const [authStore, setAuthStore] = useAtom(authStoreAtom);
|
|
||||||
|
|
||||||
const CurrentPanel = useMemo(() => {
|
const CurrentPanel = useMemo(() => {
|
||||||
return config[state];
|
return config[state];
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
setAuthStore({
|
|
||||||
hasSentEmail: false,
|
|
||||||
resendCountDown: 60,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [setAuthEmail, setAuthStore]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CurrentPanel
|
<CurrentPanel
|
||||||
email={email}
|
email={email}
|
||||||
setAuthState={setAuthState}
|
setAuthState={setAuthState}
|
||||||
setAuthEmail={setAuthEmail}
|
setAuthEmail={setAuthEmail}
|
||||||
setEmailType={setEmailType}
|
setEmailType={setEmailType}
|
||||||
authStore={authStore}
|
|
||||||
emailType={emailType}
|
emailType={emailType}
|
||||||
onSignedIn={onSignedIn}
|
onSignedIn={onSignedIn}
|
||||||
setAuthStore={useCallback(
|
|
||||||
(data: Partial<AuthStoreAtom>) => {
|
|
||||||
setAuthStore(prev => ({
|
|
||||||
...prev,
|
|
||||||
...data,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
[setAuthStore]
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
53
apps/core/src/components/affine/auth/no-access.tsx
Normal file
53
apps/core/src/components/affine/auth/no-access.tsx
Normal 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])}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -16,7 +16,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|||||||
import { useMutation } from '@affine/workspace/affine/gql';
|
import { useMutation } from '@affine/workspace/affine/gql';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import { useSetAtom } from 'jotai/react';
|
import { useSetAtom } from 'jotai/react';
|
||||||
import { type FC, useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import type { AuthPanelProps } from './index';
|
import type { AuthPanelProps } from './index';
|
||||||
|
|
||||||
@@ -118,14 +118,13 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SendEmail: FC<AuthPanelProps> = ({
|
export const SendEmail = ({
|
||||||
setAuthState,
|
setAuthState,
|
||||||
setAuthStore,
|
|
||||||
email,
|
email,
|
||||||
authStore: { hasSentEmail },
|
|
||||||
emailType,
|
emailType,
|
||||||
}) => {
|
}: AuthPanelProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const [hasSentEmail, setHasSentEmail] = useState(false);
|
||||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
|
|
||||||
const title = useEmailTitle(emailType);
|
const title = useEmailTitle(emailType);
|
||||||
@@ -143,8 +142,8 @@ export const SendEmail: FC<AuthPanelProps> = ({
|
|||||||
key: Date.now().toString(),
|
key: Date.now().toString(),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
setAuthStore({ hasSentEmail: true });
|
setHasSentEmail(true);
|
||||||
}, [email, hint, pushNotification, sendEmail, setAuthStore]);
|
}, [email, hint, pushNotification, sendEmail]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,62 +1,52 @@
|
|||||||
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
|
import {
|
||||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
AuthInput,
|
||||||
import type { Notification } from '@affine/component/notification-center/index.jotai';
|
CountDownRender,
|
||||||
import { isDesktop } from '@affine/env/constant';
|
ModalHeader,
|
||||||
|
} from '@affine/component/auth-components';
|
||||||
import { getUserQuery } from '@affine/graphql';
|
import { getUserQuery } from '@affine/graphql';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useMutation } from '@affine/workspace/affine/gql';
|
import { useMutation } from '@affine/workspace/affine/gql';
|
||||||
import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
|
import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import { type SignInResponse } from 'next-auth/react';
|
|
||||||
import { type FC, useState } from 'react';
|
import { type FC, useState } from 'react';
|
||||||
import { useCallback } 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 { emailRegex } from '../../../utils/email-regex';
|
||||||
import { buildCallbackUrl } from './callback-url';
|
|
||||||
import type { AuthPanelProps } from './index';
|
import type { AuthPanelProps } from './index';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
|
import { useAuth } from './use-auth';
|
||||||
|
|
||||||
function validateEmail(email: string) {
|
function validateEmail(email: string) {
|
||||||
return emailRegex.test(email);
|
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> = ({
|
export const SignIn: FC<AuthPanelProps> = ({
|
||||||
setAuthState,
|
setAuthState,
|
||||||
setAuthEmail,
|
setAuthEmail,
|
||||||
email,
|
email,
|
||||||
|
onSignedIn,
|
||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const loginStatus = useCurrentLoginStatus();
|
||||||
|
|
||||||
|
const { resendCountDown, allowSendEmail, signIn, signUp, signInWithGoogle } =
|
||||||
|
useAuth({
|
||||||
|
onNoAccess: useCallback(() => {
|
||||||
|
setAuthState('noAccess');
|
||||||
|
}, [setAuthState]),
|
||||||
|
});
|
||||||
|
|
||||||
const { trigger: verifyUser, isMutating } = useMutation({
|
const { trigger: verifyUser, isMutating } = useMutation({
|
||||||
mutation: getUserQuery,
|
mutation: getUserQuery,
|
||||||
});
|
});
|
||||||
const [isValidEmail, setIsValidEmail] = useState(true);
|
const [isValidEmail, setIsValidEmail] = useState(true);
|
||||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
|
||||||
|
if (loginStatus === 'authenticated') {
|
||||||
|
onSignedIn?.();
|
||||||
|
}
|
||||||
|
|
||||||
const onContinue = useCallback(async () => {
|
const onContinue = useCallback(async () => {
|
||||||
if (!validateEmail(email)) {
|
if (!validateEmail(email)) {
|
||||||
setIsValidEmail(false);
|
setIsValidEmail(false);
|
||||||
@@ -68,26 +58,16 @@ export const SignIn: FC<AuthPanelProps> = ({
|
|||||||
|
|
||||||
setAuthEmail(email);
|
setAuthEmail(email);
|
||||||
if (user) {
|
if (user) {
|
||||||
signInCloud('email', {
|
|
||||||
email: email,
|
|
||||||
callbackUrl: buildCallbackUrl('/auth/signIn'),
|
|
||||||
redirect: false,
|
|
||||||
})
|
|
||||||
.then(res => handleSendEmailError(res, pushNotification))
|
|
||||||
.catch(console.error);
|
|
||||||
setAuthState('afterSignInSendEmail');
|
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');
|
setAuthState('afterSignUpSendEmail');
|
||||||
|
|
||||||
|
await signUp(email);
|
||||||
}
|
}
|
||||||
}, [email, setAuthEmail, setAuthState, verifyUser, pushNotification]);
|
}, [email, setAuthEmail, setAuthState, signIn, signUp, verifyUser]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
@@ -103,18 +83,9 @@ export const SignIn: FC<AuthPanelProps> = ({
|
|||||||
marginTop: 30,
|
marginTop: 30,
|
||||||
}}
|
}}
|
||||||
icon={<GoogleDuotoneIcon />}
|
icon={<GoogleDuotoneIcon />}
|
||||||
onClick={useCallback(() => {
|
onClick={useCallback(async () => {
|
||||||
if (isDesktop) {
|
await signInWithGoogle();
|
||||||
open(
|
}, [signInWithGoogle])}
|
||||||
`/desktop-signin?provider=google&callback_url=${buildCallbackUrl(
|
|
||||||
'/open-app/oauth-jwt'
|
|
||||||
)}`,
|
|
||||||
'_target'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
signInCloud('google').catch(console.error);
|
|
||||||
}
|
|
||||||
}, [])}
|
|
||||||
>
|
>
|
||||||
{t['Continue with Google']()}
|
{t['Continue with Google']()}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -142,15 +113,23 @@ export const SignIn: FC<AuthPanelProps> = ({
|
|||||||
data-testid="continue-login-button"
|
data-testid="continue-login-button"
|
||||||
block
|
block
|
||||||
loading={isMutating}
|
loading={isMutating}
|
||||||
|
disabled={!allowSendEmail}
|
||||||
icon={
|
icon={
|
||||||
<ArrowDownBigIcon
|
allowSendEmail || isMutating ? (
|
||||||
width={20}
|
<ArrowDownBigIcon
|
||||||
height={20}
|
width={20}
|
||||||
style={{
|
height={20}
|
||||||
transform: 'rotate(-90deg)',
|
style={{
|
||||||
color: 'var(--affine-blue)',
|
transform: 'rotate(-90deg)',
|
||||||
}}
|
color: 'var(--affine-blue)',
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CountDownRender
|
||||||
|
className={style.resendCountdownInButton}
|
||||||
|
timeLeft={resendCountDown}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
iconPosition="end"
|
iconPosition="end"
|
||||||
onClick={onContinue}
|
onClick={onContinue}
|
||||||
|
|||||||
@@ -26,3 +26,32 @@ export const forgetPasswordButton = style({
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
display: 'none',
|
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,
|
||||||
|
});
|
||||||
|
|||||||
136
apps/core/src/components/affine/auth/use-auth.ts
Normal file
136
apps/core/src/components/affine/auth/use-auth.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -10,7 +10,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|||||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||||
import { ArrowRightSmallIcon, CameraIcon, DoneIcon } from '@blocksuite/icons';
|
import { ArrowRightSmallIcon, CameraIcon, DoneIcon } from '@blocksuite/icons';
|
||||||
import { Button, IconButton } from '@toeverything/components/button';
|
import { Button, IconButton } from '@toeverything/components/button';
|
||||||
import { useAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { type FC, Suspense, useCallback, useState } from 'react';
|
import { type FC, Suspense, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { authAtom } from '../../../../atoms';
|
import { authAtom } from '../../../../atoms';
|
||||||
@@ -137,7 +137,7 @@ const StoragePanel = () => {
|
|||||||
export const AccountSetting: FC = () => {
|
export const AccountSetting: FC = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
const [, setAuthModal] = useAtom(authAtom);
|
const setAuthModal = useSetAtom(authAtom);
|
||||||
|
|
||||||
const onChangeEmail = useCallback(() => {
|
const onChangeEmail = useCallback(() => {
|
||||||
setAuthModal({
|
setAuthModal({
|
||||||
@@ -147,14 +147,15 @@ export const AccountSetting: FC = () => {
|
|||||||
emailType: 'changeEmail',
|
emailType: 'changeEmail',
|
||||||
});
|
});
|
||||||
}, [setAuthModal, user.email]);
|
}, [setAuthModal, user.email]);
|
||||||
const onChangePassword = useCallback(() => {
|
|
||||||
|
const onPasswordButtonClick = useCallback(() => {
|
||||||
setAuthModal({
|
setAuthModal({
|
||||||
openModal: true,
|
openModal: true,
|
||||||
state: 'sendEmail',
|
state: 'sendEmail',
|
||||||
email: user.email,
|
email: user.email,
|
||||||
emailType: 'changePassword',
|
emailType: user.hasPassword ? 'changePassword' : 'setPassword',
|
||||||
});
|
});
|
||||||
}, [setAuthModal, user.email]);
|
}, [setAuthModal, user.email, user.hasPassword]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -173,7 +174,7 @@ export const AccountSetting: FC = () => {
|
|||||||
name={t['com.affine.settings.password']()}
|
name={t['com.affine.settings.password']()}
|
||||||
desc={t['com.affine.settings.password.message']()}
|
desc={t['com.affine.settings.password.message']()}
|
||||||
>
|
>
|
||||||
<Button onClick={onChangePassword}>
|
<Button onClick={onPasswordButtonClick}>
|
||||||
{user.hasPassword
|
{user.hasPassword
|
||||||
? t['com.affine.settings.password.action.change']()
|
? t['com.affine.settings.password.action.change']()
|
||||||
: t['com.affine.settings.password.action.set']()}
|
: t['com.affine.settings.password.action.set']()}
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ export const settingContent = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
globalStyle(`${settingContent} .wrapper`, {
|
globalStyle(`${settingContent} .wrapper`, {
|
||||||
width: '60%',
|
|
||||||
padding: '0 15px',
|
padding: '0 15px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
minWidth: '560px',
|
maxWidth: '560px',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,43 +85,65 @@ export class MailService {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendSignInEmail(url: string, options: Options) {
|
||||||
|
const html = emailTemplate({
|
||||||
|
title: 'Sign in to AFFiNE',
|
||||||
|
content:
|
||||||
|
'Click the button below to securely sign in. The magic link will expire in 30 minutes.',
|
||||||
|
buttonContent: 'Sign in to AFFiNE',
|
||||||
|
buttonUrl: url,
|
||||||
|
});
|
||||||
|
return this.sendMail({
|
||||||
|
html,
|
||||||
|
subject: 'Sign in to AFFiNE',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async sendChangePasswordEmail(to: string, url: string) {
|
async sendChangePasswordEmail(to: string, url: string) {
|
||||||
const html = `
|
const html = emailTemplate({
|
||||||
<h1>Change password</h1>
|
title: 'Modify your AFFiNE password',
|
||||||
<p>Click button to open change password page</p>
|
content:
|
||||||
<a href="${url}">${url}</a>
|
'Click the button below to reset your password. The magic link will expire in 30 minutes.',
|
||||||
`;
|
buttonContent: 'Set new password',
|
||||||
|
buttonUrl: url,
|
||||||
|
});
|
||||||
return this.sendMail({
|
return this.sendMail({
|
||||||
from: this.config.auth.email.sender,
|
from: this.config.auth.email.sender,
|
||||||
to,
|
to,
|
||||||
subject: `Change password`,
|
subject: `Modify your AFFiNE password`,
|
||||||
html,
|
html,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendSetPasswordEmail(to: string, url: string) {
|
async sendSetPasswordEmail(to: string, url: string) {
|
||||||
const html = `
|
const html = emailTemplate({
|
||||||
<h1>Set password</h1>
|
title: 'Set your AFFiNE password',
|
||||||
<p>Click button to open set password page</p>
|
content:
|
||||||
<a href="${url}">${url}</a>
|
'Click the button below to set your password. The magic link will expire in 30 minutes.',
|
||||||
`;
|
buttonContent: 'Set your password',
|
||||||
|
buttonUrl: url,
|
||||||
|
});
|
||||||
return this.sendMail({
|
return this.sendMail({
|
||||||
from: this.config.auth.email.sender,
|
from: this.config.auth.email.sender,
|
||||||
to,
|
to,
|
||||||
subject: `Change password`,
|
subject: `Set your AFFiNE password`,
|
||||||
html,
|
html,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async sendChangeEmail(to: string, url: string) {
|
async sendChangeEmail(to: string, url: string) {
|
||||||
const html = `
|
const html = emailTemplate({
|
||||||
<h1>Change Email</h1>
|
title: 'Verify your current email for AFFiNE',
|
||||||
<p>Click button to open change email page</p>
|
content:
|
||||||
<a href="${url}">${url}</a>
|
'You recently requested to change the email address associated with your AFFiNE account. To complete this process, please click on the verification link below. This magic link will expire in 30 minutes.',
|
||||||
`;
|
buttonContent: 'Verify and set up a new email address',
|
||||||
|
buttonUrl: url,
|
||||||
|
});
|
||||||
return this.sendMail({
|
return this.sendMail({
|
||||||
from: this.config.auth.email.sender,
|
from: this.config.auth.email.sender,
|
||||||
to,
|
to,
|
||||||
subject: `Change password`,
|
subject: `Verify your current email for AFFiNE`,
|
||||||
html,
|
html,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,25 +88,24 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
|||||||
from: config.auth.email.sender,
|
from: config.auth.email.sender,
|
||||||
async sendVerificationRequest(params: SendVerificationRequestParams) {
|
async sendVerificationRequest(params: SendVerificationRequestParams) {
|
||||||
const { identifier, url, provider } = params;
|
const { identifier, url, provider } = params;
|
||||||
const { host, searchParams, origin } = new URL(url);
|
const { searchParams, origin } = new URL(url);
|
||||||
const callbackUrl = searchParams.get('callbackUrl') || '';
|
const callbackUrl = searchParams.get('callbackUrl') || '';
|
||||||
if (!callbackUrl) {
|
if (!callbackUrl) {
|
||||||
throw new Error('callbackUrl is not set');
|
throw new Error('callbackUrl is not set');
|
||||||
}
|
}
|
||||||
// hack: check if link is opened via desktop
|
|
||||||
const schema = getSchemaFromCallbackUrl(origin, callbackUrl);
|
const schema = getSchemaFromCallbackUrl(origin, callbackUrl);
|
||||||
const wrappedUrl = wrapUrlWithOpenApp(origin, url, schema);
|
const wrappedUrl = wrapUrlWithOpenApp(origin, url, schema);
|
||||||
|
|
||||||
const result = await mailer.sendMail({
|
// hack: check if link is opened via desktop
|
||||||
|
const result = await mailer.sendSignInEmail(wrappedUrl, {
|
||||||
to: identifier,
|
to: identifier,
|
||||||
from: provider.from,
|
from: provider.from,
|
||||||
subject: `Sign in to ${host}`,
|
|
||||||
text: text({ url: wrappedUrl, host }),
|
|
||||||
html: html({ url: wrappedUrl, host }),
|
|
||||||
});
|
});
|
||||||
logger.log(
|
logger.log(
|
||||||
`send verification email success: ${result.accepted.join(', ')}`
|
`send verification email success: ${result.accepted.join(', ')}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const failed = result.rejected
|
const failed = result.rejected
|
||||||
.concat(result.pending)
|
.concat(result.pending)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@@ -298,211 +297,3 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
|||||||
},
|
},
|
||||||
inject: [Config, PrismaService, MailService],
|
inject: [Config, PrismaService, MailService],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Email HTML body
|
|
||||||
* Insert invisible space into domains from being turned into a hyperlink by email
|
|
||||||
* clients like Outlook and Apple mail, as this is confusing because it seems
|
|
||||||
* like they are supposed to click on it to sign in.
|
|
||||||
*
|
|
||||||
* @note We don't add the email address to avoid needing to escape it, if you do, remember to sanitize it!
|
|
||||||
*/
|
|
||||||
function html(params: { url: string; host: string }) {
|
|
||||||
const { url } = params;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<body style="background: #f6f7fb;overflow:hidden">
|
|
||||||
<table
|
|
||||||
width="100%"
|
|
||||||
border="0"
|
|
||||||
cellpadding="24px"
|
|
||||||
style="
|
|
||||||
background: #fff;
|
|
||||||
max-width: 450px;
|
|
||||||
margin: 32px auto 0 auto;
|
|
||||||
border-radius: 16px 16px 0 0;
|
|
||||||
box-shadow: 0px 0px 20px 0px rgba(66, 65, 73, 0.04);
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="https://affine.pro" target="_blank">
|
|
||||||
<img
|
|
||||||
src="https://cdn.affine.pro/mail/2023-8-9/affine-logo.png"
|
|
||||||
alt="AFFiNE log"
|
|
||||||
height="32px"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 28px;
|
|
||||||
font-family: inter, Arial, Helvetica, sans-serif;
|
|
||||||
color: #444;
|
|
||||||
padding-top: 0;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Verify your new email for AFFiNE
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 24px;
|
|
||||||
font-family: inter, Arial, Helvetica, sans-serif;
|
|
||||||
color: #444;
|
|
||||||
padding-top: 0;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
You recently requested to change the email address associated with your
|
|
||||||
AFFiNe account. To complete this process, please click on the
|
|
||||||
verification link below.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="margin-left: 24px; padding-top: 0; padding-bottom: 64px">
|
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
|
||||||
<tr>
|
|
||||||
<td style="border-radius: 8px" bgcolor="#1E96EB">
|
|
||||||
<a
|
|
||||||
href="${url}"
|
|
||||||
target="_blank"
|
|
||||||
style="
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inter, Arial, Helvetica, sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 24px;
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 18px;
|
|
||||||
border: 1px solid #1e96eb;
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: bold;
|
|
||||||
"
|
|
||||||
>Verify your new email address</a
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<table
|
|
||||||
width="100%"
|
|
||||||
border="0"
|
|
||||||
style="
|
|
||||||
background: #fafafa;
|
|
||||||
max-width: 450px;
|
|
||||||
margin: 0 auto 32px auto;
|
|
||||||
border-radius: 0 0 16px 16px;
|
|
||||||
box-shadow: 0px 0px 20px 0px rgba(66, 65, 73, 0.04);
|
|
||||||
padding: 20px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<tr align="center">
|
|
||||||
<td>
|
|
||||||
<table cellpadding="0">
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 0 10px">
|
|
||||||
<a href="https://github.com/toeverything/AFFiNE" target="_blank"
|
|
||||||
><img
|
|
||||||
src="https://cdn.affine.pro/mail/2023-8-9/Github.png"
|
|
||||||
alt="AFFiNE github link"
|
|
||||||
height="16px"
|
|
||||||
/></a>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 0 10px">
|
|
||||||
<a href="https://twitter.com/AffineOfficial" target="_blank">
|
|
||||||
<img
|
|
||||||
src="https://cdn.affine.pro/mail/2023-8-9/Twitter.png"
|
|
||||||
alt="AFFiNE twitter link"
|
|
||||||
height="16px"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 0 10px">
|
|
||||||
<a href="https://discord.gg/Arn7TqJBvG" target="_blank"
|
|
||||||
><img
|
|
||||||
src="https://cdn.affine.pro/mail/2023-8-9/Discord.png"
|
|
||||||
alt="AFFiNE discord link"
|
|
||||||
height="16px"
|
|
||||||
/></a>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 0 10px">
|
|
||||||
<a href="https://www.youtube.com/@affinepro" target="_blank"
|
|
||||||
><img
|
|
||||||
src="https://cdn.affine.pro/mail/2023-8-9/Youtube.png"
|
|
||||||
alt="AFFiNE youtube link"
|
|
||||||
height="16px"
|
|
||||||
/></a>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 0 10px">
|
|
||||||
<a href="https://t.me/affineworkos" target="_blank"
|
|
||||||
><img
|
|
||||||
src="https://cdn.affine.pro/mail/2023-8-9/Telegram.png"
|
|
||||||
alt="AFFiNE telegram link"
|
|
||||||
height="16px"
|
|
||||||
/></a>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 0 10px">
|
|
||||||
<a href="https://www.reddit.com/r/Affine/" target="_blank"
|
|
||||||
><img
|
|
||||||
src="https://cdn.affine.pro/mail/2023-8-9/Reddit.png"
|
|
||||||
alt="AFFiNE reddit link"
|
|
||||||
height="16px"
|
|
||||||
/></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr align="center">
|
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 20px;
|
|
||||||
font-family: inter, Arial, Helvetica, sans-serif;
|
|
||||||
color: #8e8d91;
|
|
||||||
padding-top: 8px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
One hyper-fused platform for wildly creative minds
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr align="center">
|
|
||||||
<td
|
|
||||||
style="
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 20px;
|
|
||||||
font-family: inter, Arial, Helvetica, sans-serif;
|
|
||||||
color: #8e8d91;
|
|
||||||
padding-top: 8px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Copyright<img
|
|
||||||
src="https://cdn.affine.pro/mail/2023-8-9/copyright.png"
|
|
||||||
alt="copyright"
|
|
||||||
height="14px"
|
|
||||||
style="vertical-align: middle; margin: 0 4px"
|
|
||||||
/>2023 Toeverything
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */
|
|
||||||
function text({ url, host }: { url: string; host: string }) {
|
|
||||||
return `Sign in to ${host}\n${url}\n\n`;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { forwardRef, type HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
const formatTime = (time: number): string => {
|
||||||
|
const minutes = Math.floor(time / 60);
|
||||||
|
const seconds = time % 60;
|
||||||
|
|
||||||
|
const formattedMinutes = minutes.toString().padStart(2, '0');
|
||||||
|
const formattedSeconds = seconds.toString().padStart(2, '0');
|
||||||
|
|
||||||
|
return `${formattedMinutes}:${formattedSeconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CountDownRender = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
{ timeLeft: number } & HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ timeLeft, ...props }) => {
|
||||||
|
return <div {...props}>{formatTime(timeLeft)}</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
CountDownRender.displayName = 'CountDownRender';
|
||||||
@@ -4,10 +4,10 @@ export * from './auth-page-container';
|
|||||||
export * from './back-button';
|
export * from './back-button';
|
||||||
export * from './change-email-page';
|
export * from './change-email-page';
|
||||||
export * from './change-password-page';
|
export * from './change-password-page';
|
||||||
|
export * from './count-down-render';
|
||||||
export * from './modal';
|
export * from './modal';
|
||||||
export * from './modal-header';
|
export * from './modal-header';
|
||||||
export * from './password-input';
|
export * from './password-input';
|
||||||
export * from './resend-button';
|
|
||||||
export * from './set-password-page';
|
export * from './set-password-page';
|
||||||
export * from './sign-in-page-container';
|
export * from './sign-in-page-container';
|
||||||
export * from './sign-in-success-page';
|
export * from './sign-in-success-page';
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|
||||||
import { Button } from '@toeverything/components/button';
|
|
||||||
import { type FC, useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { resendButtonWrapper } from './share.css';
|
|
||||||
|
|
||||||
const formatTime = (time: number): string => {
|
|
||||||
const minutes = Math.floor(time / 60);
|
|
||||||
const seconds = time % 60;
|
|
||||||
|
|
||||||
const formattedMinutes = minutes.toString().padStart(2, '0');
|
|
||||||
const formattedSeconds = seconds.toString().padStart(2, '0');
|
|
||||||
|
|
||||||
return `${formattedMinutes}:${formattedSeconds}`;
|
|
||||||
};
|
|
||||||
const CountDown: FC<{
|
|
||||||
seconds: number;
|
|
||||||
onEnd?: () => void;
|
|
||||||
}> = ({ seconds, onEnd }) => {
|
|
||||||
const [timeLeft, setTimeLeft] = useState(seconds);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (timeLeft === 0) {
|
|
||||||
onEnd?.();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
setTimeLeft(timeLeft - 1);
|
|
||||||
|
|
||||||
if (timeLeft - 1 === 0) {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, [onEnd, timeLeft]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ width: 45, textAlign: 'center' }}>{formatTime(timeLeft)}</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ResendButton: FC<{
|
|
||||||
onClick: () => void;
|
|
||||||
countDownSeconds?: number;
|
|
||||||
}> = ({ onClick, countDownSeconds = 60 }) => {
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
const [canResend, setCanResend] = useState(false);
|
|
||||||
|
|
||||||
const onButtonClick = useCallback(() => {
|
|
||||||
onClick();
|
|
||||||
setCanResend(false);
|
|
||||||
}, [onClick]);
|
|
||||||
|
|
||||||
const onCountDownEnd = useCallback(() => {
|
|
||||||
setCanResend(true);
|
|
||||||
}, [setCanResend]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={resendButtonWrapper}>
|
|
||||||
{canResend ? (
|
|
||||||
<Button type="plain" size="large" onClick={onButtonClick}>
|
|
||||||
{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>
|
|
||||||
<CountDown seconds={countDownSeconds} onEnd={onCountDownEnd} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -47,7 +47,6 @@ export const SetPasswordPage: FC<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<h1>This is set page</h1>
|
|
||||||
{hasSetUp ? (
|
{hasSetUp ? (
|
||||||
<Button type="primary" size="large" onClick={onOpenAffine}>
|
<Button type="primary" size="large" onClick={onOpenAffine}>
|
||||||
{t['com.affine.auth.open.affine']()}
|
{t['com.affine.auth.open.affine']()}
|
||||||
|
|||||||
@@ -433,6 +433,10 @@
|
|||||||
"com.affine.auth.sign.sent.email.message.end": " You can click the link to create an account automatically.",
|
"com.affine.auth.sign.sent.email.message.end": " You can click the link to create an account automatically.",
|
||||||
"com.affine.auth.sign.up.success.title": "Your account has been created and you’re now signed in!",
|
"com.affine.auth.sign.up.success.title": "Your account has been created and you’re now signed in!",
|
||||||
"com.affine.auth.sign.up.success.subtitle": "The app will automatically open or redirect to the web version. If you encounter any issues, you can also click the button below to manually open the AFFiNE app.",
|
"com.affine.auth.sign.up.success.subtitle": "The app will automatically open or redirect to the web version. If you encounter any issues, you can also click the button below to manually open the AFFiNE app.",
|
||||||
|
"Early Access Stage": "Early Access Stage",
|
||||||
|
"com.affine.auth.sign.no.access.hint": "AFFiNE Cloud is in early access. Check out this link to learn more about the benefits of becoming an AFFiNE Cloud Early Supporter: ",
|
||||||
|
"com.affine.auth.sign.no.access.link": "AFFiNE Cloud Early Access",
|
||||||
|
"com.affine.auth.sign.no.access.wait": "Wait for public release",
|
||||||
"com.affine.auth.page.sent.email.title": "Welcome to AFFiNE Cloud, you are almost there!",
|
"com.affine.auth.page.sent.email.title": "Welcome to AFFiNE Cloud, you are almost there!",
|
||||||
"com.affine.auth.page.sent.email.subtitle": "Please set a password of 8-20 characters with both letters and numbers to continue signing up with ",
|
"com.affine.auth.page.sent.email.subtitle": "Please set a password of 8-20 characters with both letters and numbers to continue signing up with ",
|
||||||
"com.affine.auth.later": "Later",
|
"com.affine.auth.later": "Later",
|
||||||
|
|||||||
Reference in New Issue
Block a user