Remove Stripe and paid booking flows

This commit is contained in:
2026-06-07 15:17:29 -06:00
parent 5026e0ea22
commit 8e4094c67b
19 changed files with 38 additions and 160 deletions
-25
View File
@@ -189,27 +189,8 @@ TWILIO_OPT_OUT_ENABLED=
# Set it to "1" if you need to run E2E tests locally.
NEXT_PUBLIC_IS_E2E=
# Used for internal billing system
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE=
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
STRIPE_TEAM_MONTHLY_PRICE_ID=
STRIPE_TEAM_ANNUAL_PRICE_ID=
NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID=
ORG_MONTHLY_CREDITS=
STRIPE_TEAM_PRODUCT_ID=
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
STRIPE_ORG_MONTHLY_PRICE_ID=
STRIPE_ORG_ANNUAL_PRICE_ID=
STRIPE_ORG_PRODUCT_ID=
# Used to pass trial days for orgs during the Stripe checkout session
STRIPE_ORG_TRIAL_DAYS=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET_APPS=
STRIPE_PRIVATE_KEY=
STRIPE_CLIENT_ID=
# Use for internal Public API Keys and optional
API_KEY_PREFIX=cal_
@@ -379,15 +360,9 @@ RETELL_AI_TEST_EVENT_TYPE_MAP=
RETELL_AI_TEST_CAL_API_KEY=
# Used for buying phone number for cal ai voice agent
STRIPE_PHONE_NUMBER_MONTHLY_PRICE_ID=
CAL_AI_CALL_RATE_PER_MINUTE=0.29
STRIPE_WEBHOOK_SECRET_BILLING=
# Price for buying a phone number for cal.ai voice agent (Default is 5)
NEXT_PUBLIC_CAL_AI_PHONE_NUMBER_MONTHLY_PRICE=
+1 -19
View File
@@ -1,6 +1,5 @@
"use client";
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import { useHandleBookEvent } from "@calcom/atoms/hooks/bookings/useHandleBookEvent";
import dayjs from "@calcom/dayjs";
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
@@ -13,7 +12,6 @@ import { createBooking } from "@calcom/features/bookings/lib/create-booking";
import { createRecurringBooking } from "@calcom/features/bookings/lib/create-recurring-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import type { BookerEvent, BookingResponse } from "@calcom/features/bookings/types";
import { getFullName } from "@calcom/features/form-builder/utils";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { BookingStatus } from "@calcom/prisma/enums";
@@ -122,7 +120,6 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookin
(state) => [state.bookingData, state.setBookingData],
shallow
);
const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);
const { t } = useLocale();
const bookingSuccessRedirect = useBookingSuccessRedirect();
const bookerFormErrorRef = useRef<HTMLDivElement>(null);
@@ -180,9 +177,7 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookin
return;
}
const { uid, paymentUid } = booking;
const fullName = getFullName(bookingForm.getValues("responses.name"));
const { uid } = booking;
const users = event.data?.subsetOfHosts?.length
? event.data?.subsetOfHosts.map((host) => host.user)
: event.data?.subsetOfUsers;
@@ -236,19 +231,6 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookin
);
}
if (paymentUid) {
router.push(
createPaymentLink({
paymentUid,
date: timeslot,
name: fullName,
email: bookingForm.getValues("responses.email"),
absolute: false,
})
);
return;
}
if (!uid) {
console.error("No uid returned from createBookingMutation");
return;
@@ -422,8 +422,6 @@ export default function Success(props: PageProps) {
paymentOption: props.paymentStatus.paymentOption,
}
: { success: false, refunded: false },
refundPolicy: eventType?.metadata?.apps?.stripe?.refundPolicy,
refundDaysCount: eventType?.metadata?.apps?.stripe?.refundDaysCount,
});
const successPageHeadline = (() => {
@@ -461,9 +461,7 @@ export const EventAdvancedTab = ({
);
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const multiLocation = (formMethods.getValues("locations") || []).length > 1;
const noShowFeeEnabled =
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
formMethods.getValues("metadata")?.apps?.stripe?.paymentOption === "HOLD";
const noShowFeeEnabled = false;
const isRecurringEvent = !!formMethods.getValues("recurringEvent");
const interfaceLanguageOptions =
+21 -64
View File
@@ -1,10 +1,7 @@
"use client";
import getStripe from "@calcom/app-store/stripepayment/lib/client";
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
import {
fetchSignup,
hasCheckoutSession,
isAccountUnderReview,
isUserAlreadyExistsError,
} from "@calcom/features/auth/signup/lib/fetchSignup";
@@ -29,17 +26,15 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { INVALID_CLOUDFLARE_TOKEN_ERROR } from "@calcom/lib/server/checkCfTurnstileToken";
import { IS_EUROPE } from "@calcom/lib/timezoneConstants";
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import classNames from "@calcom/ui/classNames";
import { Alert } from "@calcom/ui/components/alert";
import { Button } from "@calcom/ui/components/button";
import { CheckboxField, Form, PasswordField, SelectField, TextField } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { InfoIcon, ShieldCheckIcon, StarIcon } from "@coss/ui/icons";
import { InfoIcon, ShieldCheckIcon } from "@coss/ui/icons";
import { Analytics as DubAnalytics } from "@dub/analytics/react";
import { zodResolver } from "@hookform/resolvers/zod";
import type { getServerSideProps } from "@lib/signup/getServerSideProps";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -63,7 +58,17 @@ const TurnstileCaptcha = dynamic(() => import("@calcom/web/modules/auth/componen
type FormValues = z.infer<typeof signupSchema>;
export type SignupProps = inferSSRProps<typeof getServerSideProps>;
export type SignupProps = {
prepopulateFormValues?: FormValues;
token?: string;
orgSlug?: string;
isGoogleLoginEnabled?: boolean;
isOutlookLoginEnabled?: boolean;
orgAutoAcceptEmail?: string | null;
redirectUrl?: string | null;
emailVerificationEnabled?: boolean;
onboardingV3Enabled?: boolean;
};
const FEATURES = [
{
@@ -102,8 +107,6 @@ function truncateDomain(domain: string) {
function UsernameField({
username,
setPremium,
premium,
setUsernameTaken,
orgSlug,
usernameTaken,
@@ -111,8 +114,6 @@ function UsernameField({
...props
}: React.ComponentProps<typeof TextField> & {
username: string;
setPremium: (value: boolean) => void;
premium: boolean;
usernameTaken: boolean;
orgSlug?: string;
setUsernameTaken: (value: boolean) => void;
@@ -128,12 +129,10 @@ function UsernameField({
// If the username can't be changed, there is no point in doing the username availability check
if (disabled) return;
if (!debouncedUsername) {
setPremium(false);
setUsernameTaken(false);
return;
}
fetchUsername(debouncedUsername, orgSlug ?? null).then(({ data }) => {
setPremium(data.premium);
setUsernameTaken(!data.available);
});
}
@@ -145,7 +144,6 @@ function UsernameField({
orgSlug,
formState.isSubmitting,
formState.isSubmitSuccessful,
setPremium,
setUsernameTaken,
]);
@@ -165,16 +163,6 @@ function UsernameField({
<InfoIcon className="mr-1 inline-block h-4 w-4" />
<p>{t("already_in_use_error")}</p>
</div>
) : premium ? (
<div data-testid="premium-username-warning" className="flex items-center">
<StarIcon className="mr-1 inline-block h-4 w-4" />
<p>
{t("premium_username", {
price: getPremiumPlanPriceValue(),
interpolation: { escapeValue: false },
})}
</p>
</div>
) : null}
</div>
</div>
@@ -201,19 +189,18 @@ export default function Signup({
onboardingV3Enabled,
}: SignupProps) {
const isOrgInviteByLink = orgSlug && !prepopulateFormValues?.username;
const [premiumUsername, setPremiumUsername] = useState(false);
const [usernameTaken, setUsernameTaken] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [isMicrosoftLoading, setIsMicrosoftLoading] = useState(false);
const [accountUnderReview, setAccountUnderReview] = useState(false);
const [displayEmailForm, setDisplayEmailForm] = useState(token);
const [displayEmailForm, setDisplayEmailForm] = useState(Boolean(token));
const [turnstileKey, setTurnstileKey] = useState(0);
const searchParams = useCompatSearchParams();
const { t, i18n } = useLocale();
const router = useRouter();
const formMethods = useForm<FormValues>({
resolver: zodResolver(signupSchema),
defaultValues: prepopulateFormValues satisfies FormValues,
defaultValues: prepopulateFormValues,
mode: "onTouched",
});
const {
@@ -244,7 +231,6 @@ export default function Signup({
has_token: !!token,
is_org_invite: isOrgInviteByLink,
org_slug: orgSlug,
is_premium_username: premiumUsername,
username_taken: usernameTaken,
});
@@ -268,17 +254,6 @@ export default function Signup({
return;
}
if (hasCheckoutSession(result)) {
const stripe = await getStripe();
if (stripe) {
const { error } = await stripe.redirectToCheckout({
sessionId: result.error.checkoutSessionId,
});
if (error) console.warn(error.message);
}
return;
}
throw new Error(result.error.message);
}
@@ -327,7 +302,6 @@ export default function Signup({
has_token: !!token,
is_org_invite: isOrgInviteByLink,
org_slug: orgSlug,
is_premium_username: premiumUsername,
error_message: errorMessage,
});
formMethods.setError("apiError", { message: errorMessage });
@@ -492,7 +466,7 @@ export default function Signup({
if (!formMethods.getValues().username && isOrgInviteByLink) {
updatedValues = {
...values,
username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail),
username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail ?? null),
};
}
await signUp(updatedValues);
@@ -503,23 +477,14 @@ export default function Signup({
orgSlug={orgSlug}
label={t("username")}
username={watch("username") || ""}
premium={premiumUsername}
usernameTaken={usernameTaken}
disabled={!!orgSlug}
setUsernameTaken={(value) => setUsernameTaken(value)}
data-testid="signup-usernamefield"
setPremium={(value) => setPremiumUsername(value)}
addOnLeading={
orgSlug
? truncateDomain(
`${WEBAPP_URL.replace(
URL_PROTOCOL_REGEX,
""
)}/`
)
: truncateDomain(
`${WEBSITE_URL.replace(URL_PROTOCOL_REGEX, "")}/`
)
? truncateDomain(`${WEBAPP_URL.replace(URL_PROTOCOL_REGEX, "")}/`)
: truncateDomain(`${WEBSITE_URL.replace(URL_PROTOCOL_REGEX, "")}/`)
}
/>
) : null}
@@ -531,7 +496,7 @@ export default function Signup({
placeholder="john@doe.com"
type="email"
autoComplete="email"
disabled={prepopulateFormValues?.email}
disabled={Boolean(prepopulateFormValues?.email)}
data-testid="signup-emailfield"
/>
@@ -588,9 +553,7 @@ export default function Signup({
isSubmitting ||
usernameTaken
}>
{premiumUsername && !usernameTaken
? `${t("get_started")} (${getPremiumPlanPriceValue()})`
: t("get_started")}
{t("get_started")}
</Button>
</Form>
</div>
@@ -608,10 +571,7 @@ export default function Signup({
<>
{/* eslint-disable @next/next/no-img-element */}
<img
className={classNames(
"mr-2 h-4 w-4 text-subtle",
premiumUsername && "opacity-50"
)}
className="mr-2 h-4 w-4 text-subtle"
src="/google-icon-colored.svg"
alt="Continue with Google Icon"
/>
@@ -659,10 +619,7 @@ export default function Signup({
<>
{/* eslint-disable @next/next/no-img-element */}
<img
className={classNames(
"text-subtle mr-2 h-4 w-4",
premiumUsername && "opacity-50"
)}
className="text-subtle mr-2 h-4 w-4"
src="/microsoft-logo.svg"
alt="Continue with Microsoft Icon"
/>
+2
View File
@@ -12,6 +12,7 @@ import { getAppName } from "./utils/getAppName";
const isInWatchMode = process.argv[2] === "--watch";
const repoRoot = path.resolve(__dirname, "../../..");
const DISABLED_APPS = new Set(["stripepayment"]);
const formatFileWithBiome = (filePath: string) => {
// Normalize to forward slashes for cross-platform Biome compatibility
@@ -79,6 +80,7 @@ function generateFiles() {
function forEachAppDir(callback: (arg: App) => void, filter: (arg: App) => boolean = () => true) {
for (let i = 0; i < appDirs.length; i++) {
if (DISABLED_APPS.has(appDirs[i].name)) continue;
const configPath = path.join(APP_STORE_PATH, appDirs[i].path, "config.json");
const metadataPath = path.join(APP_STORE_PATH, appDirs[i].path, "_metadata.ts");
let app: Partial<AppMetaType>;
@@ -40,7 +40,6 @@ export const EventTypeAddonMap = {
posthog: dynamic(() => import("./posthog/components/EventTypeAppCardInterface")),
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")),
salesforce: dynamic(() => import("./salesforce/components/EventTypeAppCardInterface")),
stripepayment: dynamic(() => import("./stripepayment/components/EventTypeAppCardInterface")),
"booking-pages-tag": dynamic(
() => import("./templates/booking-pages-tag/components/EventTypeAppCardInterface")
),
@@ -66,5 +65,4 @@ export const EventTypeSettingsMap = {
paypal: dynamic(() => import("./paypal/components/EventTypeAppSettingsInterface")),
plausible: dynamic(() => import("./plausible/components/EventTypeAppSettingsInterface")),
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppSettingsInterface")),
stripepayment: dynamic(() => import("./stripepayment/components/EventTypeAppSettingsInterface")),
};
@@ -38,7 +38,6 @@ import { appKeysSchema as posthog_zod_ts } from "./posthog/zod";
import { appKeysSchema as qr_code_zod_ts } from "./qr_code/zod";
import { appKeysSchema as salesforce_zod_ts } from "./salesforce/zod";
import { appKeysSchema as shimmervideo_zod_ts } from "./shimmervideo/zod";
import { appKeysSchema as stripepayment_zod_ts } from "./stripepayment/zod";
import { appKeysSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
import { appKeysSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod";
import { appKeysSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod";
@@ -89,7 +88,6 @@ export const appKeysSchemas = {
qr_code: qr_code_zod_ts,
salesforce: salesforce_zod_ts,
shimmervideo: shimmervideo_zod_ts,
stripe: stripepayment_zod_ts,
tandemvideo: tandemvideo_zod_ts,
"booking-pages-tag": booking_pages_tag_zod_ts,
"event-type-app-card": event_type_app_card_zod_ts,
@@ -86,7 +86,6 @@ import shimmervideo_config_json from "./shimmervideo/config.json";
import signal_config_json from "./signal/config.json";
import sirius_video_config_json from "./sirius_video/config.json";
import skype_config_json from "./skype/config.json";
import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata";
import sylapsvideo_config_json from "./sylapsvideo/config.json";
import synthflow_config_json from "./synthflow/config.json";
import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata";
@@ -198,7 +197,6 @@ export const appStoreMetadata = {
signal: signal_config_json,
sirius_video: sirius_video_config_json,
skype: skype_config_json,
stripepayment: stripepayment__metadata_ts,
sylapsvideo: sylapsvideo_config_json,
synthflow: synthflow_config_json,
tandemvideo: tandemvideo__metadata_ts,
@@ -38,7 +38,6 @@ import { appDataSchema as posthog_zod_ts } from "./posthog/zod";
import { appDataSchema as qr_code_zod_ts } from "./qr_code/zod";
import { appDataSchema as salesforce_zod_ts } from "./salesforce/zod";
import { appDataSchema as shimmervideo_zod_ts } from "./shimmervideo/zod";
import { appDataSchema as stripepayment_zod_ts } from "./stripepayment/zod";
import { appDataSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
import { appDataSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod";
import { appDataSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod";
@@ -89,7 +88,6 @@ export const appDataSchemas = {
qr_code: qr_code_zod_ts,
salesforce: salesforce_zod_ts,
shimmervideo: shimmervideo_zod_ts,
stripe: stripepayment_zod_ts,
tandemvideo: tandemvideo_zod_ts,
"booking-pages-tag": booking_pages_tag_zod_ts,
"event-type-app-card": event_type_app_card_zod_ts,
@@ -65,7 +65,6 @@ export const apiHandlers = {
signal: import("./signal/api"),
sirius_video: import("./sirius_video/api"),
skype: import("./skype/api"),
stripepayment: import("./stripepayment/api"),
sylapsvideo: import("./sylapsvideo/api"),
tandemvideo: import("./tandemvideo/api"),
telegram: import("./telegram/api"),
@@ -8,5 +8,4 @@ export const PaymentServiceMap = {
hitpay: import("./hitpay/lib/PaymentService"),
"mock-payment-app": import("./mock-payment-app/lib/PaymentService"),
paypal: import("./paypal/lib/PaymentService"),
stripepayment: import("./stripepayment/lib/PaymentService"),
};
@@ -1,11 +1,7 @@
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
import prisma from "@calcom/prisma";
import type { User } from "@calcom/prisma/client";
export async function deleteUser(user: Pick<User, "id" | "email" | "metadata">) {
// If 2FA is disabled or totpCode is valid then delete the user from stripe and database
await deleteStripeCustomer(user).catch(console.warn);
// Remove my account
// TODO: Move this to Repository pattern.
await prisma.user.delete({
where: {
+10
View File
@@ -0,0 +1,10 @@
import { describe, expect, it } from "vitest";
import { IS_STRIPE_ENABLED, IS_TEAM_BILLING_ENABLED, IS_TEAM_BILLING_ENABLED_CLIENT } from "./constants";
describe("self-hosted billing capabilities", () => {
it("keeps Stripe and team billing disabled", () => {
expect(IS_STRIPE_ENABLED).toBe(false);
expect(IS_TEAM_BILLING_ENABLED).toBe(false);
expect(IS_TEAM_BILLING_ENABLED_CLIENT).toBe(false);
});
});
+3 -8
View File
@@ -121,16 +121,11 @@ export const SEO_IMG_DEFAULT = `${CAL_URL}/og-image.png`;
// as well, otherwise the URL won't be valid.
export const SEO_IMG_OGIMG = `${CAL_URL}/_next/image?w=1200&q=100&url=`;
export const SEO_IMG_OGIMG_VIDEO = `${CAL_URL}/video-og-image.png`;
export const IS_STRIPE_ENABLED = !!(
process.env.STRIPE_CLIENT_ID &&
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
process.env.STRIPE_PRIVATE_KEY
);
export const IS_STRIPE_ENABLED: boolean = false;
/** This has correct value only server side. When you want to use client side, go for IS_TEAM_BILLING_ENABLED_CLIENT. I think we should use the _CLIENT one only everywhere so that it works reliably everywhere on client as well as server */
export const IS_TEAM_BILLING_ENABLED = !!(IS_STRIPE_ENABLED && HOSTED_CAL_FEATURES);
export const IS_TEAM_BILLING_ENABLED: boolean = false;
export const IS_TEAM_BILLING_ENABLED_CLIENT =
!!process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY && HOSTED_CAL_FEATURES;
export const IS_TEAM_BILLING_ENABLED_CLIENT: boolean = false;
export const FULL_NAME_LENGTH_MAX_LIMIT = 50;
export const API_NAME_LENGTH_MAX_LIMIT = 80;
@@ -359,10 +359,6 @@ export const useEventTypeForm = ({
// Ok to cast type here because this metadata will be updated as the event type metadata
if (checkForMultiplePaymentApps(metadata)) throw new Error(t("event_setup_multiple_payment_apps_error"));
if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
throw new Error(t("seats_and_no_show_fee_error"));
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, users, scheduleName, disabledCancelling, disabledRescheduling, ...rest } = input;
// Strip children down to only the fields the server schema expects.
@@ -1408,17 +1408,6 @@ export const TestData = {
redirect_uris: ["http://localhost:3000/auth/callback"],
},
},
"stripe-payment": {
...appStoreMetadata.stripepayment,
keys: {
expiry_date: Infinity,
api_key: "",
scale_plan: "false",
client_id: "client_id",
client_secret: "client_secret",
redirect_uris: ["http://localhost:3000/auth/callback"],
},
},
},
};
@@ -9,7 +9,6 @@ import { teamsAndUserProfilesQuery } from "./procedures/teamsAndUserProfilesQuer
import { ZRemoveNotificationsSubscriptionInputSchema } from "./removeNotificationsSubscription.schema";
type AppsRouterHandlerCache = {
stripeCustomer?: typeof import("./stripeCustomer.handler").stripeCustomerHandler;
eventTypeOrder?: typeof import("./eventTypeOrder.handler").eventTypeOrderHandler;
teamsAndUserProfilesQuery?: typeof import("./teamsAndUserProfilesQuery.handler").teamsAndUserProfilesQuery;
connectAndJoin?: typeof import("./connectAndJoin.handler").Handler;
@@ -20,11 +19,6 @@ type AppsRouterHandlerCache = {
};
export const loggedInViewerRouter = router({
stripeCustomer: authedProcedure.query(async ({ ctx }) => {
const { stripeCustomerHandler } = await import("./stripeCustomer.handler");
return stripeCustomerHandler({ ctx });
}),
unlinkConnectedAccount: authedProcedure.mutation(async (opts) => {
const unlinkConnectedAccountHandler = await import("./unlinkConnectedAccount.handler").then(
(mod) => mod.default
@@ -1,4 +1,3 @@
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { prisma } from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
@@ -28,9 +27,6 @@ export const deleteMeWithoutPasswordHandler = async ({ ctx }: DeleteMeWithoutPas
throw new Error(ErrorCode.SocialIdentityProviderRequired);
}
// Remove me from Stripe
await deleteStripeCustomer(user).catch(console.warn);
// Remove my account
await prisma.user.delete({
where: {