feat: support sign-in with subscription coupon (#5768)
This commit is contained in:
@@ -23,6 +23,7 @@ import type { AuthPanelProps } from './index';
|
|||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
import { INTERNAL_BETA_URL, useAuth } from './use-auth';
|
import { INTERNAL_BETA_URL, useAuth } from './use-auth';
|
||||||
import { Captcha, useCaptcha } from './use-captcha';
|
import { Captcha, useCaptcha } from './use-captcha';
|
||||||
|
import { useSubscriptionSearch } from './use-subscription';
|
||||||
|
|
||||||
function validateEmail(email: string) {
|
function validateEmail(email: string) {
|
||||||
return emailRegex.test(email);
|
return emailRegex.test(email);
|
||||||
@@ -37,6 +38,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
|||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const loginStatus = useCurrentLoginStatus();
|
const loginStatus = useCurrentLoginStatus();
|
||||||
const [verifyToken, challenge] = useCaptcha();
|
const [verifyToken, challenge] = useCaptcha();
|
||||||
|
const subscriptionData = useSubscriptionSearch();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isMutating: isSigningIn,
|
isMutating: isSigningIn,
|
||||||
@@ -83,7 +85,8 @@ export const SignIn: FC<AuthPanelProps> = ({
|
|||||||
if (verifyToken) {
|
if (verifyToken) {
|
||||||
if (user) {
|
if (user) {
|
||||||
// provider password sign-in if user has by default
|
// provider password sign-in if user has by default
|
||||||
if (user.hasPassword) {
|
// If with payment, onl support email sign in to avoid redirect to affine app
|
||||||
|
if (user.hasPassword && !subscriptionData) {
|
||||||
setAuthState('signInWithPassword');
|
setAuthState('signInWithPassword');
|
||||||
} else {
|
} else {
|
||||||
const res = await signIn(email, verifyToken, challenge);
|
const res = await signIn(email, verifyToken, challenge);
|
||||||
@@ -103,6 +106,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
subscriptionData,
|
||||||
challenge,
|
challenge,
|
||||||
email,
|
email,
|
||||||
setAuthEmail,
|
setAuthEmail,
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { Button } from '@affine/component/ui/button';
|
|||||||
import { Loading } from '@affine/component/ui/loading';
|
import { Loading } from '@affine/component/ui/loading';
|
||||||
import { AffineShapeIcon } from '@affine/core/components/page-list';
|
import { AffineShapeIcon } from '@affine/core/components/page-list';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import type { SubscriptionRecurring } from '@affine/graphql';
|
import type { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||||
import {
|
import {
|
||||||
changePasswordMutation,
|
changePasswordMutation,
|
||||||
checkoutMutation,
|
createCheckoutSessionMutation,
|
||||||
subscriptionQuery,
|
subscriptionQuery,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -30,18 +30,25 @@ const usePaymentRedirect = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const recurring = searchData.recurring as SubscriptionRecurring;
|
const recurring = searchData.recurring as SubscriptionRecurring;
|
||||||
|
const plan = searchData.plan as SubscriptionPlan;
|
||||||
|
const coupon = searchData.coupon;
|
||||||
const idempotencyKey = useMemo(() => nanoid(), []);
|
const idempotencyKey = useMemo(() => nanoid(), []);
|
||||||
const { trigger: checkoutSubscription } = useMutation({
|
const { trigger: checkoutSubscription } = useMutation({
|
||||||
mutation: checkoutMutation,
|
mutation: createCheckoutSessionMutation,
|
||||||
});
|
});
|
||||||
|
|
||||||
return useAsyncCallback(async () => {
|
return useAsyncCallback(async () => {
|
||||||
const { checkout } = await checkoutSubscription({
|
const { createCheckoutSession: checkoutUrl } = await checkoutSubscription({
|
||||||
recurring,
|
input: {
|
||||||
idempotencyKey,
|
recurring,
|
||||||
|
plan,
|
||||||
|
coupon,
|
||||||
|
idempotencyKey,
|
||||||
|
successCallbackLink: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
window.open(checkout, '_self', 'norefferer');
|
window.open(checkoutUrl, '_self', 'norefferer');
|
||||||
}, [recurring, idempotencyKey, checkoutSubscription]);
|
}, [recurring, plan, coupon, idempotencyKey, checkoutSubscription]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CenterLoading = () => {
|
const CenterLoading = () => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
enum SubscriptionKey {
|
enum SubscriptionKey {
|
||||||
Recurring = 'subscription_recurring',
|
Recurring = 'subscription_recurring',
|
||||||
Plan = 'subscription_plan',
|
Plan = 'subscription_plan',
|
||||||
|
Coupon = 'coupon',
|
||||||
SignUp = 'sign_up', // A new user with subscription journey: signup > set password > pay in stripe > go to app
|
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
|
Token = 'token', // When signup, there should have a token to set password
|
||||||
}
|
}
|
||||||
@@ -22,11 +23,13 @@ export function useSubscriptionSearch() {
|
|||||||
|
|
||||||
const recurring = searchParams.get(SubscriptionKey.Recurring);
|
const recurring = searchParams.get(SubscriptionKey.Recurring);
|
||||||
const plan = searchParams.get(SubscriptionKey.Plan);
|
const plan = searchParams.get(SubscriptionKey.Plan);
|
||||||
|
const coupon = searchParams.get(SubscriptionKey.Coupon);
|
||||||
const withSignUp = searchParams.get(SubscriptionKey.SignUp) === '1';
|
const withSignUp = searchParams.get(SubscriptionKey.SignUp) === '1';
|
||||||
const passwordToken = searchParams.get(SubscriptionKey.Token);
|
const passwordToken = searchParams.get(SubscriptionKey.Token);
|
||||||
return {
|
return {
|
||||||
recurring,
|
recurring,
|
||||||
plan,
|
plan,
|
||||||
|
coupon,
|
||||||
withSignUp,
|
withSignUp,
|
||||||
passwordToken,
|
passwordToken,
|
||||||
getRedirectUrl(signUp?: boolean) {
|
getRedirectUrl(signUp?: boolean) {
|
||||||
@@ -35,6 +38,10 @@ export function useSubscriptionSearch() {
|
|||||||
[SubscriptionKey.Plan, plan ?? ''],
|
[SubscriptionKey.Plan, plan ?? ''],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (coupon) {
|
||||||
|
paymentParams.set(SubscriptionKey.Coupon, coupon);
|
||||||
|
}
|
||||||
|
|
||||||
if (signUp) {
|
if (signUp) {
|
||||||
paymentParams.set(SubscriptionKey.SignUp, '1');
|
paymentParams.set(SubscriptionKey.SignUp, '1');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
SubscriptionMutator,
|
SubscriptionMutator,
|
||||||
} from '@affine/core/hooks/use-subscription';
|
} from '@affine/core/hooks/use-subscription';
|
||||||
import {
|
import {
|
||||||
checkoutMutation,
|
createCheckoutSessionMutation,
|
||||||
SubscriptionPlan,
|
SubscriptionPlan,
|
||||||
SubscriptionRecurring,
|
SubscriptionRecurring,
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
@@ -359,7 +359,7 @@ const Upgrade = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const { isMutating, trigger } = useMutation({
|
const { isMutating, trigger } = useMutation({
|
||||||
mutation: checkoutMutation,
|
mutation: createCheckoutSessionMutation,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newTabRef = useRef<Window | null>(null);
|
const newTabRef = useRef<Window | null>(null);
|
||||||
@@ -383,13 +383,21 @@ const Upgrade = ({
|
|||||||
newTabRef.current.focus();
|
newTabRef.current.focus();
|
||||||
} else {
|
} else {
|
||||||
await trigger(
|
await trigger(
|
||||||
{ recurring, idempotencyKey },
|
{
|
||||||
|
input: {
|
||||||
|
recurring,
|
||||||
|
idempotencyKey,
|
||||||
|
plan: SubscriptionPlan.Pro, // Only support prod plan now.
|
||||||
|
coupon: null,
|
||||||
|
successCallbackLink: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
// FIXME: safari prevents from opening new tab by window api
|
// FIXME: safari prevents from opening new tab by window api
|
||||||
// TODO(@xp): what if electron?
|
// TODO(@xp): what if electron?
|
||||||
const newTab = window.open(
|
const newTab = window.open(
|
||||||
data.checkout,
|
data.createCheckoutSession,
|
||||||
'_blank',
|
'_blank',
|
||||||
'noopener noreferrer'
|
'noopener noreferrer'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
mutation checkout(
|
mutation createCheckoutSession($input: CreateCheckoutSessionInput!) {
|
||||||
$recurring: SubscriptionRecurring!
|
createCheckoutSession(input: $input)
|
||||||
$idempotencyKey: String!
|
|
||||||
) {
|
|
||||||
checkout(recurring: $recurring, idempotencyKey: $idempotencyKey)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,14 +127,14 @@ mutation changePassword($token: String!, $newPassword: String!) {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkoutMutation = {
|
export const createCheckoutSessionMutation = {
|
||||||
id: 'checkoutMutation' as const,
|
id: 'createCheckoutSessionMutation' as const,
|
||||||
operationName: 'checkout',
|
operationName: 'createCheckoutSession',
|
||||||
definitionName: 'checkout',
|
definitionName: 'createCheckoutSession',
|
||||||
containsFile: false,
|
containsFile: false,
|
||||||
query: `
|
query: `
|
||||||
mutation checkout($recurring: SubscriptionRecurring!, $idempotencyKey: String!) {
|
mutation createCheckoutSession($input: CreateCheckoutSessionInput!) {
|
||||||
checkout(recurring: $recurring, idempotencyKey: $idempotencyKey)
|
createCheckoutSession(input: $input)
|
||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -200,12 +200,14 @@ export type ChangePasswordMutation = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CheckoutMutationVariables = Exact<{
|
export type CreateCheckoutSessionMutationVariables = Exact<{
|
||||||
recurring: SubscriptionRecurring;
|
input: CreateCheckoutSessionInput;
|
||||||
idempotencyKey: Scalars['String']['input'];
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type CheckoutMutation = { __typename?: 'Mutation'; checkout: string };
|
export type CreateCheckoutSessionMutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
createCheckoutSession: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateCustomerPortalMutationVariables = Exact<{
|
export type CreateCustomerPortalMutationVariables = Exact<{
|
||||||
[key: string]: never;
|
[key: string]: never;
|
||||||
@@ -1049,9 +1051,9 @@ export type Mutations =
|
|||||||
response: ChangePasswordMutation;
|
response: ChangePasswordMutation;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
name: 'checkoutMutation';
|
name: 'createCheckoutSessionMutation';
|
||||||
variables: CheckoutMutationVariables;
|
variables: CreateCheckoutSessionMutationVariables;
|
||||||
response: CheckoutMutation;
|
response: CreateCheckoutSessionMutation;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
name: 'createCustomerPortalMutation';
|
name: 'createCustomerPortalMutation';
|
||||||
|
|||||||
Reference in New Issue
Block a user