diff --git a/packages/frontend/component/src/components/auth-components/change-password-page.tsx b/packages/frontend/component/src/components/auth-components/change-password-page.tsx index 01680417d..f48bc56f8 100644 --- a/packages/frontend/component/src/components/auth-components/change-password-page.tsx +++ b/packages/frontend/component/src/components/auth-components/change-password-page.tsx @@ -1,8 +1,10 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { Button } from '@toeverything/components/button'; +import { useSetAtom } from 'jotai'; import type { FC } from 'react'; import { useCallback, useState } from 'react'; +import { pushNotificationAtom } from '../notification-center'; import { AuthPageContainer } from './auth-page-container'; import { SetPassword } from './set-password'; type User = { @@ -14,18 +16,27 @@ type User = { export const ChangePasswordPage: FC<{ user: User; - onSetPassword: (password: string) => void; + onSetPassword: (password: string) => Promise; onOpenAffine: () => void; }> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => { const t = useAFFiNEI18N(); const [hasSetUp, setHasSetUp] = useState(false); + const pushNotification = useSetAtom(pushNotificationAtom); const onSetPassword = useCallback( (passWord: string) => { - propsOnSetPassword(passWord); - setHasSetUp(true); + propsOnSetPassword(passWord) + .then(() => setHasSetUp(true)) + .catch(e => + pushNotification({ + title: t['com.affine.auth.password.set-failed'](), + message: String(e), + key: Date.now().toString(), + type: 'error', + }) + ); }, - [propsOnSetPassword] + [propsOnSetPassword, t, pushNotification] ); return ( diff --git a/packages/frontend/component/src/components/auth-components/set-password-page.tsx b/packages/frontend/component/src/components/auth-components/set-password-page.tsx index cf126f883..026462fd7 100644 --- a/packages/frontend/component/src/components/auth-components/set-password-page.tsx +++ b/packages/frontend/component/src/components/auth-components/set-password-page.tsx @@ -1,8 +1,10 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { Button } from '@toeverything/components/button'; +import { useSetAtom } from 'jotai'; import type { FC } from 'react'; import { useCallback, useState } from 'react'; +import { pushNotificationAtom } from '../notification-center'; import { AuthPageContainer } from './auth-page-container'; import { SetPassword } from './set-password'; @@ -15,18 +17,27 @@ type User = { export const SetPasswordPage: FC<{ user: User; - onSetPassword: (password: string) => void; + onSetPassword: (password: string) => Promise; onOpenAffine: () => void; }> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => { const t = useAFFiNEI18N(); const [hasSetUp, setHasSetUp] = useState(false); + const pushNotification = useSetAtom(pushNotificationAtom); const onSetPassword = useCallback( (passWord: string) => { - propsOnSetPassword(passWord); - setHasSetUp(true); + propsOnSetPassword(passWord) + .then(() => setHasSetUp(true)) + .catch(e => + pushNotification({ + title: t['com.affine.auth.password.set-failed'](), + message: String(e), + key: Date.now().toString(), + type: 'error', + }) + ); }, - [propsOnSetPassword] + [propsOnSetPassword, pushNotification, t] ); return ( diff --git a/packages/frontend/component/src/components/auth-components/sign-up-page.tsx b/packages/frontend/component/src/components/auth-components/sign-up-page.tsx index fe0bd8730..f8055f11e 100644 --- a/packages/frontend/component/src/components/auth-components/sign-up-page.tsx +++ b/packages/frontend/component/src/components/auth-components/sign-up-page.tsx @@ -1,8 +1,10 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { Button } from '@toeverything/components/button'; +import { useSetAtom } from 'jotai'; import type { FC } from 'react'; import { useCallback, useState } from 'react'; +import { pushNotificationAtom } from '../notification-center'; import { AuthPageContainer } from './auth-page-container'; import { SetPassword } from './set-password'; type User = { @@ -14,18 +16,33 @@ type User = { export const SignUpPage: FC<{ user: User; - onSetPassword: (password: string) => void; + onSetPassword: (password: string) => Promise; + openButtonText?: string; onOpenAffine: () => void; -}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => { +}> = ({ + user: { email }, + onSetPassword: propsOnSetPassword, + onOpenAffine, + openButtonText, +}) => { const t = useAFFiNEI18N(); const [hasSetUp, setHasSetUp] = useState(false); + const pushNotification = useSetAtom(pushNotificationAtom); const onSetPassword = useCallback( (passWord: string) => { - propsOnSetPassword(passWord); - setHasSetUp(true); + propsOnSetPassword(passWord) + .then(() => setHasSetUp(true)) + .catch(e => + pushNotification({ + title: t['com.affine.auth.password.set-failed'](), + message: String(e), + key: Date.now().toString(), + type: 'error', + }) + ); }, - [propsOnSetPassword] + [propsOnSetPassword, pushNotification, t] ); const onLater = useCallback(() => { setHasSetUp(true); @@ -51,7 +68,7 @@ export const SignUpPage: FC<{ > {hasSetUp ? ( ) : ( { + const searchData = useSubscriptionSearch(); + if (!searchData?.recurring) { + throw new Error('Invalid recurring data.'); + } + + const recurring = searchData.recurring as SubscriptionRecurring; + const idempotencyKey = useMemo(() => nanoid(), []); + const { trigger: checkoutSubscription } = useMutation({ + mutation: checkoutMutation, + }); + + return useCallback(() => { + checkoutSubscription({ recurring, idempotencyKey }) + .then(({ checkout }) => { + window.open(checkout, '_self', 'norefferer'); + }) + .catch(e => console.error(e)); + }, [recurring, idempotencyKey, checkoutSubscription]); +}; + const CenterLoading = () => { return (
@@ -51,54 +78,65 @@ const SubscriptionExisting = () => { ); }; -const SubscriptionRedirectInner: FC = () => { - const subscriptionData = useSubscriptionSearch(); - const idempotencyKey = useMemo(() => nanoid(), []); - const { data } = useQuery({ - query: subscriptionQuery, - }); - const { trigger: checkoutSubscription } = useMutation({ - mutation: checkoutMutation, - }); - +const SubscriptionRedirection = ({ redirect }: { redirect: () => void }) => { useEffect(() => { - if (!subscriptionData) { - throw new Error('No subscription data found'); - } - - if (data.currentUser?.subscription) { - return; - } - - // 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( - ({ checkout }) => { - window.open(checkout, '_self', 'norefferer'); - } - ); + redirect(); }, 100); return () => { clearTimeout(timeoutId); }; + }, [redirect]); - // Just run this once, do not react to changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + return ; +}; - if (data.currentUser?.subscription) { +const SubscriptionRedirectWithData = () => { + const t = useAFFiNEI18N(); + const user = useCurrentUser(); + const searchData = useSubscriptionSearch(); + const openPaymentUrl = usePaymentRedirect(); + + const { trigger: changePassword } = useMutation({ + mutation: changePasswordMutation, + }); + const { data: subscriptionData } = useQuery({ + query: subscriptionQuery, + }); + + const onSetPassword = useCallback( + async (password: string) => { + await changePassword({ + token: searchData?.passwordToken ?? '', + newPassword: password, + }); + }, + [changePassword, searchData] + ); + + if (searchData?.withSignUp) { + return ( + + ); + } + + if (subscriptionData.currentUser?.subscription) { return ; } - return ; + return ; }; export const SubscriptionRedirect = () => { return ( }> - + ); }; diff --git a/packages/frontend/core/src/components/affine/auth/use-auth.ts b/packages/frontend/core/src/components/affine/auth/use-auth.ts index 91f3e294e..e2b338b01 100644 --- a/packages/frontend/core/src/components/affine/auth/use-auth.ts +++ b/packages/frontend/core/src/components/affine/auth/use-auth.ts @@ -78,7 +78,7 @@ export const useAuth = () => { { email: email, callbackUrl: subscriptionData - ? subscriptionData.redirectUrl + ? subscriptionData.getRedirectUrl(false) : '/auth/signIn', redirect: false, }, @@ -119,7 +119,7 @@ export const useAuth = () => { { email: email, callbackUrl: subscriptionData - ? subscriptionData.redirectUrl + ? subscriptionData.getRedirectUrl(true) : '/auth/signUp', redirect: false, }, diff --git a/packages/frontend/core/src/components/affine/auth/use-subscription.ts b/packages/frontend/core/src/components/affine/auth/use-subscription.ts index f398255e4..b13902c56 100644 --- a/packages/frontend/core/src/components/affine/auth/use-subscription.ts +++ b/packages/frontend/core/src/components/affine/auth/use-subscription.ts @@ -4,6 +4,8 @@ import { useSearchParams } from 'react-router-dom'; enum SubscriptionKey { Recurring = 'subscription_recurring', Plan = 'subscription_plan', + SignUp = 'sign_up', // A new user with subscription journey: signup > set password > pay in stripe > go to app + Token = 'token', // When signup, there should have a token to set password } export function useSubscriptionSearch() { @@ -20,14 +22,23 @@ export function useSubscriptionSearch() { const recurring = searchParams.get(SubscriptionKey.Recurring); const plan = searchParams.get(SubscriptionKey.Plan); + const withSignUp = searchParams.get(SubscriptionKey.SignUp) === '1'; + const passwordToken = searchParams.get(SubscriptionKey.Token); return { recurring, plan, - get redirectUrl() { + withSignUp, + passwordToken, + getRedirectUrl(signUp?: boolean) { const paymentParams = new URLSearchParams([ [SubscriptionKey.Recurring, recurring ?? ''], [SubscriptionKey.Plan, plan ?? ''], ]); + + if (signUp) { + paymentParams.set(SubscriptionKey.SignUp, '1'); + } + return `/auth/subscription-redirect?${paymentParams.toString()}`; }, }; diff --git a/packages/frontend/core/src/pages/auth.tsx b/packages/frontend/core/src/pages/auth.tsx index 95f2f2231..e84a7a189 100644 --- a/packages/frontend/core/src/pages/auth.tsx +++ b/packages/frontend/core/src/pages/auth.tsx @@ -80,11 +80,11 @@ export const AuthPage = (): ReactElement | null => { ); const onSetPassword = useCallback( - (password: string) => { - changePassword({ + async (password: string) => { + await changePassword({ token: searchParams.get('token') || '', newPassword: password, - }).catch(console.error); + }); }, [changePassword, searchParams] ); diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 07cca76d4..bc73c4fdf 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -86,6 +86,7 @@ "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.title": "Welcome to AFFiNE Cloud, you are almost there!", "com.affine.auth.password": "Password", + "com.affine.auth.password.set-failed": "Set Password Failed", "com.affine.auth.password.error": "Invalid password", "com.affine.auth.reset.password": "Reset Password", "com.affine.auth.reset.password.message": "You will receive an email with a link to reset your password. Please check your inbox.", @@ -767,6 +768,7 @@ "com.affine.payment.upgrade-success-page.text": "Congratulations! Your AFFiNE account has been successfully upgraded to a Pro account.", "com.affine.payment.upgrade-success-page.support": "If you have any questions, please contact our <1> customer support.", "com.affine.payment.subscription.exist": "You already have a subscription.", + "com.affine.payment.subscription.go-to-subscribe": "Subscribe AFFiNE", "com.affine.other-page.nav.official-website": "Official Website", "com.affine.other-page.nav.affine-community": "AFFiNE Community", "com.affine.other-page.nav.blog": "Blog",