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 dynamic from "next/dynamic";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { NewTeamData } from "@calcom/features/ee/teams/lib/types";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Switch, Button } from "@calcom/ui/v2";
|
||||
|
||||
const PurchaseNewTeam = ({
|
||||
newTeamData,
|
||||
paymentIntent,
|
||||
clientSecret,
|
||||
teamPrices,
|
||||
total,
|
||||
billingFrequency,
|
||||
newTeamData,
|
||||
}: {
|
||||
newTeamData: NewTeamData;
|
||||
paymentIntent: string;
|
||||
clientSecret: string;
|
||||
teamPrices: {
|
||||
monthly: number;
|
||||
yearly: number;
|
||||
};
|
||||
total: number;
|
||||
billingFrequency: string;
|
||||
newTeamData: NewTeamData;
|
||||
}) => {
|
||||
const [message, setMessage] = useState("");
|
||||
const [billingFrequency, setBillingFrequency] = useState("monthly");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [paymentProcessing, setPaymentProcessing] = useState(false);
|
||||
const stripe = useStripe();
|
||||
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
|
||||
useEffect(() => {
|
||||
if (!stripe) {
|
||||
return;
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
if (!stripe || !elements) return;
|
||||
|
||||
if (!paymentIntent) {
|
||||
return;
|
||||
}
|
||||
setPaymentProcessing(true);
|
||||
|
||||
stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => {
|
||||
switch (paymentIntent.status) {
|
||||
case "succeeded":
|
||||
setMessage("Payment succeeded!");
|
||||
break;
|
||||
case "processing":
|
||||
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;
|
||||
}
|
||||
const { error } = await stripe.confirmPayment({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: `${CAL_URL}/settings/profile`,
|
||||
},
|
||||
redirect: "if_required",
|
||||
});
|
||||
}, [stripe]);
|
||||
|
||||
if (error) {
|
||||
console.log("🚀 ~ file: PurchaseNewTeam.tsx ~ line 71 ~ handleSubmit ~ error", error);
|
||||
} else {
|
||||
createTeamMutation.mutate(newTeamData);
|
||||
}
|
||||
};
|
||||
|
||||
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 />
|
||||
|
||||
<Button
|
||||
className="mt-4 w-full justify-center"
|
||||
loading={paymentProcessing}
|
||||
onClick={() => handleSubmit()}>
|
||||
Pay ${teamPrices[billingFrequency as keyof typeof teamPrices]}
|
||||
Pay ${total} / {billingFrequency}
|
||||
</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 Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
@ -45,6 +45,7 @@ const CreateNewTeamPage = () => {
|
|||
slug: "",
|
||||
logo: "",
|
||||
members: [],
|
||||
billingFrequency: "monthly",
|
||||
});
|
||||
const [clientSecret, setClientSecret] = useState("");
|
||||
const [paymentIntent, setPaymentIntent] = useState("");
|
||||
|
@ -82,25 +83,19 @@ const CreateNewTeamPage = () => {
|
|||
|
||||
const currentStepIndex = steps.indexOf(currentStep);
|
||||
|
||||
const createPaymentIntentMutation = trpc.useMutation(["viewer.teams.mutatePaymentIntent"], {
|
||||
const createPaymentIntentMutation = trpc.useMutation(["viewer.teams.createPaymentIntent"], {
|
||||
onSuccess: (data) => {
|
||||
console.log("🚀 ~ file: [[...step]].tsx ~ line 86 ~ CreateNewTeamPage ~ data", data);
|
||||
setClientSecret(data.client_secret);
|
||||
setPaymentIntent(data.id);
|
||||
setClientSecret(data.clientSecret);
|
||||
goToIndex(2);
|
||||
},
|
||||
});
|
||||
|
||||
const getTeamPricesQuery = trpc.useQuery(["viewer.teams.getTeamPrices"], {
|
||||
onSuccess: (data) => {
|
||||
setTeamPrices(data);
|
||||
console.log("🚀 ~ file: [[...step]].tsx ~ line 95 ~ CreateNewTeamPage ~ data", data);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
createPaymentIntentMutation.mutate({ amount: 1500 });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="dark:bg-brand dark:text-brand-contrast min-h-screen text-black"
|
||||
|
@ -133,7 +128,6 @@ const CreateNewTeamPage = () => {
|
|||
<CreateNewTeam
|
||||
nextStep={(values: NewTeamFormValues) => {
|
||||
setNewTeamData({ ...values, members: [] });
|
||||
localStorage.setItem("newTeamValues", JSON.stringify(values));
|
||||
goToIndex(1);
|
||||
}}
|
||||
/>
|
||||
|
@ -146,17 +140,21 @@ const CreateNewTeamPage = () => {
|
|||
{currentStep === "add-team-members" && (
|
||||
<AddNewTeamMembers
|
||||
teamPrices={teamPrices}
|
||||
nextStep={(values: PendingMember[]) => {
|
||||
console.log("🚀 ~ file: [[...step]].tsx ~ line 144 ~ CreateNewTeamPage ~ values", values);
|
||||
setNewTeamData({ ...newTeamData, members: [...values] });
|
||||
// localStorage.removeItem("newTeamValues");
|
||||
// purchaseTeamMutation.mutate({
|
||||
// ...newTeamData,
|
||||
// members: [...values],
|
||||
// language: i18n.language,
|
||||
// });
|
||||
createPaymentIntentMutation.mutate({ amount: values.length * 15 * 100 });
|
||||
goToIndex(2);
|
||||
nextStep={(values: {
|
||||
members: PendingMember[];
|
||||
billingFrequency: "monthly" | "yearly";
|
||||
}) => {
|
||||
console.log("🚀 ~ file: [[...step]].tsx ~ line 148 ~ CreateNewTeamPage ~ values", values);
|
||||
createPaymentIntentMutation.mutate({
|
||||
teamName: newTeamData.name,
|
||||
billingFrequency: values.billingFrequency,
|
||||
seats: values.members.length,
|
||||
});
|
||||
setNewTeamData({
|
||||
...newTeamData,
|
||||
members: [...values.members],
|
||||
billingFrequency: values.billingFrequency,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -164,9 +162,13 @@ const CreateNewTeamPage = () => {
|
|||
{currentStep === "purchase-new-team" && (
|
||||
<Elements stripe={stripe} options={{ clientSecret }}>
|
||||
<PurchaseNewTeam
|
||||
newTeamData={newTeamData}
|
||||
paymentIntent={paymentIntent}
|
||||
clientSecret={clientSecret}
|
||||
total={
|
||||
newTeamData.members.length *
|
||||
teamPrices[newTeamData.billingFrequency as keyof typeof teamPrices]
|
||||
}
|
||||
newTeamData={newTeamData}
|
||||
/>
|
||||
</Elements>
|
||||
)}
|
||||
|
|
|
@ -21,7 +21,7 @@ const AddNewTeamMembers = ({
|
|||
nextStep,
|
||||
teamPrices,
|
||||
}: {
|
||||
nextStep: (values: PendingMember[]) => void;
|
||||
nextStep: (values: { members: PendingMember[]; billingFrequency: "monthly" | "yearly" }) => void;
|
||||
teamPrices: {
|
||||
monthly: number;
|
||||
yearly: number;
|
||||
|
@ -68,7 +68,7 @@ const AddNewTeamMembers = ({
|
|||
name: session?.data.user.name || "",
|
||||
email: session?.data.user.email || "",
|
||||
username: session?.data.user.username || "",
|
||||
userId: session?.data.user.id || "",
|
||||
id: session?.data.user.id || "",
|
||||
role: "OWNER",
|
||||
});
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ const AddNewTeamMembers = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Form form={formMethods} handleSubmit={(values) => nextStep(values.members)}>
|
||||
<Form form={formMethods} handleSubmit={(values) => nextStep(values)}>
|
||||
<Controller
|
||||
name="members"
|
||||
render={({ field: { value } }) => (
|
||||
|
@ -179,15 +179,24 @@ const AddNewTeamMembers = ({
|
|||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="mt-4 flex space-x-2">
|
||||
<Switch
|
||||
onClick={() => setBillingFrequency(billingFrequency === "monthly" ? "yearly" : "monthly")}
|
||||
/>
|
||||
<p>
|
||||
Switch to yearly and save {numberOfMembers * (teamPrices.monthly * 12 - teamPrices.yearly)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
control={formMethods.control}
|
||||
name="billingFrequency"
|
||||
defaultValue={"monthly"}
|
||||
render={(field: { value }) => (
|
||||
<div className="mt-4 flex space-x-2">
|
||||
<Switch
|
||||
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">
|
||||
{t("checkout")}
|
||||
</Button>
|
||||
|
|
|
@ -15,18 +15,19 @@ export const createMember = async ({
|
|||
pendingMember,
|
||||
language,
|
||||
teamSubscriptionActive,
|
||||
}): {
|
||||
}: {
|
||||
teamId: number;
|
||||
teamName: string;
|
||||
inviter: string;
|
||||
pendingMember: PendingMember;
|
||||
language: string;
|
||||
teamSubscriptionActive?: boolean;
|
||||
} => {
|
||||
if (pendingMember.username) {
|
||||
}) => {
|
||||
if (pendingMember.username & pendingMember.id) {
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
teamId,
|
||||
userId: pendingMember.userId,
|
||||
userId: pendingMember.id,
|
||||
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 }) => {
|
||||
const { teamId, seats, email } = input;
|
||||
return await stripe.checkout.sessions.create({
|
||||
|
|
|
@ -18,4 +18,5 @@ export interface PendingMember {
|
|||
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 {
|
||||
deleteTeamFromStripe,
|
||||
purchaseTeamSubscription,
|
||||
createTeamCustomer,
|
||||
createTeamSubscription,
|
||||
createPaymentIntent,
|
||||
getTeamPricing,
|
||||
updatePaymentIntent,
|
||||
|
@ -688,23 +690,69 @@ export const viewerTeamsRouter = createProtectedRouter()
|
|||
return teamPrices;
|
||||
},
|
||||
})
|
||||
.mutation("mutatePaymentIntent", {
|
||||
.mutation("createPaymentIntent", {
|
||||
input: z.object({
|
||||
amount: z.number(),
|
||||
paymentIntentId: z.string().optional(),
|
||||
teamName: z.string(),
|
||||
billingFrequency: z.string(),
|
||||
seats: z.number(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const { amount, paymentIntentId } = input;
|
||||
try {
|
||||
// If there is no payment intent, then create one
|
||||
if (!paymentIntentId) {
|
||||
const paymentIntent = await createPaymentIntent({ amount, receiptEmail: ctx.user.email });
|
||||
return paymentIntent;
|
||||
}
|
||||
console.log("🚀 ~ file: teams.tsx ~ line 699 ~ resolve ~ input", input);
|
||||
// First create the customer
|
||||
const customer = await createTeamCustomer(input.teamName, ctx.user.email);
|
||||
|
||||
await updatePaymentIntent({ amount, paymentIntentId });
|
||||
} catch (e) {
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: e.message });
|
||||
// Create the subscription for the team
|
||||
const subscription = await createTeamSubscription(customer.id, input.billingFrequency, input.seats);
|
||||
|
||||
// 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