From f1fbc95c0de998eded77d1b179eb16727bc749a0 Mon Sep 17 00:00:00 2001 From: ved015 Date: Sun, 24 May 2026 15:30:42 +0530 Subject: [PATCH] add top-up and credits in settings --- apps/web/components/settings/billing.tsx | 762 +++++++++++++++-------- apps/web/package.json | 6 +- bun.lock | 3 + packages/ui/components/dialog.tsx | 2 +- 4 files changed, 515 insertions(+), 258 deletions(-) diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index fcf450bc..8de02cf1 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -4,35 +4,41 @@ import { dmSans125ClassName } from "@/lib/fonts" import { calculateUsagePercent } from "@/lib/billing-utils" import { PLAN_DISPLAY_NAMES, useTokenUsage } from "@/hooks/use-token-usage" import { cn } from "@lib/utils" +import { useAuth } from "@lib/auth-context" import { Dialog, DialogClose, DialogContent, DialogTrigger, } from "@ui/components/dialog" -import { useQueryClient } from "@tanstack/react-query" -import { useCustomer, useListPlans } from "autumn-js/react" +import { Coins as PhosphorCoins } from "@phosphor-icons/react" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { useCustomer } from "autumn-js/react" import { - Check, CreditCard, ExternalLink, LoaderIcon, + Plus, ReceiptText, Settings, X, Zap, } from "lucide-react" -import { useEffect, useMemo, useState } from "react" +import { type ComponentType, useEffect, useMemo, useState } from "react" import { toast } from "sonner" const API_BASE = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const CREDIT_FEATURE_ID = "usd_credits" -const FALLBACK_TOP_UP_PLAN_ID = "api_topup" -const TOP_UP_AMOUNTS = [10, 25, 50] as const +const TOP_UP_PLAN_ID = "credits_topup" +const TOP_UP_AMOUNTS = [10, 25, 50, 100] as const const AUTO_TOP_UP_THRESHOLDS = [2, 5, 10] as const const AUTO_TOP_UP_AMOUNTS = [10, 25, 50] as const +const ConsoleCoinsIcon = PhosphorCoins as unknown as ComponentType<{ + className?: string + weight?: "light" +}> type BillingInvoice = { planIds?: string[] @@ -44,19 +50,26 @@ type BillingInvoice = { hostedInvoiceUrl?: string | null } -type BillingAutoTopup = { +type AutoTopupConfig = { featureId: string enabled: boolean threshold: number quantity: number - invoiceMode?: boolean - purchaseLimit?: { + purchaseLimit: { interval: "hour" | "day" | "week" | "month" - intervalCount?: number + intervalCount: number limit: number - } + } | null } +type AutoTopupsResponse = + | { + ok: true + hasPaymentMethod: boolean + autoTopup: AutoTopupConfig | null + } + | { ok: false; reason: string; message?: string } + function SectionTitle({ children, aside, @@ -133,8 +146,14 @@ function FieldSelect({ onChange: (value: number) => void disabled?: boolean }) { + const cols = values.length <= 3 ? "grid-cols-3" : "grid-cols-4" return ( -
+
{values.map((item) => ( -
-
- + + + + +
+
+

+ Buy Credits +

+

+ Add USD to your balance for metered usage. +

+
+ + + +
- -
-
-
-

- Auto top-up -

-

- Add credits automatically when the workspace balance gets - low. -

-
+
+
+

+ Choose an amount +

+ { + setTopUpAmount(value) + setCustomTopUpAmount("") + }} + disabled={topUpPendingAmount !== null} + /> +
+ + + setCustomTopUpAmount(event.target.value) + } + placeholder="e.g. 75" + type="number" + value={customTopUpAmount} + className="h-11 rounded-[10px] border border-white/10 bg-[#080B10] px-3 text-[14px] text-[#FAFAFA] outline-none placeholder:text-[#737373] focus:border-[#0054AD]" + /> +
+
+ +
+ +
+
+

+ Auto reload +

+ + {autoTopUpEnabled ? "on" : "off"} + +
+ +
+

+ Auto reload is{" "} + {autoTopUpEnabled ? "enabled" : "disabled"} +

+ +
+ + {!hasPaymentMethod && !activeAutoTopUp?.enabled ? ( +

+ Save a card in Manage Billing to enable automatic + reloads. +

+ ) : null} + +
+
+ + { + const value = Number.parseFloat( + event.target.value, + ) + setAutoTopUpThreshold( + Number.isFinite(value) ? value : 0, + ) + }} + type="number" + value={ + Number.isFinite(autoTopUpThreshold) + ? autoTopUpThreshold + : "" + } + className="h-10 rounded-[8px] border border-white/10 bg-[#080B10] px-3 text-[13px] text-[#FAFAFA] outline-none focus:border-[#0054AD] disabled:opacity-60" + /> +
+
+ + { + const value = Number.parseFloat( + event.target.value, + ) + setAutoTopUpAmount( + Number.isFinite(value) ? value : 0, + ) + }} + type="number" + value={ + Number.isFinite(autoTopUpAmount) + ? autoTopUpAmount + : "" + } + className="h-10 rounded-[8px] border border-white/10 bg-[#080B10] px-3 text-[13px] text-[#FAFAFA] outline-none focus:border-[#0054AD] disabled:opacity-60" + /> +
+
+ +
+
+
+ + + + {!isAdmin ? ( +

+ Only owners/admins can purchase credits. +

+ ) : null} +
+ + -
- -
-
-

- Trigger below -

- -
-
-

- Add each time -

- -
-
- -
-
- -

- Limit: up to 10 automatic top-ups per month. -

-
-
-
- + + {hasPaidPlan ? ( + +
+
+ +
+

+ Top-up credits{" "} + + (optional) + +

+

+ {creditRemaining > 0 + ? `${formatUsd(creditRemaining)} available` + : "No top-up credits yet"} +

+

+ Optional add-on that{" "} + + rolls over + {" "} + month-to-month, separate from your monthly usage above. +

+
+
+ +
+
+ ) : null} +
Invoice history - {autumn.isLoading ? ( + {invoicesQuery.isLoading ? (
@@ -823,6 +1070,11 @@ export default function Billing() {
{invoices.map((invoice) => { const date = formatDate(normalizeTimestamp(invoice.createdAt)) + const label = invoice.planIds?.length + ? invoice.planIds + .map((id) => getInvoiceProductLabel(id)) + .join(", ") + : "Billing invoice" return (
- {invoice.planIds?.length - ? invoice.planIds.join(", ") - : "Billing invoice"} + {label}

{invoice.stripeId} diff --git a/apps/web/package.json b/apps/web/package.json index 3535d9a4..847912a0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -2,7 +2,10 @@ "name": "@repo/web", "version": "0.1.0", "private": true, - "portless": { "name": "app.dev.supermemory", "script": "dev:app" }, + "portless": { + "name": "app.dev.supermemory", + "script": "dev:app" + }, "scripts": { "dev": "portless", "dev:app": "next dev --port ${PORT:-3000}", @@ -29,6 +32,7 @@ "@floating-ui/react": "^0.27.0", "@lobbyside/react": "0.2.0", "@opennextjs/cloudflare": "^1.12.0", + "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", diff --git a/bun.lock b/bun.lock index f1d890ca..ee980381 100644 --- a/bun.lock +++ b/bun.lock @@ -141,6 +141,7 @@ "@floating-ui/react": "^0.27.0", "@lobbyside/react": "0.2.0", "@opennextjs/cloudflare": "^1.12.0", + "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", @@ -1276,6 +1277,8 @@ "@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="], + "@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], "@pixi/colord": ["@pixi/colord@2.9.6", "", {}, "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA=="], diff --git a/packages/ui/components/dialog.tsx b/packages/ui/components/dialog.tsx index b40fabba..430b23b6 100644 --- a/packages/ui/components/dialog.tsx +++ b/packages/ui/components/dialog.tsx @@ -36,7 +36,7 @@ function DialogOverlay({ return (