Skip to main content
Glama

Convex MCP server

Official
by get-convex
UpgradePlanContent.tsx10.8 kB
import { Button } from "@ui/Button"; import { Spinner } from "@ui/Spinner"; import { TextInput } from "@ui/TextInput"; import React, { useCallback, useEffect, useState } from "react"; import { useDebounce } from "react-use"; import { Elements } from "@stripe/react-stripe-js"; import { useGetCoupon, useCreateSubscription } from "api/billing"; import { FormikProvider, useFormik, useFormikContext } from "formik"; import * as Yup from "yup"; import { Address, PlanResponse, Team } from "generatedApi"; import Link from "next/link"; import { PriceSummary } from "components/billing/PriceSummary"; import { PaymentDetailsForm } from "./PaymentDetailsForm"; import { BillingAddressInputs } from "./BillingAddressInputs"; import { useStripePaymentSetup } from "../../hooks/useStripe"; import { BillingContactInputs } from "./BillingContactInputs"; import { SpendingLimits, spendingLimitsSchema, spendingLimitValueToCents, } from "./SpendingLimits"; import { UpgradeFormState } from "./upgradeFormState"; export const debounceDurationMs = 200; export type UpgradePlanContentProps = { plan: PlanResponse; couponDurationInMonths?: number; numMembers: number; teamMemberDiscountPct?: number; requiresPaymentMethod?: boolean; isLoadingPromo?: boolean; promoCodeError?: string; setPaymentMethod: (paymentMethod?: string) => void; billingAddressInputs: React.ReactNode; paymentDetailsForm: React.ReactNode; isChef: boolean; teamManagedBy?: string; }; export const CreateSubscriptionSchema = Yup.object().shape({ name: Yup.string() .min(1, "Billing contact name must be at least 1 character long.") .max(128, "Billing contact name must be at most 128 characters long.") .required("Billing contact name is required."), email: Yup.string() .email("Invalid email address.") .min(1, "Billing contact name must be at least 1 character long.") .required("Billing contact email is required."), }); export function UpgradePlanContentContainer({ team, email: profileEmail, name: profileName, onUpgradeComplete, plan, isChef, ...props }: Pick<UpgradePlanContentProps, "numMembers" | "plan"> & { team: Team; email?: string; name?: string | null; onUpgradeComplete: () => void; isChef: boolean; }) { const createSubscription = useCreateSubscription(team.id); const formState = useFormik<UpgradeFormState>({ initialValues: { promoCode: "", name: profileName || "", email: profileEmail || "", planId: plan.id, paymentMethod: undefined, billingAddress: undefined, spendingLimitWarningThresholdUsd: "", spendingLimitDisableThresholdUsd: null, }, validationSchema: CreateSubscriptionSchema.concat( spendingLimitsSchema({ // A new billing cycle starts when the user upgrades, so we don’t need to show the // warning about setting a spending limit lower than the amount spent in the current // billing cycle. currentSpending: undefined, }), ), onSubmit: async (v) => { await createSubscription({ planId: v.planId, paymentMethod: v.paymentMethod, billingAddress: v.billingAddress, name: v.name, email: v.email, ...spendingLimitValueToCents(v), }); onUpgradeComplete(); }, }); const [debouncedPromoCode, setDebouncedPromoCode] = useState( formState.values.promoCode, ); useDebounce( () => { setDebouncedPromoCode(formState.values.promoCode); }, debounceDurationMs, [formState.values.promoCode], ); const couponData = useGetCoupon(team.id, plan.id, debouncedPromoCode || ""); useEffect(() => { void formState.setFieldValue( "planId", couponData.coupon?.planId || plan.id, ); // Don't need setFieldValue in deps // eslint-disable-next-line react-hooks/exhaustive-deps }, [couponData.coupon, plan.id]); const setPaymentMethod = useCallback( async (method?: string) => { await formState.setFieldValue("paymentMethod", method); }, // eslint-disable-next-line react-hooks/exhaustive-deps [], ); const setBillingAddress = useCallback( async (address?: Address) => { await formState.setFieldValue("billingAddress", address); }, // eslint-disable-next-line react-hooks/exhaustive-deps [], ); const { stripePromise, options, retrieveSetupIntent, confirmSetup, resetClientSecret, } = useStripePaymentSetup( team, formState.values.paymentMethod ?? undefined, setPaymentMethod, ); return ( <FormikProvider value={formState}> <UpgradePlanContent {...props} plan={plan} isChef={isChef} teamManagedBy={team.managedBy || undefined} setPaymentMethod={(p) => { if (!p) { resetClientSecret(); } void formState.setFieldValue("paymentMethod", p); }} teamMemberDiscountPct={couponData.coupon?.percentOff} requiresPaymentMethod={ couponData.coupon ? couponData.coupon.requiresPaymentMethod : true } couponDurationInMonths={ couponData.coupon?.durationInMonths ?? undefined } isLoadingPromo={couponData.isLoading} promoCodeError={couponData.errorMessage} billingAddressInputs={ options.clientSecret ? ( <div className="flex flex-col gap-2"> <h4>Billing Address</h4> <Elements stripe={stripePromise} options={options}> <BillingAddressInputs onChangeAddress={setBillingAddress} /> </Elements> </div> ) : undefined } paymentDetailsForm={ // Using dependency injection to pass in the Stripe form // so we can test the UpgradePlanContent component in isolation options.clientSecret ? ( <Elements stripe={stripePromise} options={options}> <PaymentDetailsForm retrieveSetupIntent={retrieveSetupIntent} confirmSetup={confirmSetup} /> </Elements> ) : undefined } /> </FormikProvider> ); } export function UpgradePlanContent({ plan, couponDurationInMonths, teamMemberDiscountPct = 0, numMembers, requiresPaymentMethod = true, isLoadingPromo = false, promoCodeError, setPaymentMethod, billingAddressInputs, paymentDetailsForm, isChef, teamManagedBy, }: UpgradePlanContentProps) { const formState = useFormikContext<UpgradeFormState>(); if (teamMemberDiscountPct < 0 || teamMemberDiscountPct > 1) { throw new Error( `Invalid teamMemberDiscountPct: ${teamMemberDiscountPct}. Must be between 0 and 1.`, ); } return ( <> {isChef && plan.planType === "CONVEX_STARTER_PLUS" && ( <p className="mb-2"> {plan.name} is recommended for Convex Chef users.{" "} <Link href="/team/settings/billing" className="text-content-link hover:underline" > View all plans. </Link> </p> )} <div className="flex flex-col gap-6"> <PriceSummary plan={plan} teamMemberDiscountPct={teamMemberDiscountPct} numMembers={numMembers} requiresPaymentMethod={requiresPaymentMethod} couponDurationInMonths={couponDurationInMonths} isUpgrading={false} teamManagedBy={teamManagedBy} /> {plan.planType === "CONVEX_PROFESSIONAL" && ( <div className="flex max-w-64 items-center gap-2"> <TextInput label="Promo code" placeholder="Enter a promo code" onChange={(e) => formState.setFieldValue( "promoCode", e.target.value.toUpperCase(), ) } value={formState.values.promoCode} id="promoCode" error={promoCodeError} /> {isLoadingPromo && ( <span data-testid="loading-spinner" className="mt-4"> <Spinner /> </span> )} </div> )} <div className="flex flex-col gap-2"> <h4>Billing Contact</h4> <BillingContactInputs formState={formState} /> </div> {billingAddressInputs} <div className="flex flex-col gap-2"> <h4>Usage Spending Limits</h4> <SpendingLimits /> </div> {requiresPaymentMethod && !formState.values.paymentMethod && ( <div className="flex flex-col gap-2"> <h4>Payment Details</h4> {/* Reduce the amount of space stripe can take up so it doesn't render horizontal overflow */} <div className="max-w-[calc(100%-1rem)]">{paymentDetailsForm}</div> </div> )} {(!requiresPaymentMethod || formState.values.paymentMethod) && ( <form className="mt-2 flex flex-col items-start gap-4 text-sm" onSubmit={async (e) => { e.preventDefault(); await formState.handleSubmit(); }} > <div className="flex gap-4"> {formState.values.paymentMethod && ( <Button size="sm" onClick={() => setPaymentMethod(undefined)} data-testid="update-payment-method-button" variant="neutral" > Change payment method </Button> )} <Button data-testid="upgrade-plan-button" className="w-fit" size="sm" disabled={ isLoadingPromo || (requiresPaymentMethod && !formState.values.paymentMethod) || !formState.values.billingAddress } type="submit" loading={formState.isSubmitting} tip={ requiresPaymentMethod && !formState.values.paymentMethod ? "Add a payment method to continue." : !formState.values.name ? "Enter a billing contact name to continue." : !formState.values.email ? "Enter a billing contact email to continue." : !formState.values.billingAddress ? "Enter a billing address to continue." : undefined } > Confirm and Upgrade </Button> </div> </form> )} </div> </> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server