This commit is contained in:
Alex Holliday committed 2025-12-02 00:20:09 +00:00
1 parent 01c0e9ef4d
commit 1b162578c2
22 files changed
+450 -72

No files matched your search

@@ -7,7 +7,12 @@ import Tooltip from "@mui/material/Tooltip";
import { useSelector } from "react-redux";
import type { RootState } from "@/store";
import { useMemo } from "react";
import type { EntitlemenstKey } from "@/types/entitlements";
type EntitlementsKey =
| "monitorsMax"
| "notificationChannelsMax"
| "statusPagesMax"
| "checksIntervalMsMin"
| "teamsMax";
export const HeaderCreate = ({
label,
isLoading,
@@ -18,7 +23,7 @@ export const HeaderCreate = ({
label?: string;
isLoading: boolean;
path: string;
entitlement: EntitlemenstKey | null;
entitlement: EntitlementsKey | null;
entitlementCount: number;
}) => {
const theme = useTheme();
@@ -29,7 +34,7 @@ export const HeaderCreate = ({
if (!entitlement) return false;
if (!ent || typeof ent[entitlement] !== "number") return false;
return entitlementCount < ent[entitlement] ? false : true;
}, [ent, entitlementCount]);
}, [ent, entitlement, entitlementCount]);
return (
<Stack
direction="row"
@@ -32,7 +32,6 @@ interface BasePageWithStatesProps extends StackProps {
error: any;
items: any[];
bullets: string[] | unknown;
page: string;
actionButtonText: string;
actionLink: string;
@@ -83,6 +82,7 @@ interface MonitorBasePageWithStatesProps extends StackProps {
loading: boolean;
error: any;
items: any[];
monitorLimitReached: boolean;
page: string;
actionLink?: string;
children: React.ReactNode;
@@ -100,6 +100,7 @@ export const MonitorBasePageWithStates: React.FC<
loading,
error,
items,
monitorLimitReached,
page,
actionLink,
children,
@@ -130,6 +131,7 @@ export const MonitorBasePageWithStates: React.FC<
bullets={t(`${page}Monitor.fallback.checks`, { returnObjects: true })}
actionButtonText={t(`${page}Monitor.fallback.actionButton`)}
actionLink={actionLink || ""}
monitorLimitReached={monitorLimitReached}
/>
);
}
@@ -123,12 +123,14 @@ export const EmptyMonitorFallback = ({
bullets,
actionButtonText,
actionLink,
monitorLimitReached,
}: {
page: string;
title: string;
bullets: any;
actionButtonText: string;
actionLink: string;
monitorLimitReached: boolean;
}) => {
const theme = useTheme();
const navigate = useNavigate();
@@ -158,8 +160,9 @@ export const EmptyMonitorFallback = ({
variant="contained"
color="accent"
onClick={() => navigate(actionLink)}
disabled={monitorLimitReached}
>
{actionButtonText}
{monitorLimitReached ? "Monitor limit reached" : actionButtonText}
</Button>
</Stack>
</Stack>
+22
View File
@@ -1,6 +1,8 @@
import { BasePage, InfoBox, PlanCard } from "@/components/design-elements";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import { Button } from "@/components/inputs";
import { useAppSelector } from "@/hooks/AppHooks";
import { useGet, usePost } from "@/hooks/UseApi";
@@ -27,6 +29,14 @@ const BillingPage = () => {
}
};
const handleCancel = async () => {
const result = await post("/billing/cancel", {});
const redirectUrl = result?.data?.redirectUrl;
if (redirectUrl) {
window.location = redirectUrl;
}
};
return (
<BasePage>
<InfoBox
@@ -48,6 +58,18 @@ const BillingPage = () => {
</Grid>
))}
</Grid>
{selectedPlan !== "free" && (
<Box>
<Button
variant="contained"
color="error"
onClick={handleCancel}
loading={isPosting}
>
Cancel subscription
</Button>
</Box>
)}
</BasePage>
);
};
+33 -1
View File
@@ -1,5 +1,37 @@
import { BasePage } from "@/components/design-elements";
import Stack from "@mui/material/Stack";
import CircularProgress from "@mui/material/CircularProgress";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { Navigate, useSearchParams } from "react-router-dom";
import { useGet } from "@/hooks/UseApi";
import type { ApiResponse } from "@/hooks/UseApi";
const BillingSuccessPage = () => {
return "Great";
const theme = useTheme();
const [searchParams] = useSearchParams();
const plan = searchParams.get("plan");
const { response } = useGet<ApiResponse<any>>(
`/billing/confirm?plan=${plan}`,
{},
{}
);
const success = response?.data?.success ?? false;
if (success === true) {
return <Navigate to="/billing" replace />;
} else {
return (
<BasePage>
<Stack direction="row" alignItems={"center"} gap={theme.spacing(10)}>
<CircularProgress color="accent" />
<Typography>Awaiting confirmation..</Typography>
</Stack>
</BasePage>
);
}
};
export default BillingSuccessPage;
@@ -19,12 +19,14 @@ import useMediaQuery from "@mui/material/useMediaQuery";
import { useState } from "react";
import { useDelete } from "@/hooks/UseApi";
import { InitializingStatusBox } from "@/components/design-elements/StatusBox";
import { useAppSelector } from "@/hooks/AppHooks";
const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH;
const PageSpeedMonitorsPage = () => {
const theme = useTheme();
const { t } = useTranslation();
const { user } = useAppSelector((state) => state.auth);
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const [selectedMonitor, setSelectedMonitor] = useState<IMonitor | null>(null);
const isDialogOpen = Boolean(selectedMonitor);
@@ -64,6 +66,8 @@ const PageSpeedMonitorsPage = () => {
const downCount = response?.data?.downCount || 0;
const pausedCount = response?.data?.pausedCount || 0;
const monitorLimitReached = count >= (user?.entitlements?.monitorsMax || 0);
const handleConfirm = async () => {
await deleteFn(`/monitors/${selectedMonitor?._id}`);
setSelectedMonitor(null);
@@ -79,6 +83,7 @@ const PageSpeedMonitorsPage = () => {
loading={isValidating}
error={error}
items={monitors}
monitorLimitReached={monitorLimitReached}
page="infrastructure"
actionLink="/infrastructure/create"
>
@@ -19,11 +19,14 @@ import useMediaQuery from "@mui/material/useMediaQuery";
import { useState } from "react";
import { useDelete } from "@/hooks/UseApi";
import { InitializingStatusBox } from "@/components/design-elements/StatusBox";
import { useAppSelector } from "@/hooks/AppHooks";
const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH;
const PageSpeedMonitorsPage = () => {
const theme = useTheme();
const { user } = useAppSelector((state) => state.auth);
const { t } = useTranslation();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const [selectedMonitor, setSelectedMonitor] = useState<IMonitor | null>(null);
@@ -64,6 +67,8 @@ const PageSpeedMonitorsPage = () => {
const downCount = response?.data?.downCount || 0;
const pausedCount = response?.data?.pausedCount || 0;
const monitorLimitReached = count >= (user?.entitlements?.monitorsMax || 0);
const handleConfirm = async () => {
await deleteFn(`/monitors/${selectedMonitor?._id}`);
setSelectedMonitor(null);
@@ -81,6 +86,7 @@ const PageSpeedMonitorsPage = () => {
items={monitors}
page="pageSpeed"
actionLink="/pagespeed/create"
monitorLimitReached={monitorLimitReached}
>
<InfoBox
title="Website Performance Tracking"
+6 -1
View File
@@ -10,6 +10,7 @@ import { HeaderCreate } from "@/components/common";
import Stack from "@mui/material/Stack";
import { Dialog } from "@/components/inputs";
import { MonitorTable } from "@/pages/uptime/MonitorTable";
import { InitializingStatusBox } from "@/components/design-elements/StatusBox";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
@@ -26,12 +27,13 @@ import {
import useMediaQuery from "@mui/material/useMediaQuery";
import { useState } from "react";
import { useDelete } from "@/hooks/UseApi";
import { InitializingStatusBox } from "@/components/design-elements/StatusBox";
import { useAppSelector } from "@/hooks/AppHooks";
const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH;
const UptimeMonitors = () => {
const theme = useTheme();
const { user } = useAppSelector((state) => state.auth);
const { t } = useTranslation();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const [selectedMonitor, setSelectedMonitor] = useState<IMonitor | null>(null);
@@ -91,6 +93,8 @@ const UptimeMonitors = () => {
const downCount = response?.data?.downCount || 0;
const pausedCount = response?.data?.pausedCount || 0;
const monitorLimitReached = count >= (user?.entitlements?.monitorsMax || 0);
const handleConfirm = async () => {
await deleteFn(`/monitors/${selectedMonitor?._id}`);
setSelectedMonitor(null);
@@ -120,6 +124,7 @@ const UptimeMonitors = () => {
items={monitorItems}
page="uptime"
actionLink="/uptime/create"
monitorLimitReached={monitorLimitReached}
>
<InfoBox
title="Website & API Uptime Monitoring"
+6
View File
@@ -23,3 +23,9 @@ declare module "@mui/material/Button" {
accent: true;
}
}
declare module "@mui/material/CircularProgress" {
interface CircularProgressPropsColorOverrides {
accent: true;
}
}
+2
View File
@@ -14,6 +14,7 @@ export interface IEnvConfig {
SMTP_USER: string;
SMTP_PASS: string;
STRIPE_SECRET: string;
STRIPE_WEBHOOK_SECRET: string;
}
dotenv.config();
@@ -32,4 +33,5 @@ export const config: IEnvConfig = {
SMTP_USER: process.env.SMTP_USER || "not_set",
SMTP_PASS: process.env.SMTP_PASS || "not_set",
STRIPE_SECRET: process.env.STRIPE_SECRET || "not_set",
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET || "not_set",
};
@@ -12,6 +12,11 @@ export interface IBillingController {
res: Response,
next: NextFunction
) => Promise<void>;
confirmPlan: (
req: Request,
res: Response,
next: NextFunction
) => Promise<void>;
}
class BillingController implements IBillingController {
@@ -38,6 +43,7 @@ class BillingController implements IBillingController {
if (!orgId) throw new ApiError("Organization not found", 404);
const planKey: PlanKey | undefined = req.body.planKey;
const redirectUrl = await this.billingService.subscribePlan(
user.email,
orgId,
@@ -53,6 +59,44 @@ class BillingController implements IBillingController {
next(error);
}
};
cancelPlan = async (req: Request, res: Response, next: NextFunction) => {
try {
const orgId = req.user?.orgId;
if (!orgId) throw new ApiError("Organization not found", 404);
const redirectUrl = await this.billingService.cancelSubscription(orgId);
res.status(200).json({
message: "Subscription cancellation successful",
data: { redirectUrl },
});
return;
} catch (error) {
next(error);
}
};
confirmPlan = async (req: Request, res: Response, next: NextFunction) => {
try {
const orgId = req.user?.orgId;
if (!orgId) throw new ApiError("Organization not found", 404);
const expectedPlanKey = req.query.plan as PlanKey;
const success = await this.billingService.confirmPlan(
orgId,
expectedPlanKey
);
res.status(200).json({
message: "Plan confirmation endpoint hit",
data: { success },
});
return;
} catch (error) {
next(error);
}
};
}
export default BillingController;
+13 -32
View File
@@ -1,46 +1,27 @@
import type { Request, Response, NextFunction } from "express";
import { StripeService } from "@/services/index.js";
import { config } from "@/config/index.js";
export interface IStripeController {
webhook: (req: Request, res: Response, next: NextFunction) => Promise<void>;
}
class StripeController implements IStripeController {
private stripeService: StripeService;
constructor(stripeService: StripeService) {
this.stripeService = stripeService;
}
webhook = async (req: Request, res: Response, next: NextFunction) => {
try {
const sig = req.headers["stripe-signature"] as string | undefined;
const raw = req.body as unknown; // Buffer when express.raw is used
let event: any = undefined;
if (Buffer.isBuffer(raw)) {
const json = raw.toString("utf8");
try {
event = JSON.parse(json);
} catch (_e) {
res.status(401).json({ message: "Unauthorized" });
}
} else if (typeof raw === "string") {
try {
event = JSON.parse(raw);
} catch (_e) {
res.status(400).json({ message: "Invalid JSON payload" });
}
} else if (raw && typeof raw === "object") {
event = raw;
let event = req.body;
const signature = req.headers["stripe-signature"];
if (!signature || Array.isArray(signature)) {
throw new Error("Stripe signature is missing or invalid");
}
// Basic routing by event type (no-ops for now)
const type = event?.type;
switch (type) {
case "checkout.session.completed":
case "customer.subscription.updated":
case "customer.subscription.deleted":
case "invoice.payment_succeeded":
case "invoice.payment_failed":
default:
break;
}
res.status(200);
const result = await this.stripeService.webhook(event, signature);
res.status(200).send({ received: true });
} catch (error) {
throw error;
}
@@ -147,8 +147,6 @@ class TeamMemberController implements ITeamMemberController {
throw new ApiError("No team member ID", 400);
}
console.log(teamMemberId);
const deleted = await this.teamMemberService.delete(orgId, teamMemberId);
invalidateCachesForUser(teamMemberId);
if (!deleted) {
+1 -4
View File
@@ -18,12 +18,11 @@ export interface IOrg extends Document {
// Billing
billingCustomerId?: string;
subscriptionId?: string;
priceId: string;
priceIds?: string[];
subscriptionStatus?: SubscriptionStatus;
currentPeriodStart?: Date;
currentPeriodEnd?: Date;
trialEnd?: Date;
cancelAtPeriodEnd?: boolean;
// Cached entitlements
entitlements: {
@@ -61,8 +60,6 @@ const orgSchema = new Schema<IOrg>(
},
currentPeriodStart: { type: Date },
currentPeriodEnd: { type: Date },
trialEnd: { type: Date },
cancelAtPeriodEnd: { type: Boolean },
entitlements: {
plan: { type: String },
monitorsMax: { type: Number },
+1 -1
View File
@@ -76,7 +76,7 @@ export const initControllers = (services: any) => {
services.notificationService
);
controllers.stripeController = new StripeController();
controllers.stripeController = new StripeController(services.stripeService);
controllers.billingController = new BillingController(
services.billingService
);
+3
View File
@@ -28,6 +28,7 @@ import {
RecoveryService,
IncidentService,
SettingsService,
StripeService,
BillingService,
} from "@/services/index.js";
@@ -44,6 +45,7 @@ export const initServices = async () => {
const networkService = new NetworkService(got);
const statusService = new StatusService();
const settingsService = new SettingsService();
const stripeService = new StripeService();
const billingService = new BillingService();
const emailService = new EmailService(userService, settingsService);
settingsService.setEmailService(emailService);
@@ -99,6 +101,7 @@ export const initServices = async () => {
incidentService,
settingsService,
billingService,
stripeService,
};
Object.values(services).forEach((service) => {
@@ -61,8 +61,6 @@ const verifyOrgPermission = (resourceActions: string[]) => {
return next(new ApiError("User not authenticated", 401));
}
console.log(req.user);
const orgPermissions = userContext.roles?.orgRole?.permissions || [];
const allowed = hasPermission(orgPermissions, resourceActions);
if (!allowed) {
+12
View File
@@ -26,6 +26,18 @@ class BillingRoutes {
validateBody(subscribePlanSchema),
this.controller.subscribePlan
);
this.router.post(
"/cancel",
verifyToken,
addUserContext,
this.controller.cancelPlan
);
this.router.get(
"/confirm",
verifyToken,
addUserContext,
this.controller.confirmPlan
);
};
getRouter() {
+6 -3
View File
@@ -1,4 +1,4 @@
import { Router } from "express";
import { Router, raw } from "express";
import StripeController from "@/controllers/StripeController.js";
// Note: webhook route must be mounted with express.raw at app/init level.
@@ -12,7 +12,11 @@ class StripeRoutes {
}
initRoutes = () => {
this.router.post("/webhook", this.controller.webhook);
this.router.post(
"/webhook",
raw({ type: "application/json" }),
this.controller.webhook
);
};
getRouter() {
@@ -21,4 +25,3 @@ class StripeRoutes {
}
export default StripeRoutes;
+1
View File
@@ -24,5 +24,6 @@ export { default as JobGenerator } from "@/services/infrastructure/JobGenerator.
export { default as ServiceRegistry } from "@/services/system/ServiceRegistry.js";
export { default as SettingsService } from "@/services/system/SettingsService.js";
export { default as BillingService } from "@/services/system/BillingService.js";
export { default as StripeService } from "@/services/system/StripeService.js";
export * from "@/services/infrastructure/NotificationServices/index.js";
+104 -21
View File
@@ -15,6 +15,8 @@ export interface IBillingService {
orgId: string,
planKey: PlanKey
): Promise<string>;
cancelSubscription(orgId: string): Promise<string>;
confirmPlan(orgId: string, expectedPlanKey: PlanKey): Promise<boolean>;
}
const SERVICE_NAME = "BillingService";
@@ -27,10 +29,10 @@ class BillingService implements IBillingService {
this.SERVICE_NAME = SERVICE_NAME;
this.stripeClient = new Stripe(config.STRIPE_SECRET);
}
async listPlans(): Promise<Entitlements[]> {
listPlans = async () => {
const order: PlanKey[] = ["free", "pro", "business", "enterprise"];
return order.map((key) => Plans[key]);
}
};
subscribePlan = async (email: string, orgId: string, planKey: PlanKey) => {
if (!planKey) {
@@ -41,7 +43,6 @@ class BillingService implements IBillingService {
throw new ApiError("Invalid planKey", 400);
}
// Ensure billing customer exists for this org
const org = await Org.findById(orgId);
if (!org) throw new ApiError("Organization not found", 404);
if (!org.billingCustomerId) {
@@ -63,28 +64,110 @@ class BillingService implements IBillingService {
throw new ApiError("Price not found for the selected plan", 404);
}
let redirectUrl: string;
const successUrl = `${config.ORIGIN}/billing/success?plan=${planKey}`;
const cancelUrl = `${config.ORIGIN}/billing/`;
const session = await this.stripeClient.checkout.sessions.create({
billing_address_collection: "auto",
line_items: [
{
price,
quantity: 1,
const subscriptionId = org.subscriptionId;
// Updating an existing subscription
if (subscriptionId) {
const session = await this.stripeClient.billingPortal.sessions.create({
customer: String(org.billingCustomerId),
return_url: successUrl,
flow_data: {
type: "subscription_update",
subscription_update: {
subscription: subscriptionId,
},
},
],
mode: "subscription",
success_url: successUrl,
cancel_url: cancelUrl,
customer: String(org.billingCustomerId),
client_reference_id: String(org._id),
subscription_data: { metadata: { orgId: String(org._id) } },
metadata: { orgId: String(org._id) },
});
if (session.url === null) {
throw new ApiError("Failed to create checkout session", 500);
});
redirectUrl = session.url;
}
return session.url;
// Creating a new subscription
else {
const session = await this.stripeClient.checkout.sessions.create({
billing_address_collection: "auto",
line_items: [
{
price,
quantity: 1,
},
],
mode: "subscription",
success_url: successUrl,
cancel_url: cancelUrl,
customer: String(org.billingCustomerId),
client_reference_id: String(org._id),
subscription_data: { metadata: { orgId: String(org._id) } },
metadata: { orgId: String(org._id) },
});
if (!session.url) {
throw new ApiError("Failed to create checkout session", 500);
}
redirectUrl = session.url;
}
return redirectUrl;
};
cancelSubscription = async (orgId: string): Promise<string> => {
const successUrl = `${config.ORIGIN}/billing/success?plan=free`;
const org = await Org.findById(orgId);
if (!org) throw new ApiError("Organization not found", 404);
const subscriptionId = org.subscriptionId;
if (!subscriptionId) {
throw new ApiError("No active subscription found", 400);
}
const session = await this.stripeClient.billingPortal.sessions.create({
customer: String(org.billingCustomerId),
return_url: successUrl,
flow_data: {
type: "subscription_cancel",
subscription_cancel: {
subscription: subscriptionId,
},
},
});
const redirect = session.url;
if (!redirect) {
throw new ApiError("Failed to create billing portal session", 500);
}
return redirect;
};
confirmPlan = async (orgId: string, expectedPlanKey: PlanKey) => {
const org = await Org.findOne({ _id: orgId });
if (!org) throw new ApiError("Organization not found", 404);
if (!org.subscriptionId) {
return expectedPlanKey === "free";
}
const sub = await this.stripeClient.subscriptions.retrieve(
org.subscriptionId
);
const activePlan = sub.items.data[0]?.price.lookup_key;
if (activePlan === org.planKey) {
return true;
}
if (activePlan === expectedPlanKey) {
return true;
}
if (activePlan !== expectedPlanKey) {
return false;
}
return true;
};
}
+170
View File
@@ -0,0 +1,170 @@
import Stripe from "stripe";
import { config } from "@/config/index.js";
import {
Org,
Monitor,
StatusPage,
NotificationChannel,
} from "@/db/models/index.js";
import { PlanKey, Plans } from "@/types/entitlements.js";
const SERVICE_NAME = "StripeService";
interface IStripeService {
webhook: (
event: string | Buffer,
signature: string
) => Promise<Stripe.Subscription | null>;
}
class StripeService implements IStripeService {
public SERVICE_NAME: string;
private stripe: Stripe;
constructor() {
this.SERVICE_NAME = SERVICE_NAME;
this.stripe = new Stripe(config.STRIPE_SECRET);
}
private handleSubscription = async (subscription: Stripe.Subscription) => {
const orgId = subscription.metadata.orgId;
const subscriptionId = subscription.id;
const planKey =
(subscription.items.data[0]?.price.lookup_key as PlanKey) || "free";
const priceId = subscription.items.data[0]?.price.id || "unknown";
const status = subscription.status;
const currentPeriodStart =
subscription.items.data[0]?.current_period_start || 0;
const currentPeriodEnd =
subscription.items.data[0]?.current_period_end || 0;
await Org.findOneAndUpdate(
{ _id: orgId },
{
$set: {
planKey,
subscriptionId,
subscriptionStatus: status,
priceId,
currentPeriodStart,
currentPeriodEnd,
entitlements: Plans[planKey],
},
},
{ new: true }
);
await this.pruneResourcesForOrg(String(orgId), Plans[planKey]);
};
private handleCancel = async (subscription: Stripe.Subscription) => {
const orgId = subscription.metadata.orgId;
await Org.findOneAndUpdate(
{ _id: orgId },
{
$set: {
planKey: "free",
entitlements: Plans["free"],
},
$unset: {
subscriptionId: "",
subscriptionStatus: "",
priceId: "",
currentPeriodStart: "",
currentPeriodEnd: "",
},
},
{ new: true }
);
await this.pruneResourcesForOrg(String(orgId), Plans["free"]);
};
private pruneResourcesForOrg = async (
orgId: string,
entitlements: {
monitorsMax: number;
notificationChannelsMax: number;
statusPagesMax: number;
}
) => {
const prune = async (
model: any,
cap: number,
filter: Record<string, unknown>
) => {
if (!Number.isFinite(cap) || cap === Number.MAX_SAFE_INTEGER) return;
const count = await model.countDocuments(filter);
if (count <= cap) return;
const excess = count - cap;
const docs = await model
.find(filter)
.sort({ createdAt: -1 })
.limit(excess)
.select("_id");
const ids = docs.map((d: any) => d._id);
if (ids.length) {
await model.deleteMany({ _id: { $in: ids } });
}
};
const baseFilter = { orgId } as const;
await prune(Monitor, entitlements.monitorsMax, baseFilter);
await prune(
NotificationChannel,
entitlements.notificationChannelsMax,
baseFilter
);
await prune(StatusPage, entitlements.statusPagesMax, baseFilter);
};
webhook = async (event: string | Buffer, signature: string) => {
const whSecret = config.STRIPE_WEBHOOK_SECRET;
if (!whSecret || whSecret === "not_set") {
throw new Error("Stripe webhook secret is not set");
}
const stripeEvent = this.stripe.webhooks.constructEvent(
event,
signature,
whSecret
);
let subscription: Stripe.Subscription | null;
let status: Stripe.Subscription.Status;
switch (stripeEvent.type) {
case "customer.subscription.deleted":
subscription = stripeEvent.data.object;
status = subscription.status;
this.handleCancel(subscription);
break;
case "customer.subscription.created":
subscription = stripeEvent.data.object;
status = subscription.status;
this.handleSubscription(subscription);
break;
case "customer.subscription.updated":
subscription = stripeEvent.data.object;
status = subscription.status;
this.handleSubscription(subscription);
break;
case "checkout.session.completed":
subscription = null;
console.log("Checkout session completed.");
break;
case "invoice.payment_succeeded":
subscription = null;
console.log("Invoice payment succeeded.");
break;
default:
// Unexpected event type
subscription = null;
console.log(`Unhandled event type ${stripeEvent.type}.`);
}
return subscription;
};
}
export default StripeService;