Create successful payment
This commit is contained in:
parent
a9e897d99e
commit
c60e9bdc32
|
@ -1,90 +1,68 @@
|
||||||
import { PaymentElement, useStripe, useElements } from "@stripe/react-stripe-js";
|
import { PaymentElement, useStripe, useElements } from "@stripe/react-stripe-js";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { NewTeamData } from "@calcom/features/ee/teams/lib/types";
|
import { NewTeamData } from "@calcom/features/ee/teams/lib/types";
|
||||||
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Switch, Button } from "@calcom/ui/v2";
|
import { Switch, Button } from "@calcom/ui/v2";
|
||||||
|
|
||||||
const PurchaseNewTeam = ({
|
const PurchaseNewTeam = ({
|
||||||
newTeamData,
|
|
||||||
paymentIntent,
|
paymentIntent,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
teamPrices,
|
total,
|
||||||
|
billingFrequency,
|
||||||
|
newTeamData,
|
||||||
}: {
|
}: {
|
||||||
newTeamData: NewTeamData;
|
|
||||||
paymentIntent: string;
|
paymentIntent: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
teamPrices: {
|
total: number;
|
||||||
monthly: number;
|
billingFrequency: string;
|
||||||
yearly: number;
|
newTeamData: NewTeamData;
|
||||||
};
|
|
||||||
}) => {
|
}) => {
|
||||||
const [message, setMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const [billingFrequency, setBillingFrequency] = useState("monthly");
|
|
||||||
const [paymentProcessing, setPaymentProcessing] = useState(false);
|
const [paymentProcessing, setPaymentProcessing] = useState(false);
|
||||||
const stripe = useStripe();
|
const stripe = useStripe();
|
||||||
const elements = useElements();
|
const elements = useElements();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const updatePaymentIntentMutation = trpc.useMutation(["viewer.teams.mutatePaymentIntent"]);
|
const createTeamMutation = trpc.useMutation(["viewer.teams.createTeam"], {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
router.push(`${CAL_URL}/settings/teams/${data.id}/profile`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Handle Stripe payment
|
const handleSubmit = async () => {
|
||||||
useEffect(() => {
|
if (!stripe || !elements) return;
|
||||||
if (!stripe) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!paymentIntent) {
|
setPaymentProcessing(true);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => {
|
const { error } = await stripe.confirmPayment({
|
||||||
switch (paymentIntent.status) {
|
elements,
|
||||||
case "succeeded":
|
confirmParams: {
|
||||||
setMessage("Payment succeeded!");
|
return_url: `${CAL_URL}/settings/profile`,
|
||||||
break;
|
},
|
||||||
case "processing":
|
redirect: "if_required",
|
||||||
setMessage("Your payment is processing.");
|
|
||||||
break;
|
|
||||||
case "requires_payment_method":
|
|
||||||
setMessage("Your payment was not successful, please try again.");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setMessage("Something went wrong.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, [stripe]);
|
|
||||||
|
if (error) {
|
||||||
|
console.log("🚀 ~ file: PurchaseNewTeam.tsx ~ line 71 ~ handleSubmit ~ error", error);
|
||||||
|
} else {
|
||||||
|
createTeamMutation.mutate(newTeamData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <div className="flex justify-between">
|
|
||||||
<p>Total</p>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
{newTeamData.members.length} members x ${teamPrices[billingFrequency as keyof typeof teamPrices]}{" "}
|
|
||||||
/ {billingFrequency} ={" "}
|
|
||||||
{newTeamData.members.length * teamPrices[billingFrequency as keyof typeof teamPrices]}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div className="mt-4 flex space-x-2">
|
|
||||||
<Switch onClick={() => setBillingFrequency(billingFrequency === "monthly" ? "yearly" : "monthly")} />
|
|
||||||
<p>
|
|
||||||
Switch to yearly and save{" "}
|
|
||||||
{newTeamData.members.length * (teamPrices.monthly * 12 - teamPrices.yearly)}
|
|
||||||
</p>
|
|
||||||
</div> */}
|
|
||||||
<hr className="my-4" />
|
|
||||||
<PaymentElement />
|
<PaymentElement />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="mt-4 w-full justify-center"
|
className="mt-4 w-full justify-center"
|
||||||
loading={paymentProcessing}
|
loading={paymentProcessing}
|
||||||
onClick={() => handleSubmit()}>
|
onClick={() => handleSubmit()}>
|
||||||
Pay ${teamPrices[billingFrequency as keyof typeof teamPrices]}
|
Pay ${total} / {billingFrequency}
|
||||||
</Button>
|
</Button>
|
||||||
|
<p>Error processing payment: {errorMessage}</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Elements } from "@stripe/react-stripe-js";
|
import { Elements, useElements } from "@stripe/react-stripe-js";
|
||||||
import { loadStripe } from "@stripe/stripe-js";
|
import { loadStripe } from "@stripe/stripe-js";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
@ -45,6 +45,7 @@ const CreateNewTeamPage = () => {
|
||||||
slug: "",
|
slug: "",
|
||||||
logo: "",
|
logo: "",
|
||||||
members: [],
|
members: [],
|
||||||
|
billingFrequency: "monthly",
|
||||||
});
|
});
|
||||||
const [clientSecret, setClientSecret] = useState("");
|
const [clientSecret, setClientSecret] = useState("");
|
||||||
const [paymentIntent, setPaymentIntent] = useState("");
|
const [paymentIntent, setPaymentIntent] = useState("");
|
||||||
|
@ -82,25 +83,19 @@ const CreateNewTeamPage = () => {
|
||||||
|
|
||||||
const currentStepIndex = steps.indexOf(currentStep);
|
const currentStepIndex = steps.indexOf(currentStep);
|
||||||
|
|
||||||
const createPaymentIntentMutation = trpc.useMutation(["viewer.teams.mutatePaymentIntent"], {
|
const createPaymentIntentMutation = trpc.useMutation(["viewer.teams.createPaymentIntent"], {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
console.log("🚀 ~ file: [[...step]].tsx ~ line 86 ~ CreateNewTeamPage ~ data", data);
|
setClientSecret(data.clientSecret);
|
||||||
setClientSecret(data.client_secret);
|
goToIndex(2);
|
||||||
setPaymentIntent(data.id);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getTeamPricesQuery = trpc.useQuery(["viewer.teams.getTeamPrices"], {
|
const getTeamPricesQuery = trpc.useQuery(["viewer.teams.getTeamPrices"], {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
setTeamPrices(data);
|
setTeamPrices(data);
|
||||||
console.log("🚀 ~ file: [[...step]].tsx ~ line 95 ~ CreateNewTeamPage ~ data", data);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
createPaymentIntentMutation.mutate({ amount: 1500 });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="dark:bg-brand dark:text-brand-contrast min-h-screen text-black"
|
className="dark:bg-brand dark:text-brand-contrast min-h-screen text-black"
|
||||||
|
@ -133,7 +128,6 @@ const CreateNewTeamPage = () => {
|
||||||
<CreateNewTeam
|
<CreateNewTeam
|
||||||
nextStep={(values: NewTeamFormValues) => {
|
nextStep={(values: NewTeamFormValues) => {
|
||||||
setNewTeamData({ ...values, members: [] });
|
setNewTeamData({ ...values, members: [] });
|
||||||
localStorage.setItem("newTeamValues", JSON.stringify(values));
|
|
||||||
goToIndex(1);
|
goToIndex(1);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -146,17 +140,21 @@ const CreateNewTeamPage = () => {
|
||||||
{currentStep === "add-team-members" && (
|
{currentStep === "add-team-members" && (
|
||||||
<AddNewTeamMembers
|
<AddNewTeamMembers
|
||||||
teamPrices={teamPrices}
|
teamPrices={teamPrices}
|
||||||
nextStep={(values: PendingMember[]) => {
|
nextStep={(values: {
|
||||||
console.log("🚀 ~ file: [[...step]].tsx ~ line 144 ~ CreateNewTeamPage ~ values", values);
|
members: PendingMember[];
|
||||||
setNewTeamData({ ...newTeamData, members: [...values] });
|
billingFrequency: "monthly" | "yearly";
|
||||||
// localStorage.removeItem("newTeamValues");
|
}) => {
|
||||||
// purchaseTeamMutation.mutate({
|
console.log("🚀 ~ file: [[...step]].tsx ~ line 148 ~ CreateNewTeamPage ~ values", values);
|
||||||
// ...newTeamData,
|
createPaymentIntentMutation.mutate({
|
||||||
// members: [...values],
|
teamName: newTeamData.name,
|
||||||
// language: i18n.language,
|
billingFrequency: values.billingFrequency,
|
||||||
// });
|
seats: values.members.length,
|
||||||
createPaymentIntentMutation.mutate({ amount: values.length * 15 * 100 });
|
});
|
||||||
goToIndex(2);
|
setNewTeamData({
|
||||||
|
...newTeamData,
|
||||||
|
members: [...values.members],
|
||||||
|
billingFrequency: values.billingFrequency,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -164,9 +162,13 @@ const CreateNewTeamPage = () => {
|
||||||
{currentStep === "purchase-new-team" && (
|
{currentStep === "purchase-new-team" && (
|
||||||
<Elements stripe={stripe} options={{ clientSecret }}>
|
<Elements stripe={stripe} options={{ clientSecret }}>
|
||||||
<PurchaseNewTeam
|
<PurchaseNewTeam
|
||||||
newTeamData={newTeamData}
|
|
||||||
paymentIntent={paymentIntent}
|
paymentIntent={paymentIntent}
|
||||||
clientSecret={clientSecret}
|
clientSecret={clientSecret}
|
||||||
|
total={
|
||||||
|
newTeamData.members.length *
|
||||||
|
teamPrices[newTeamData.billingFrequency as keyof typeof teamPrices]
|
||||||
|
}
|
||||||
|
newTeamData={newTeamData}
|
||||||
/>
|
/>
|
||||||
</Elements>
|
</Elements>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -21,7 +21,7 @@ const AddNewTeamMembers = ({
|
||||||
nextStep,
|
nextStep,
|
||||||
teamPrices,
|
teamPrices,
|
||||||
}: {
|
}: {
|
||||||
nextStep: (values: PendingMember[]) => void;
|
nextStep: (values: { members: PendingMember[]; billingFrequency: "monthly" | "yearly" }) => void;
|
||||||
teamPrices: {
|
teamPrices: {
|
||||||
monthly: number;
|
monthly: number;
|
||||||
yearly: number;
|
yearly: number;
|
||||||
|
@ -68,7 +68,7 @@ const AddNewTeamMembers = ({
|
||||||
name: session?.data.user.name || "",
|
name: session?.data.user.name || "",
|
||||||
email: session?.data.user.email || "",
|
email: session?.data.user.email || "",
|
||||||
username: session?.data.user.username || "",
|
username: session?.data.user.username || "",
|
||||||
userId: session?.data.user.id || "",
|
id: session?.data.user.id || "",
|
||||||
role: "OWNER",
|
role: "OWNER",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ const AddNewTeamMembers = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form form={formMethods} handleSubmit={(values) => nextStep(values.members)}>
|
<Form form={formMethods} handleSubmit={(values) => nextStep(values)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="members"
|
name="members"
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
|
@ -179,15 +179,24 @@ const AddNewTeamMembers = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className="mt-4 flex space-x-2">
|
<Controller
|
||||||
<Switch
|
control={formMethods.control}
|
||||||
onClick={() => setBillingFrequency(billingFrequency === "monthly" ? "yearly" : "monthly")}
|
name="billingFrequency"
|
||||||
/>
|
defaultValue={"monthly"}
|
||||||
<p>
|
render={(field: { value }) => (
|
||||||
Switch to yearly and save {numberOfMembers * (teamPrices.monthly * 12 - teamPrices.yearly)}
|
<div className="mt-4 flex space-x-2">
|
||||||
</p>
|
<Switch
|
||||||
</div>
|
onCheckedChange={(e) =>
|
||||||
|
formMethods.setValue("billingFrequency", e ? "yearly" : "monthly")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Switch to yearly and save{" "}
|
||||||
|
{numberOfMembers * (teamPrices.monthly * 12 - teamPrices.yearly)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Button EndIcon={Icon.FiArrowRight} className="mt-6 w-full justify-center" type="submit">
|
<Button EndIcon={Icon.FiArrowRight} className="mt-6 w-full justify-center" type="submit">
|
||||||
{t("checkout")}
|
{t("checkout")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -15,18 +15,19 @@ export const createMember = async ({
|
||||||
pendingMember,
|
pendingMember,
|
||||||
language,
|
language,
|
||||||
teamSubscriptionActive,
|
teamSubscriptionActive,
|
||||||
}): {
|
}: {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
|
inviter: string;
|
||||||
pendingMember: PendingMember;
|
pendingMember: PendingMember;
|
||||||
language: string;
|
language: string;
|
||||||
teamSubscriptionActive?: boolean;
|
teamSubscriptionActive?: boolean;
|
||||||
} => {
|
}) => {
|
||||||
if (pendingMember.username) {
|
if (pendingMember.username & pendingMember.id) {
|
||||||
await prisma.membership.create({
|
await prisma.membership.create({
|
||||||
data: {
|
data: {
|
||||||
teamId,
|
teamId,
|
||||||
userId: pendingMember.userId,
|
userId: pendingMember.id,
|
||||||
role: pendingMember.role as MembershipRole,
|
role: pendingMember.role as MembershipRole,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,6 +16,30 @@ export const getTeamPricing = async () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createTeamCustomer = async (teamName: string, ownerEmail: string) => {
|
||||||
|
return await stripe.customers.create({
|
||||||
|
name: teamName,
|
||||||
|
email: ownerEmail,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTeamSubscription = async (customerId: string, billingFrequency: string, seats: number) => {
|
||||||
|
return await stripe.subscriptions.create({
|
||||||
|
customer: customerId,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
price:
|
||||||
|
billingFrequency === "monthly"
|
||||||
|
? process.env.STRIPE_TEAM_MONTHLY_PRICE_ID
|
||||||
|
: process.env.STRIPE_TEAM_YEARLY_PRICE_ID,
|
||||||
|
quantity: seats,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
payment_behavior: "default_incomplete",
|
||||||
|
expand: ["latest_invoice.payment_intent"],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const purchaseTeamSubscription = async (input: { teamId: number; seats: number; email: string }) => {
|
export const purchaseTeamSubscription = async (input: { teamId: number; seats: number; email: string }) => {
|
||||||
const { teamId, seats, email } = input;
|
const { teamId, seats, email } = input;
|
||||||
return await stripe.checkout.sessions.create({
|
return await stripe.checkout.sessions.create({
|
||||||
|
|
|
@ -18,4 +18,5 @@ export interface PendingMember {
|
||||||
sendInviteEmail?: boolean;
|
sendInviteEmail?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NewTeamData = NewTeamFormValues & NewTeamMembersFieldArray;
|
export type NewTeamData = NewTeamFormValues &
|
||||||
|
NewTeamMembersFieldArray & { billingFrequency: "monthly" | "yearly" };
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { createMember } from "@calcom/features/ee/teams/lib/inviteMember";
|
||||||
import {
|
import {
|
||||||
deleteTeamFromStripe,
|
deleteTeamFromStripe,
|
||||||
purchaseTeamSubscription,
|
purchaseTeamSubscription,
|
||||||
|
createTeamCustomer,
|
||||||
|
createTeamSubscription,
|
||||||
createPaymentIntent,
|
createPaymentIntent,
|
||||||
getTeamPricing,
|
getTeamPricing,
|
||||||
updatePaymentIntent,
|
updatePaymentIntent,
|
||||||
|
@ -688,23 +690,69 @@ export const viewerTeamsRouter = createProtectedRouter()
|
||||||
return teamPrices;
|
return teamPrices;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.mutation("mutatePaymentIntent", {
|
.mutation("createPaymentIntent", {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
amount: z.number(),
|
teamName: z.string(),
|
||||||
paymentIntentId: z.string().optional(),
|
billingFrequency: z.string(),
|
||||||
|
seats: z.number(),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const { amount, paymentIntentId } = input;
|
console.log("🚀 ~ file: teams.tsx ~ line 699 ~ resolve ~ input", input);
|
||||||
try {
|
// First create the customer
|
||||||
// If there is no payment intent, then create one
|
const customer = await createTeamCustomer(input.teamName, ctx.user.email);
|
||||||
if (!paymentIntentId) {
|
|
||||||
const paymentIntent = await createPaymentIntent({ amount, receiptEmail: ctx.user.email });
|
|
||||||
return paymentIntent;
|
|
||||||
}
|
|
||||||
|
|
||||||
await updatePaymentIntent({ amount, paymentIntentId });
|
// Create the subscription for the team
|
||||||
} catch (e) {
|
const subscription = await createTeamSubscription(customer.id, input.billingFrequency, input.seats);
|
||||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: e.message });
|
|
||||||
|
// We just need the client secret for the payment intent
|
||||||
|
return { clientSecret: subscription.latest_invoice.payment_intent.client_secret };
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation("createTeam", {
|
||||||
|
input: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
logo: z.string().optional(),
|
||||||
|
members: z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.union([z.string(), z.null()]),
|
||||||
|
email: z.string(),
|
||||||
|
id: z.number().optional(),
|
||||||
|
username: z.union([z.string(), z.null()]),
|
||||||
|
role: z.union([z.literal("OWNER"), z.literal("ADMIN"), z.literal("MEMBER")]),
|
||||||
|
avatar: z.union([z.string(), z.null()]).optional(),
|
||||||
|
sendInviteEmail: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const { name, slug, logo, members } = input;
|
||||||
|
|
||||||
|
const createTeam = await ctx.prisma.team.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
logo,
|
||||||
|
subscriptionStatus: "ACTIVE",
|
||||||
|
members: {
|
||||||
|
create: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
role: MembershipRole.OWNER,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const member of members) {
|
||||||
|
if (member.id !== ctx.user.id)
|
||||||
|
createMember({
|
||||||
|
teamId: createTeam.id,
|
||||||
|
teamName: name,
|
||||||
|
inviter: ctx.user.name || "Owner",
|
||||||
|
pendingMember: member,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
return createTeam;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user