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

View File

@@ -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) {
const html = `
<h1>Change password</h1>
<p>Click button to open change password page</p>
<a href="${url}">${url}</a>
`;
const html = emailTemplate({
title: 'Modify your AFFiNE password',
content:
'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({
from: this.config.auth.email.sender,
to,
subject: `Change password`,
subject: `Modify your AFFiNE password`,
html,
});
}
async sendSetPasswordEmail(to: string, url: string) {
const html = `
<h1>Set password</h1>
<p>Click button to open set password page</p>
<a href="${url}">${url}</a>
`;
const html = emailTemplate({
title: 'Set your AFFiNE password',
content:
'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({
from: this.config.auth.email.sender,
to,
subject: `Change password`,
subject: `Set your AFFiNE password`,
html,
});
}
async sendChangeEmail(to: string, url: string) {
const html = `
<h1>Change Email</h1>
<p>Click button to open change email page</p>
<a href="${url}">${url}</a>
`;
const html = emailTemplate({
title: 'Verify your current email for AFFiNE',
content:
'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({
from: this.config.auth.email.sender,
to,
subject: `Change password`,
subject: `Verify your current email for AFFiNE`,
html,
});
}

View File

@@ -88,25 +88,24 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
from: config.auth.email.sender,
async sendVerificationRequest(params: SendVerificationRequestParams) {
const { identifier, url, provider } = params;
const { host, searchParams, origin } = new URL(url);
const { searchParams, origin } = new URL(url);
const callbackUrl = searchParams.get('callbackUrl') || '';
if (!callbackUrl) {
throw new Error('callbackUrl is not set');
}
// hack: check if link is opened via desktop
const schema = getSchemaFromCallbackUrl(origin, callbackUrl);
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,
from: provider.from,
subject: `Sign in to ${host}`,
text: text({ url: wrappedUrl, host }),
html: html({ url: wrappedUrl, host }),
});
logger.log(
`send verification email success: ${result.accepted.join(', ')}`
);
const failed = result.rejected
.concat(result.pending)
.filter(Boolean);
@@ -298,211 +297,3 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
},
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`;
}

View File

@@ -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';

View File

@@ -4,10 +4,10 @@ export * from './auth-page-container';
export * from './back-button';
export * from './change-email-page';
export * from './change-password-page';
export * from './count-down-render';
export * from './modal';
export * from './modal-header';
export * from './password-input';
export * from './resend-button';
export * from './set-password-page';
export * from './sign-in-page-container';
export * from './sign-in-success-page';

View File

@@ -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>
);
};

View File

@@ -47,7 +47,6 @@ export const SetPasswordPage: FC<{
)
}
>
<h1>This is set page</h1>
{hasSetUp ? (
<Button type="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}

View File

@@ -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.up.success.title": "Your account has been created and youre 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.",
"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.subtitle": "Please set a password of 8-20 characters with both letters and numbers to continue signing up with ",
"com.affine.auth.later": "Later",