diff --git a/apps/web/app/(ee)/api/cron/discount-codes/delete/route.ts b/apps/web/app/(ee)/api/cron/discount-codes/disable/route.ts similarity index 92% rename from apps/web/app/(ee)/api/cron/discount-codes/delete/route.ts rename to apps/web/app/(ee)/api/cron/discount-codes/disable/route.ts index 8a81938c602..7b4792d8c51 100644 --- a/apps/web/app/(ee)/api/cron/discount-codes/delete/route.ts +++ b/apps/web/app/(ee)/api/cron/discount-codes/disable/route.ts @@ -14,7 +14,7 @@ const inputSchema = z.object({ provider: z.enum(DiscountProvider), }); -// POST /api/cron/discount-codes/delete +// POST /api/cron/discount-codes/disable – disable a discount code from the provider (Stripe, Shopify, etc.) export const POST = withCron(async ({ rawBody }) => { const { provider, code, programId } = inputSchema.parse(JSON.parse(rawBody)); diff --git a/apps/web/app/(ee)/api/cron/partners/ban/route.ts b/apps/web/app/(ee)/api/cron/partners/ban/route.ts index 84bd7d69a48..89ac41c8851 100644 --- a/apps/web/app/(ee)/api/cron/partners/ban/route.ts +++ b/apps/web/app/(ee)/api/cron/partners/ban/route.ts @@ -138,7 +138,7 @@ export const POST = withCron(async ({ rawBody }) => { recordLink(links, { deleted: true }), // Queue discount code deletions - deleteDiscountCodes(discountCodes), + deleteDiscountCodes(discountCodes, { isSoftDelete: true }), ]); // Send email diff --git a/apps/web/app/(ee)/api/cron/partners/deactivate/route.ts b/apps/web/app/(ee)/api/cron/partners/deactivate/route.ts index a2db6546a77..2658f91a608 100644 --- a/apps/web/app/(ee)/api/cron/partners/deactivate/route.ts +++ b/apps/web/app/(ee)/api/cron/partners/deactivate/route.ts @@ -58,7 +58,7 @@ export const POST = withCron(async ({ rawBody }) => { const discountCodes = programEnrollments.flatMap(({ discountCodes }) => discountCodes.map((dc) => dc), ); - await deleteDiscountCodes(discountCodes); + await deleteDiscountCodes(discountCodes, { isSoftDelete: true }); console.log("[bulkDeactivatePartners] Queued discount code deletions."); // Find the program diff --git a/apps/web/app/(ee)/api/discount-codes/route.ts b/apps/web/app/(ee)/api/discount-codes/route.ts index db5f9870a55..9b2c81de3c9 100644 --- a/apps/web/app/(ee)/api/discount-codes/route.ts +++ b/apps/web/app/(ee)/api/discount-codes/route.ts @@ -11,6 +11,7 @@ import { DiscountCodeSchema, getDiscountCodesQuerySchema, } from "@/lib/zod/schemas/discount"; +import { APP_DOMAIN } from "@dub/utils"; import { waitUntil } from "@vercel/functions"; import { NextResponse } from "next/server"; @@ -100,12 +101,15 @@ export const POST = withWorkspace( code, }, }, + include: { + partner: true, + }, }); if (duplicateByCode) { throw new DubApiError({ - code: "bad_request", - message: `A discount with the code ${code} already exists in the program. Please choose a different code.`, + code: "conflict", + message: `This discount code "${code}" is already in use by [${duplicateByCode.partner.email}](${APP_DOMAIN}/${workspace.slug}/program/partners/${duplicateByCode.partner.id}). Please choose a different code.`, }); } } diff --git a/apps/web/app/(ee)/api/partner-profile/programs/[programId]/links/route.ts b/apps/web/app/(ee)/api/partner-profile/programs/[programId]/links/route.ts index a068d86f35d..fb1f32983df 100644 --- a/apps/web/app/(ee)/api/partner-profile/programs/[programId]/links/route.ts +++ b/apps/web/app/(ee)/api/partner-profile/programs/[programId]/links/route.ts @@ -37,6 +37,7 @@ export const GET = withPartnerProfile(async ({ partner, params }) => { return { ...link, discountCode: discountCode?.code, + discountCodeDisabledAt: discountCode?.disabledAt ?? null, }; }); diff --git a/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts b/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts index d7465ab9c01..3376fbc1acf 100644 --- a/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts +++ b/apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts @@ -70,14 +70,21 @@ export async function attributeViaPromotionCodeId({ code: promotionCode.code, }, }, - select: { + include: { link: true, }, }); if (!discountCode) { console.log( - `Couldn't find link associated with promotion code ${promotionCode.code}, skipping...`, + `Couldn't find discount code "${promotionCode.code}" in program "${workspace.defaultProgramId}", skipping...`, + ); + return null; + } + + if (discountCode.disabledAt) { + console.log( + `Discount code "${discountCode.code}" is disabled, skipping...`, ); return null; } diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/links/partner-link-card.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/links/partner-link-card.tsx index aa2bcde4361..9bb044a58ce 100644 --- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/links/partner-link-card.tsx +++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/links/partner-link-card.tsx @@ -28,6 +28,7 @@ import { getApexDomain, getPrettyUrl, nFormatter, + PARTNERS_DOMAIN, } from "@dub/utils"; import NumberFlow from "@number-flow/react"; import Link from "next/link"; @@ -75,6 +76,19 @@ export function PartnerLinkCard({ link }: { link: PartnerProfileLinkProps }) { const isDeactivated = programEnrollment?.status === "deactivated"; + const discountCodeSection = link.discountCode ? ( +
+ + Discount code + + +
+ ) : null; + return ( ); })()} - {link.discountCode && ( - -
- - Discount code - - -
-
- )} + {discountCodeSection && + (link.discountCodeDisabledAt ? ( + discountCodeSection + ) : ( + + {discountCodeSection} + + ))} {displayOption === "cards" && } diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/links/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/links/page.tsx index 80704276aa0..11256da1407 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/links/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/links/page.tsx @@ -333,7 +333,12 @@ const PartnerDiscountCodes = ({ { id: "code", header: "Code", - cell: ({ row }) => , + cell: ({ row }) => ( + + ), }, { id: "shortLink", diff --git a/apps/web/lib/discounts/delete-discount-code.ts b/apps/web/lib/discounts/delete-discount-code.ts index e02953d0bf1..33c0232b363 100644 --- a/apps/web/lib/discounts/delete-discount-code.ts +++ b/apps/web/lib/discounts/delete-discount-code.ts @@ -17,6 +17,7 @@ type DeleteDiscountCodesParams = Pick< // 4. When a partner is moved to a different group export async function deleteDiscountCodes( input: (DeleteDiscountCodesParams | null | undefined)[], + { isSoftDelete = false }: { isSoftDelete?: boolean } = {}, ) { const discountCodes = input.filter( (dc): dc is NonNullable => dc != null, @@ -29,18 +30,36 @@ export async function deleteDiscountCodes( return; } - // Delete the discount codes from the database - const deletedDiscountCodes = await prisma.discountCode.deleteMany({ - where: { - id: { - in: discountCodes.map(({ id }) => id), + if (isSoftDelete) { + // Soft delete the discount codes from the database (mark them as disabled) + const disabledDiscountCodes = await prisma.discountCode.updateMany({ + where: { + id: { + in: discountCodes.map(({ id }) => id), + }, + }, + data: { + disabledAt: new Date(), }, - }, - }); + }); - console.log( - `[deleteDiscountCodes] Deleted ${deletedDiscountCodes.count} discount codes.`, - ); + console.log( + `[deleteDiscountCodes] Disabled ${disabledDiscountCodes.count} discount codes.`, + ); + } else { + // Delete the discount codes from the database + const deletedDiscountCodes = await prisma.discountCode.deleteMany({ + where: { + id: { + in: discountCodes.map(({ id }) => id), + }, + }, + }); + + console.log( + `[deleteDiscountCodes] Deleted ${deletedDiscountCodes.count} discount codes.`, + ); + } // Only enqueue external-provider cleanup for codes whose provider is known. // Orphaned codes (discount relation is null) still get deleted locally above @@ -63,7 +82,7 @@ export async function deleteDiscountCodes( for (const chunkOfCodes of chunks) { await enqueueBatchJobs( chunkOfCodes.map((discountCode) => ({ - url: `${APP_DOMAIN_WITH_NGROK}/api/cron/discount-codes/delete`, + url: `${APP_DOMAIN_WITH_NGROK}/api/cron/discount-codes/disable`, method: "POST", queueName: "delete-discount-code", body: { diff --git a/apps/web/lib/zod/schemas/discount.ts b/apps/web/lib/zod/schemas/discount.ts index 44c773972b2..194d6d422f2 100644 --- a/apps/web/lib/zod/schemas/discount.ts +++ b/apps/web/lib/zod/schemas/discount.ts @@ -62,6 +62,12 @@ export const DiscountCodeSchema = z.object({ discountId: z.string().nullable(), partnerId: z.string(), linkId: z.string(), + disabledAt: z.coerce + .date() + .nullish() + .describe( + "When this discount code was disabled, which happens when a partner is banned or deactivated.", + ), }); export const createDiscountCodeSchema = z.object({ diff --git a/apps/web/lib/zod/schemas/partner-profile.ts b/apps/web/lib/zod/schemas/partner-profile.ts index 608d98a1faf..303279daf98 100644 --- a/apps/web/lib/zod/schemas/partner-profile.ts +++ b/apps/web/lib/zod/schemas/partner-profile.ts @@ -100,6 +100,7 @@ export const PartnerProfileLinkSchema = LinkSchema.pick({ createdAt: z.string().or(z.date()), partnerGroupDefaultLinkId: z.string().nullish(), discountCode: z.string().nullable().default(null), + discountCodeDisabledAt: z.coerce.date().nullable().default(null), }); export const PartnerProfileCustomerSchema = CustomerEnrichedSchema.pick({ diff --git a/apps/web/package.json b/apps/web/package.json index 32181801d15..ca16b4d5978 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -129,7 +129,7 @@ "remark-gfm": "^4.0.0", "sanitize-html": "^2.17.0", "shiki": "^1.14.1", - "sonner": "^1.4.41", + "sonner": "^2.0.7", "streamdown": "^2.3.0", "stripe": "^18.2.0", "svix": "^1.76.1", diff --git a/apps/web/prisma/schema/discount.prisma b/apps/web/prisma/schema/discount.prisma index 937d56377fe..296dc260438 100644 --- a/apps/web/prisma/schema/discount.prisma +++ b/apps/web/prisma/schema/discount.prisma @@ -34,6 +34,7 @@ model DiscountCode { linkId String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + disabledAt DateTime? program Program @relation(fields: [programId], references: [id], onDelete: Cascade) discount Discount? @relation(fields: [discountId], references: [id], onDelete: SetNull) diff --git a/apps/web/ui/modals/add-discount-code-modal.tsx b/apps/web/ui/modals/add-discount-code-modal.tsx index 5c361cd8c82..bbdf5ac6b87 100644 --- a/apps/web/ui/modals/add-discount-code-modal.tsx +++ b/apps/web/ui/modals/add-discount-code-modal.tsx @@ -18,7 +18,8 @@ import { toast } from "sonner"; import { useDebounce } from "use-debounce"; import * as z from "zod/v4"; import { ERROR_MAP } from "../partners/constants"; -import { X } from "../shared/icons"; +import { CustomToast } from "../shared/custom-toast"; +import { AlertCircleFill, X } from "../shared/icons"; import { UpgradeRequiredToast } from "../shared/upgrade-required-toast"; type FormData = z.infer; @@ -87,28 +88,30 @@ const AddDiscountCodeModal = ({ toast.success("Discount code created and copied to clipboard!"); }, onError: (error) => { - if (error) { - const code = Object.keys(ERROR_MAP).find((key) => - error.startsWith(key), - ); + const code = Object.keys(ERROR_MAP).find((key) => + error.startsWith(key), + ); - if (code) { - const { title, ctaLabel, ctaUrl } = ERROR_MAP[code]; - const message = error.replace(`${code}: `, ""); + if (code) { + const { title, ctaLabel, ctaUrl } = ERROR_MAP[code]; + const message = error.replace(`${code}: `, ""); - toast.custom(() => ( - - )); - return; - } + toast.custom(() => ( + + )); + return; + } else if (error.includes("already in use")) { + toast.custom(() => ( + {error} + )); + } else { + toast.error(error); } - - toast.error(error); }, }); }; diff --git a/apps/web/ui/partners/discounts/discount-code-badge.tsx b/apps/web/ui/partners/discounts/discount-code-badge.tsx index a51d3a01081..e91310f4aef 100644 --- a/apps/web/ui/partners/discounts/discount-code-badge.tsx +++ b/apps/web/ui/partners/discounts/discount-code-badge.tsx @@ -1,9 +1,51 @@ -import { Tag, useCopyToClipboard } from "@dub/ui"; +import { Tag, Tooltip, useCopyToClipboard } from "@dub/ui"; import { cn } from "@dub/utils"; import { toast } from "sonner"; -export function DiscountCodeBadge({ code }: { code: string }) { +export function DiscountCodeBadge({ + code, + disabledAt, + disabledTooltip = "This discount code was disabled because the partner was banned or deactivated. To re-enable it, delete this code and create a new one.", +}: { + code: string; + disabledAt?: Date | string | null; + disabledTooltip?: string; +}) { const [copied, copyToClipboard] = useCopyToClipboard(); + const isDisabled = !!disabledAt; + + const content = ( + <> + +
+ {code} +
+ + ); + + if (isDisabled) { + return ( + +
+ {content} +
+
+ ); + } + return ( ); } diff --git a/apps/web/ui/shared/custom-toast.tsx b/apps/web/ui/shared/custom-toast.tsx index e6dc298b4f1..5666c6d6d93 100644 --- a/apps/web/ui/shared/custom-toast.tsx +++ b/apps/web/ui/shared/custom-toast.tsx @@ -1,4 +1,4 @@ -import ReactMarkdown from "react-markdown"; +import { MarkdownDescription } from "./markdown-description"; export function CustomToast({ icon: Icon, @@ -10,20 +10,9 @@ export function CustomToast({ return ( ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf6031bf853..1925b54eefb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -366,8 +366,8 @@ importers: specifier: ^1.14.1 version: 1.14.1 sonner: - specifier: ^1.4.41 - version: 1.4.41(react-dom@19.1.3(react@19.1.3))(react@19.1.3) + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.1.3(react@19.1.3))(react@19.1.3) streamdown: specifier: ^2.3.0 version: 2.3.0(react-dom@19.1.3(react@19.1.3))(react@19.1.3) @@ -672,7 +672,7 @@ importers: dependencies: '@hubspot/cli': specifier: ^7.6.2 - version: 7.6.2(@babel/core@7.28.4)(@types/node@18.11.9)(@types/react-dom@19.1.9(@types/react@19.1.15))(@types/react@19.1.15)(encoding@0.1.13)(prettier@3.6.2)(rollup@4.52.5)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.27.0)(typescript@5.6.2)(vite@5.4.8(@types/node@18.11.9)(terser@5.27.0)) + version: 7.6.2(@babel/core@7.24.5)(@types/node@18.11.9)(@types/react-dom@19.1.9(@types/react@19.1.15))(@types/react@19.1.15)(encoding@0.1.13)(prettier@3.6.2)(rollup@4.52.5)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.27.0)(typescript@5.6.2)(vite@5.4.8(@types/node@18.11.9)(terser@5.27.0)) devDependencies: '@types/node': specifier: 18.11.9 @@ -11964,6 +11964,12 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -14502,9 +14508,9 @@ snapshots: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.24.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.27.1 optional: true @@ -15334,7 +15340,7 @@ snapshots: - encoding - supports-color - '@hubspot/cli@7.6.2(@babel/core@7.28.4)(@types/node@18.11.9)(@types/react-dom@19.1.9(@types/react@19.1.15))(@types/react@19.1.15)(encoding@0.1.13)(prettier@3.6.2)(rollup@4.52.5)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.27.0)(typescript@5.6.2)(vite@5.4.8(@types/node@18.11.9)(terser@5.27.0))': + '@hubspot/cli@7.6.2(@babel/core@7.24.5)(@types/node@18.11.9)(@types/react-dom@19.1.9(@types/react@19.1.15))(@types/react@19.1.15)(encoding@0.1.13)(prettier@3.6.2)(rollup@4.52.5)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.27.0)(typescript@5.6.2)(vite@5.4.8(@types/node@18.11.9)(terser@5.27.0))': dependencies: '@hubspot/local-dev-lib': 3.19.1 '@hubspot/project-parsing-lib': 0.8.6(@hubspot/local-dev-lib@3.19.1) @@ -15365,7 +15371,7 @@ snapshots: yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@hubspot/cms-dev-server': 1.0.38(@babel/core@7.28.4)(@types/node@18.11.9)(@types/react-dom@19.1.9(@types/react@19.1.15))(@types/react@19.1.15)(encoding@0.1.13)(prettier@3.6.2)(rollup@4.52.5)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.27.0) + '@hubspot/cms-dev-server': 1.0.38(@babel/core@7.24.5)(@types/node@18.11.9)(@types/react-dom@19.1.9(@types/react@19.1.15))(@types/react@19.1.15)(encoding@0.1.13)(prettier@3.6.2)(rollup@4.52.5)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.27.0) '@modelcontextprotocol/sdk': 1.13.3 transitivePeerDependencies: - '@babel/core' @@ -15403,7 +15409,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optional: true - '@hubspot/cms-dev-server@1.0.38(@babel/core@7.28.4)(@types/node@18.11.9)(@types/react-dom@19.1.9(@types/react@19.1.15))(@types/react@19.1.15)(encoding@0.1.13)(prettier@3.6.2)(rollup@4.52.5)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.27.0)': + '@hubspot/cms-dev-server@1.0.38(@babel/core@7.24.5)(@types/node@18.11.9)(@types/react-dom@19.1.9(@types/react@19.1.15))(@types/react@19.1.15)(encoding@0.1.13)(prettier@3.6.2)(rollup@4.52.5)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.27.0)': dependencies: '@babel/code-frame': 7.26.2 '@babel/parser': 7.26.2 @@ -15430,7 +15436,7 @@ snapshots: '@vitejs/plugin-react': 4.7.0(vite@5.4.8(@types/node@18.11.9)(terser@5.27.0)) ansi-to-html: 0.7.2 babel-plugin-macros: 3.1.0 - babel-plugin-styled-components: 2.1.4(@babel/core@7.28.4)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + babel-plugin-styled-components: 2.1.4(@babel/core@7.24.5)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) chalk: 5.4.1 class-variance-authority: 0.7.0 cli-progress: 3.12.0 @@ -15449,7 +15455,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) request: 2.88.2 storybook: 8.6.14(prettier@3.6.2) - styled-jsx: 5.1.2(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react@18.3.1) + styled-jsx: 5.1.2(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react@18.3.1) tailwind-merge: 2.6.0 tailwindcss-animate: 1.0.7 typescript: 4.7.4 @@ -20478,11 +20484,11 @@ snapshots: resolve: 1.22.6 optional: true - babel-plugin-styled-components@2.1.4(@babel/core@7.28.4)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + babel-plugin-styled-components@2.1.4(@babel/core@7.24.5)(styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-module-imports': 7.25.9 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.24.5) lodash: 4.17.21 picomatch: 2.3.1 styled-components: 6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -27267,6 +27273,11 @@ snapshots: react: 19.1.3 react-dom: 19.1.3(react@19.1.3) + sonner@2.0.7(react-dom@19.1.3(react@19.1.3))(react@19.1.3): + dependencies: + react: 19.1.3 + react-dom: 19.1.3(react@19.1.3) + source-map-js@1.2.0: {} source-map-js@1.2.1: {} @@ -27566,12 +27577,12 @@ snapshots: tslib: 2.6.2 optional: true - styled-jsx@5.1.2(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react@18.3.1): + styled-jsx@5.1.2(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 optionalDependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.24.5 babel-plugin-macros: 3.1.0 optional: true