mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-07-26 09:24:05 +00:00
Integrate the new api_max plan ($100/mo, $130 credits) from mono into the
nova web app, mirroring mono's tier semantics.
- Tiers: insert api_max between pro and scale; resolves to Pro-tier for feature gates while displaying as "Max" (queries.ts, use-token-usage)
- Billing page: Max plan card ("Most Popular"), checkout/cancel/invoice wiring; advanced carousel page now 3-up (Max/Scale/Enterprise); Scale builds on Max
- Carousel auto-opens to the page holding the current plan (Max/Scale/ Enterprise users land on their own card instead of Free+Pro)
- Cancel: retention dialog listing what you lose + type-to-confirm "CANCEL"
- Cancel/resume state mirroring console: detect canceledAt-scheduled cancellation, show "Cancelling" pill + "Cancels on {date} · N days left", and a "Resume plan" (uncancel) action
179 lines
4.8 KiB
TypeScript
179 lines
4.8 KiB
TypeScript
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
|
import { toast } from "sonner"
|
|
import type { z } from "zod"
|
|
import type { DocumentsWithMemoriesResponseSchema } from "../validation/api"
|
|
import { $fetch } from "./api"
|
|
|
|
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
|
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
|
|
|
export const PLAN_TIERS = [
|
|
"api_pro",
|
|
"api_max",
|
|
"api_scale",
|
|
"api_enterprise",
|
|
] as const
|
|
export type PlanTier = (typeof PLAN_TIERS)[number]
|
|
|
|
export type SubscriptionStatusMap = Record<
|
|
string,
|
|
{ allowed: boolean; status: string | null }
|
|
>
|
|
|
|
const DEFAULT_SUBSCRIPTION_STATUS: SubscriptionStatusMap = {
|
|
api_pro: { allowed: false, status: null },
|
|
api_max: { allowed: false, status: null },
|
|
api_scale: { allowed: false, status: null },
|
|
api_enterprise: { allowed: false, status: null },
|
|
}
|
|
|
|
export function isAllowedFrom(
|
|
status: SubscriptionStatusMap,
|
|
minimumTier: PlanTier,
|
|
): boolean {
|
|
const minIndex = PLAN_TIERS.indexOf(minimumTier)
|
|
return PLAN_TIERS.slice(minIndex).some((tier) => {
|
|
const s = status[tier]
|
|
return s?.status === "active"
|
|
})
|
|
}
|
|
|
|
export function getSubscriptionStatus(
|
|
subscriptions: Array<{ planId: string; status: string }> | undefined,
|
|
): SubscriptionStatusMap {
|
|
const statusMap: SubscriptionStatusMap = { ...DEFAULT_SUBSCRIPTION_STATUS }
|
|
if (!subscriptions) return statusMap
|
|
|
|
const subMap = new Map(subscriptions.map((s) => [s.planId, s]))
|
|
|
|
for (const tier of PLAN_TIERS) {
|
|
const sub = subMap.get(tier)
|
|
statusMap[tier] = {
|
|
allowed: sub?.status === "active",
|
|
status: sub?.status ?? null,
|
|
}
|
|
}
|
|
return statusMap
|
|
}
|
|
|
|
export function hasActivePlan(
|
|
subscriptions: Array<{ planId: string; status: string }> | undefined,
|
|
minimumTier: PlanTier,
|
|
): boolean {
|
|
return isAllowedFrom(getSubscriptionStatus(subscriptions), minimumTier)
|
|
}
|
|
|
|
export type CanceledSubscription = { planId: string; endsAt: number | null }
|
|
|
|
// A subscription scheduled to cancel at period end: still active, but canceledAt is set.
|
|
export function getCanceledSubscription(
|
|
subscriptions:
|
|
| Array<{
|
|
planId: string
|
|
status?: string
|
|
canceledAt?: number | null
|
|
currentPeriodEnd?: number | null
|
|
expiresAt?: number | null
|
|
}>
|
|
| undefined,
|
|
): CanceledSubscription | null {
|
|
const sub = subscriptions?.find(
|
|
(s) =>
|
|
s.status === "active" &&
|
|
s.canceledAt != null &&
|
|
(PLAN_TIERS as readonly string[]).includes(s.planId),
|
|
)
|
|
if (!sub) return null
|
|
return {
|
|
planId: sub.planId,
|
|
endsAt: sub.currentPeriodEnd ?? sub.expiresAt ?? null,
|
|
}
|
|
}
|
|
|
|
export const useDeleteDocument = (selectedProject: string) => {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (documentId: string) => {
|
|
// context for LLM: delete/memories/:documentId is documents delete endpoint not memories delete endpoint
|
|
const response = await $fetch(`@delete/documents/${documentId}`)
|
|
if (response.error) {
|
|
throw new Error(response.error?.message || "Failed to delete document")
|
|
}
|
|
return response.data
|
|
},
|
|
onMutate: async (documentId: string) => {
|
|
await queryClient.cancelQueries({
|
|
queryKey: ["documents-with-memories", selectedProject],
|
|
})
|
|
|
|
const previousData = queryClient.getQueryData([
|
|
"documents-with-memories",
|
|
selectedProject,
|
|
])
|
|
|
|
queryClient.setQueryData(
|
|
["documents-with-memories", selectedProject],
|
|
(old: unknown) => {
|
|
if (!old || typeof old !== "object") return old
|
|
|
|
// Handle Infinite Query structure (TanStack Query v5 uses 'pages')
|
|
if (
|
|
"pages" in old &&
|
|
Array.isArray((old as Record<string, unknown>).pages)
|
|
) {
|
|
const typedOld = old as {
|
|
pages: Array<{ documents?: DocumentWithMemories[] }>
|
|
}
|
|
return {
|
|
...typedOld,
|
|
pages: typedOld.pages.map((page) => ({
|
|
...page,
|
|
documents: page.documents?.filter(
|
|
(doc: DocumentWithMemories) => doc.id !== documentId,
|
|
),
|
|
})),
|
|
}
|
|
}
|
|
|
|
// Handle Standard Query structure
|
|
if (
|
|
"documents" in old &&
|
|
Array.isArray((old as Record<string, unknown>).documents)
|
|
) {
|
|
const typedOld = old as { documents: DocumentWithMemories[] }
|
|
return {
|
|
...typedOld,
|
|
documents: typedOld.documents.filter(
|
|
(doc: DocumentWithMemories) => doc.id !== documentId,
|
|
),
|
|
}
|
|
}
|
|
|
|
return old
|
|
},
|
|
)
|
|
|
|
return { previousData }
|
|
},
|
|
onSuccess: () => {
|
|
toast.success("Memory deleted successfully")
|
|
},
|
|
onError: (error, _documentId, context) => {
|
|
if (context?.previousData) {
|
|
queryClient.setQueryData(
|
|
["documents-with-memories", selectedProject],
|
|
context.previousData,
|
|
)
|
|
}
|
|
toast.error("Failed to delete memory", {
|
|
description: error instanceof Error ? error.message : "Unknown error",
|
|
})
|
|
},
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["documents-with-memories", selectedProject],
|
|
})
|
|
},
|
|
})
|
|
}
|