mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-23 13:02:21 +08:00
feat(core): support subscribe plan after login (#4788)
This commit is contained in:
@@ -7,13 +7,14 @@ import {
|
|||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||||
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';
|
import { useAuth } from './use-auth';
|
||||||
import { Captcha, useCaptcha } from './use-captcha';
|
import { Captcha, useCaptcha } from './use-captcha';
|
||||||
|
import { useSubscriptionSearch } from './use-subscription';
|
||||||
|
|
||||||
export const AfterSignInSendEmail = ({
|
export const AfterSignInSendEmail = ({
|
||||||
setAuthState,
|
setAuthState,
|
||||||
@@ -23,6 +24,7 @@ export const AfterSignInSendEmail = ({
|
|||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const loginStatus = useCurrentLoginStatus();
|
const loginStatus = useCurrentLoginStatus();
|
||||||
const [verifyToken, challenge] = useCaptcha();
|
const [verifyToken, challenge] = useCaptcha();
|
||||||
|
const subscriptionData = useSubscriptionSearch();
|
||||||
|
|
||||||
const { resendCountDown, allowSendEmail, signIn } = useAuth();
|
const { resendCountDown, allowSendEmail, signIn } = useAuth();
|
||||||
if (loginStatus === 'authenticated') {
|
if (loginStatus === 'authenticated') {
|
||||||
@@ -35,6 +37,14 @@ export const AfterSignInSendEmail = ({
|
|||||||
}
|
}
|
||||||
}, [challenge, email, signIn, verifyToken]);
|
}, [challenge, email, signIn, verifyToken]);
|
||||||
|
|
||||||
|
const onSignInWithPasswordClick = useCallback(() => {
|
||||||
|
setAuthState('signInWithPassword');
|
||||||
|
}, [setAuthState]);
|
||||||
|
|
||||||
|
const onBackBottomClick = useCallback(() => {
|
||||||
|
setAuthState('signIn');
|
||||||
|
}, [setAuthState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
@@ -75,26 +85,25 @@ export const AfterSignInSendEmail = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={style.authMessage} style={{ marginTop: 20 }}>
|
<div className={style.authMessage} style={{ marginTop: 20 }}>
|
||||||
{/*prettier-ignore*/}
|
{t['com.affine.auth.sign.auth.code.message']()}
|
||||||
<Trans i18nKey="com.affine.auth.sign.auth.code.message.password">
|
{subscriptionData ? null : ( // If with payment, just support email sign in to avoid duplicate redirect to the same stripe url.
|
||||||
If you haven't received the email, please check your spam folder.
|
<React.Fragment>
|
||||||
Or <span
|
|
||||||
className="link"
|
{/*prettier-ignore*/}
|
||||||
data-testid='sign-in-with-password'
|
<Trans i18nKey="com.affine.auth.sign.auth.code.message.password">
|
||||||
onClick={useCallback(() => {
|
Or <span
|
||||||
setAuthState('signInWithPassword');
|
className="link"
|
||||||
}, [setAuthState])}
|
data-testid='sign-in-with-password'
|
||||||
>
|
onClick={onSignInWithPasswordClick}
|
||||||
sign in with password
|
>
|
||||||
</span> instead.
|
sign in with password
|
||||||
</Trans>
|
</span> instead.
|
||||||
|
</Trans>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BackButton
|
<BackButton onClick={onBackBottomClick} />
|
||||||
onClick={useCallback(() => {
|
|
||||||
setAuthState('signIn');
|
|
||||||
}, [setAuthState])}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const loadingContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
width: '100vw',
|
||||||
|
height: '60vh',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { SubscriptionRecurring } from '@affine/graphql';
|
||||||
|
import { checkoutMutation } from '@affine/graphql';
|
||||||
|
import { useMutation } from '@affine/workspace/affine/gql';
|
||||||
|
import { Loading } from '@toeverything/components/loading';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { type FC, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './subscription-redirect.css';
|
||||||
|
import { useSubscriptionSearch } from './use-subscription';
|
||||||
|
|
||||||
|
export const SubscriptionRedirect: FC = () => {
|
||||||
|
const subscriptionData = useSubscriptionSearch();
|
||||||
|
const idempotencyKey = useMemo(() => nanoid(), []);
|
||||||
|
const { trigger: checkoutSubscription } = useMutation({
|
||||||
|
mutation: checkoutMutation,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!subscriptionData) {
|
||||||
|
throw new Error('No subscription data found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// This component will be render multiple times, use timeout to avoid multiple effect.
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
const recurring = subscriptionData.recurring as SubscriptionRecurring;
|
||||||
|
checkoutSubscription({ recurring, idempotencyKey }).then(data => {
|
||||||
|
window.open(data.checkout, '_self', 'norefferer');
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Just run this once, do not react to changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.loadingContainer}>
|
||||||
|
<Loading size={40} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import { type SignInResponse } from 'next-auth/react';
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { signInCloud } from '../../../utils/cloud-utils';
|
import { signInCloud } from '../../../utils/cloud-utils';
|
||||||
|
import { useSubscriptionSearch } from './use-subscription';
|
||||||
|
|
||||||
const COUNT_DOWN_TIME = 60;
|
const COUNT_DOWN_TIME = 60;
|
||||||
export const INTERNAL_BETA_URL = `https://community.affine.pro/c/insider-general/`;
|
export const INTERNAL_BETA_URL = `https://community.affine.pro/c/insider-general/`;
|
||||||
@@ -58,6 +59,7 @@ const countDownAtom = atom(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
|
const subscriptionData = useSubscriptionSearch();
|
||||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
const [authStore, setAuthStore] = useAtom(authStoreAtom);
|
const [authStore, setAuthStore] = useAtom(authStoreAtom);
|
||||||
const startResendCountDown = useSetAtom(countDownAtom);
|
const startResendCountDown = useSetAtom(countDownAtom);
|
||||||
@@ -75,7 +77,9 @@ export const useAuth = () => {
|
|||||||
'email',
|
'email',
|
||||||
{
|
{
|
||||||
email: email,
|
email: email,
|
||||||
callbackUrl: '/auth/signIn',
|
callbackUrl: subscriptionData
|
||||||
|
? subscriptionData.redirectUrl
|
||||||
|
: '/auth/signIn',
|
||||||
redirect: false,
|
redirect: false,
|
||||||
},
|
},
|
||||||
challenge
|
challenge
|
||||||
@@ -98,7 +102,7 @@ export const useAuth = () => {
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
[pushNotification, setAuthStore, startResendCountDown]
|
[pushNotification, setAuthStore, startResendCountDown, subscriptionData]
|
||||||
);
|
);
|
||||||
|
|
||||||
const signUp = useCallback(
|
const signUp = useCallback(
|
||||||
@@ -114,7 +118,9 @@ export const useAuth = () => {
|
|||||||
'email',
|
'email',
|
||||||
{
|
{
|
||||||
email: email,
|
email: email,
|
||||||
callbackUrl: '/auth/signUp',
|
callbackUrl: subscriptionData
|
||||||
|
? subscriptionData.redirectUrl
|
||||||
|
: '/auth/signUp',
|
||||||
redirect: false,
|
redirect: false,
|
||||||
},
|
},
|
||||||
challenge
|
challenge
|
||||||
@@ -137,7 +143,7 @@ export const useAuth = () => {
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
[pushNotification, setAuthStore, startResendCountDown]
|
[pushNotification, setAuthStore, startResendCountDown, subscriptionData]
|
||||||
);
|
);
|
||||||
|
|
||||||
const signInWithGoogle = useCallback(() => {
|
const signInWithGoogle = useCallback(() => {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
enum SubscriptionKey {
|
||||||
|
Recurring = 'subscription_recurring',
|
||||||
|
Plan = 'subscription_plan',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSubscriptionSearch() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const withPayment =
|
||||||
|
searchParams.has(SubscriptionKey.Recurring) &&
|
||||||
|
searchParams.has(SubscriptionKey.Plan);
|
||||||
|
|
||||||
|
if (!withPayment) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recurring = searchParams.get(SubscriptionKey.Recurring);
|
||||||
|
const plan = searchParams.get(SubscriptionKey.Plan);
|
||||||
|
return {
|
||||||
|
recurring,
|
||||||
|
plan,
|
||||||
|
get redirectUrl() {
|
||||||
|
const paymentParams = new URLSearchParams([
|
||||||
|
[SubscriptionKey.Recurring, recurring ?? ''],
|
||||||
|
[SubscriptionKey.Plan, plan ?? ''],
|
||||||
|
]);
|
||||||
|
return `/auth/subscription-redirect?${paymentParams.toString()}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [searchParams]);
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { SubscriptionRedirect } from '../components/affine/auth/subscription-redirect';
|
||||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||||
import { useCurrentUser } from '../hooks/affine/use-current-user';
|
import { useCurrentUser } from '../hooks/affine/use-current-user';
|
||||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
@@ -36,6 +37,7 @@ const authTypeSchema = z.enum([
|
|||||||
'signUp',
|
'signUp',
|
||||||
'changeEmail',
|
'changeEmail',
|
||||||
'confirm-change-email',
|
'confirm-change-email',
|
||||||
|
'subscription-redirect',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const AuthPage = (): ReactElement | null => {
|
export const AuthPage = (): ReactElement | null => {
|
||||||
@@ -132,6 +134,9 @@ export const AuthPage = (): ReactElement | null => {
|
|||||||
case 'confirm-change-email': {
|
case 'confirm-change-email': {
|
||||||
return <ConfirmChangeEmail onOpenAffine={onOpenAffine} />;
|
return <ConfirmChangeEmail onOpenAffine={onOpenAffine} />;
|
||||||
}
|
}
|
||||||
|
case 'subscription-redirect': {
|
||||||
|
return <SubscriptionRedirect />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
"com.affine.auth.set.password.save": "Save Password",
|
"com.affine.auth.set.password.save": "Save Password",
|
||||||
"com.affine.auth.sign.auth.code.error.hint": "Wrong code, please try again",
|
"com.affine.auth.sign.auth.code.error.hint": "Wrong code, please try again",
|
||||||
"com.affine.auth.sign.auth.code.message": "If you haven't received the email, please check your spam folder.",
|
"com.affine.auth.sign.auth.code.message": "If you haven't received the email, please check your spam folder.",
|
||||||
"com.affine.auth.sign.auth.code.message.password": "If you haven't received the email, please check your spam folder. Or <1>sign in with password</1> instead.",
|
"com.affine.auth.sign.auth.code.message.password": "Or <1>sign in with password</1> instead.",
|
||||||
"com.affine.auth.sign.auth.code.on.resend.hint": "Send code again",
|
"com.affine.auth.sign.auth.code.on.resend.hint": "Send code again",
|
||||||
"com.affine.auth.sign.auth.code.resend.hint": "Resend code",
|
"com.affine.auth.sign.auth.code.resend.hint": "Resend code",
|
||||||
"com.affine.auth.sign.condition": "Terms of Conditions",
|
"com.affine.auth.sign.condition": "Terms of Conditions",
|
||||||
|
|||||||
Reference in New Issue
Block a user