mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat: enhance billing panel with auto-recharge and credit management features
- Added auto-recharge settings management, including fetching, updating, and toggling auto-recharge functionality. - Integrated credit balance and team usage retrieval to provide users with real-time financial insights. - Implemented card management features, allowing users to add, update, and delete saved payment methods. - Updated API interactions to support new endpoints for credits and auto-recharge functionalities. - Improved user experience with loading states and error handling for credit-related operations.
This commit is contained in:
@@ -39,7 +39,12 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@noble/hashes": "^2.0.1",
|
||||
"@noble/secp256k1": "^3.0.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@scure/bip32": "^2.0.1",
|
||||
"@scure/bip39": "^2.0.1",
|
||||
"@sentry/react": "^10.38.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-deep-link": "^2",
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { billingApi } from '../../../services/api/billingApi';
|
||||
import {
|
||||
type AutoRechargeSettings,
|
||||
type CreditBalance,
|
||||
creditsApi,
|
||||
type SavedCard,
|
||||
type TeamUsage,
|
||||
} from '../../../services/api/creditsApi';
|
||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||
import { fetchCurrentUser } from '../../../store/userSlice';
|
||||
import type { PlanTier } from '../../../types/api';
|
||||
@@ -15,6 +22,25 @@ import {
|
||||
PLANS,
|
||||
} from './billingHelpers';
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────────────
|
||||
const THRESHOLD_OPTIONS = [5, 10, 20] as const;
|
||||
const RECHARGE_OPTIONS = [10, 20, 50, 100] as const;
|
||||
const WEEKLY_LIMIT_OPTIONS = [25, 50, 100, 200, 500] as const;
|
||||
|
||||
const CARD_BRAND_LABELS: Record<string, string> = {
|
||||
visa: 'Visa',
|
||||
mastercard: 'Mastercard',
|
||||
amex: 'Amex',
|
||||
discover: 'Discover',
|
||||
jcb: 'JCB',
|
||||
diners: 'Diners',
|
||||
unionpay: 'UnionPay',
|
||||
};
|
||||
|
||||
function cardBrandLabel(brand: string) {
|
||||
return CARD_BRAND_LABELS[brand.toLowerCase()] ?? brand.charAt(0).toUpperCase() + brand.slice(1);
|
||||
}
|
||||
|
||||
// ── Component ───────────────────────────────────────────────────────────
|
||||
const BillingPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
@@ -31,7 +57,12 @@ const BillingPanel = () => {
|
||||
const currentTier: PlanTier = activeTeam?.team.subscription?.plan ?? 'FREE';
|
||||
const hasActive = activeTeam?.team.subscription?.hasActiveSubscription ?? false;
|
||||
const planExpiry = activeTeam?.team.subscription?.planExpiry;
|
||||
const usage = user?.usage;
|
||||
|
||||
// Credits & usage state
|
||||
const [creditBalance, setCreditBalance] = useState<CreditBalance | null>(null);
|
||||
const [teamUsage, setTeamUsage] = useState<TeamUsage | null>(null);
|
||||
const [isLoadingCredits, setIsLoadingCredits] = useState(false);
|
||||
const [isToppingUp, setIsToppingUp] = useState(false);
|
||||
|
||||
// Local state
|
||||
const [billingInterval, setBillingInterval] = useState<'monthly' | 'annual'>('monthly');
|
||||
@@ -43,9 +74,45 @@ const BillingPanel = () => {
|
||||
const pollStartRef = useRef<number>(0);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
// Fetch current plan on mount
|
||||
// ── Auto-recharge state ──────────────────────────────────────────────────
|
||||
const [arSettings, setArSettings] = useState<AutoRechargeSettings | null>(null);
|
||||
const [arLoading, setArLoading] = useState(true);
|
||||
const [arError, setArError] = useState<string | null>(null);
|
||||
const [arSaving, setArSaving] = useState(false);
|
||||
const [arThreshold, setArThreshold] = useState(5);
|
||||
const [arAmount, setArAmount] = useState(20);
|
||||
const [arWeeklyLimit, setArWeeklyLimit] = useState(50);
|
||||
const [arDirty, setArDirty] = useState(false);
|
||||
|
||||
// Recompute dirty flag whenever local settings or server settings change
|
||||
useEffect(() => {
|
||||
if (!arSettings) return;
|
||||
setArDirty(
|
||||
arThreshold !== arSettings.thresholdUsd ||
|
||||
arAmount !== arSettings.rechargeAmountUsd ||
|
||||
arWeeklyLimit !== arSettings.weeklyLimitUsd
|
||||
);
|
||||
}, [arThreshold, arAmount, arWeeklyLimit, arSettings]);
|
||||
|
||||
// ── Cards state ──────────────────────────────────────────────────────────
|
||||
const [cards, setCards] = useState<SavedCard[]>([]);
|
||||
const [cardsLoading, setCardsLoading] = useState(true);
|
||||
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
||||
const [deletingCardId, setDeletingCardId] = useState<string | null>(null);
|
||||
const [settingDefaultId, setSettingDefaultId] = useState<string | null>(null);
|
||||
|
||||
// Fetch current plan, credits balance, and team usage on mount
|
||||
useEffect(() => {
|
||||
billingApi.getCurrentPlan().catch(console.error);
|
||||
|
||||
setIsLoadingCredits(true);
|
||||
Promise.all([creditsApi.getBalance(), creditsApi.getTeamUsage()])
|
||||
.then(([balance, usage]) => {
|
||||
setCreditBalance(balance);
|
||||
setTeamUsage(usage);
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setIsLoadingCredits(false));
|
||||
}, []);
|
||||
|
||||
// When crypto is selected, force annual
|
||||
@@ -62,6 +129,130 @@ const BillingPanel = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ── Fetch auto-recharge settings + cards on mount ────────────────────────
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const [settings, cardsData] = await Promise.all([
|
||||
creditsApi.getAutoRecharge(),
|
||||
creditsApi.getCards(),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
setArSettings(settings);
|
||||
setArThreshold(settings.thresholdUsd);
|
||||
setArAmount(settings.rechargeAmountUsd);
|
||||
setArWeeklyLimit(settings.weeklyLimitUsd);
|
||||
setCards(cardsData.cards);
|
||||
} catch {
|
||||
if (!cancelled) setArError('Failed to load auto-recharge settings.');
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setArLoading(false);
|
||||
setCardsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
load().catch(console.error);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ── Auto-recharge handlers ───────────────────────────────────────────────
|
||||
const handleArToggle = async () => {
|
||||
if (!arSettings || arSaving) return;
|
||||
const nextEnabled = !arSettings.enabled;
|
||||
|
||||
// Prevent enabling without a saved card
|
||||
if (nextEnabled && !arSettings.hasSavedPaymentMethod && cards.length === 0) {
|
||||
setArError('Add a payment card before enabling auto-recharge.');
|
||||
return;
|
||||
}
|
||||
|
||||
setArSaving(true);
|
||||
setArError(null);
|
||||
try {
|
||||
const updated = await creditsApi.updateAutoRecharge({ enabled: nextEnabled });
|
||||
setArSettings(updated);
|
||||
} catch (err) {
|
||||
const msg =
|
||||
err && typeof err === 'object' && 'error' in err
|
||||
? String((err as { error: unknown }).error)
|
||||
: 'Failed to update auto-recharge.';
|
||||
setArError(msg);
|
||||
} finally {
|
||||
setArSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleArSave = async () => {
|
||||
if (!arSettings || arSaving) return;
|
||||
setArSaving(true);
|
||||
setArError(null);
|
||||
try {
|
||||
const updated = await creditsApi.updateAutoRecharge({
|
||||
thresholdUsd: arThreshold,
|
||||
rechargeAmountUsd: arAmount,
|
||||
weeklyLimitUsd: arWeeklyLimit,
|
||||
});
|
||||
setArSettings(updated);
|
||||
setArDirty(false);
|
||||
} catch (err) {
|
||||
const msg =
|
||||
err && typeof err === 'object' && 'error' in err
|
||||
? String((err as { error: unknown }).error)
|
||||
: 'Failed to save settings.';
|
||||
setArError(msg);
|
||||
} finally {
|
||||
setArSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Card handlers ────────────────────────────────────────────────────────
|
||||
const handleSetDefault = async (paymentMethodId: string) => {
|
||||
if (settingDefaultId) return;
|
||||
setSettingDefaultId(paymentMethodId);
|
||||
try {
|
||||
const updated = await creditsApi.updateCard(paymentMethodId, { isDefault: true });
|
||||
setCards(updated.cards);
|
||||
} catch {
|
||||
setArError('Failed to update default card.');
|
||||
} finally {
|
||||
setSettingDefaultId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCard = async (paymentMethodId: string) => {
|
||||
if (deletingCardId) return;
|
||||
setDeletingCardId(paymentMethodId);
|
||||
setConfirmDeleteId(null);
|
||||
try {
|
||||
const updated = await creditsApi.deleteCard(paymentMethodId);
|
||||
setCards(updated.cards);
|
||||
// Refresh auto-recharge settings (hasSavedPaymentMethod may change)
|
||||
const refreshed = await creditsApi.getAutoRecharge();
|
||||
setArSettings(refreshed);
|
||||
} catch {
|
||||
setArError('Failed to remove card.');
|
||||
} finally {
|
||||
setDeletingCardId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCard = async () => {
|
||||
// Card setup requires Stripe.js confirmation.
|
||||
// Use the Stripe Customer Portal as the secure entry point.
|
||||
try {
|
||||
const { portalUrl } = await billingApi.createPortalSession();
|
||||
await openUrl(portalUrl);
|
||||
} catch {
|
||||
setArError('Could not open payment portal. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle payment:success deep link event
|
||||
useEffect(() => {
|
||||
const onPaymentSuccess = async () => {
|
||||
@@ -102,7 +293,6 @@ const BillingPanel = () => {
|
||||
currentTierRef.current = currentTier;
|
||||
}, [currentTier]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
||||
const startPolling = useCallback(() => {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
pollStartRef.current = Date.now();
|
||||
@@ -162,6 +352,18 @@ const BillingPanel = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleTopUp = async (amountUsd: number) => {
|
||||
setIsToppingUp(true);
|
||||
try {
|
||||
const result = await creditsApi.topUp(amountUsd, 'stripe');
|
||||
await openUrl(result.url);
|
||||
} catch (err) {
|
||||
console.error('Top-up failed:', err);
|
||||
} finally {
|
||||
setIsToppingUp(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ── JSX ─────────────────────────────────────────────────────────────
|
||||
return (
|
||||
<div className="overflow-hidden flex flex-col">
|
||||
@@ -174,40 +376,20 @@ const BillingPanel = () => {
|
||||
{/* <div className="flex items-center justify-between max-w-md mx-auto"> */}
|
||||
<div className="overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
<div className="max-w-md mt-4 mx-auto">
|
||||
<div className="p-2.5">
|
||||
<div className="max-w-md mt-4 mx-auto px-4 space-y-3">
|
||||
{/* ── Current Plan Header ───────────────────────────────── */}
|
||||
<div className="rounded-2xl border border-stone-700/50 bg-stone-800/40 p-3">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
Your Current Plan {currentTier}
|
||||
</h3>
|
||||
{usage && (
|
||||
<span className="text-xs text-stone-400">
|
||||
{Math.round((usage.spentThisCycleUsd / usage.cycleBudgetUsd) * 100)}% used
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasActive && (
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
{planExpiry && (
|
||||
<p className="text-xs text-stone-400">
|
||||
Renews{' '}
|
||||
{new Date(planExpiry).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
<h3 className="text-sm font-semibold text-white">Current Plan — {currentTier}</h3>
|
||||
{hasActive && (
|
||||
<button
|
||||
onClick={handleManageSubscription}
|
||||
className="text-xs text-primary-400 hover:text-primary-300 font-medium transition-colors">
|
||||
Manage Subscription
|
||||
Manage
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Renewal date (for non-active subscriptions) */}
|
||||
{!hasActive && planExpiry && (
|
||||
)}
|
||||
</div>
|
||||
{planExpiry && (
|
||||
<p className="text-xs text-stone-400 mb-1.5">
|
||||
Renews{' '}
|
||||
{new Date(planExpiry).toLocaleDateString('en-US', {
|
||||
@@ -217,20 +399,131 @@ const BillingPanel = () => {
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{usage && (
|
||||
<div className="h-1.5 bg-stone-700/60 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300 bg-primary-500"
|
||||
style={{
|
||||
width: `${Math.min(
|
||||
100,
|
||||
(usage.spentThisCycleUsd / usage.cycleBudgetUsd) * 100
|
||||
)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Inference Budget (Team Usage) ─────────────────────── */}
|
||||
<div className="rounded-2xl border border-stone-700/50 bg-stone-800/40 p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-sm font-semibold text-white">Inference Budget</h3>
|
||||
{isLoadingCredits && <span className="text-[10px] text-stone-500">Loading…</span>}
|
||||
{teamUsage && !isLoadingCredits && (
|
||||
<span className="text-xs text-stone-400">
|
||||
${teamUsage.remainingUsd.toFixed(2)} / ${teamUsage.cycleBudgetUsd.toFixed(2)}{' '}
|
||||
remaining
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{teamUsage ? (
|
||||
<>
|
||||
<div className="h-1.5 bg-stone-700/60 rounded-full overflow-hidden mb-2">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 ${
|
||||
teamUsage.remainingUsd <= 0
|
||||
? 'bg-coral-500'
|
||||
: teamUsage.remainingUsd / teamUsage.cycleBudgetUsd < 0.2
|
||||
? 'bg-amber-500'
|
||||
: 'bg-primary-500'
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min(100, (teamUsage.remainingUsd / teamUsage.cycleBudgetUsd) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] text-stone-500">
|
||||
Daily usage: ${teamUsage.dailyUsage.toFixed(3)}
|
||||
</span>
|
||||
<span className="text-[11px] text-stone-500">
|
||||
{(
|
||||
(teamUsage.totalInputTokensThisCycle +
|
||||
teamUsage.totalOutputTokensThisCycle) /
|
||||
1000
|
||||
).toFixed(1)}
|
||||
k tokens this cycle
|
||||
</span>
|
||||
</div>
|
||||
{teamUsage.remainingUsd <= 0 && (
|
||||
<p className="text-[11px] text-coral-400 mt-1.5">
|
||||
Budget exhausted — top up your credits to continue using AI features.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : isLoadingCredits ? (
|
||||
<div className="h-1.5 w-full rounded-full bg-stone-700/60 animate-pulse" />
|
||||
) : (
|
||||
<p className="text-xs text-stone-500">Unable to load usage data</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Credits Balance & Top-up ──────────────────────────── */}
|
||||
<div className="rounded-2xl border border-stone-700/50 bg-stone-800/40 p-3">
|
||||
<h3 className="text-sm font-semibold text-white mb-2">Credits Balance</h3>
|
||||
{creditBalance ? (
|
||||
<div className="space-y-1.5 mb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-stone-400">General credits</span>
|
||||
<span className="text-xs font-medium text-white">
|
||||
${creditBalance.balanceUsd.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-stone-400">Top-up credits</span>
|
||||
<span className="text-xs font-medium text-white">
|
||||
${creditBalance.topUpBalanceUsd.toFixed(2)}
|
||||
{creditBalance.topUpBaselineUsd != null &&
|
||||
creditBalance.topUpBaselineUsd > 0 && (
|
||||
<span className="text-stone-500 font-normal">
|
||||
{' '}
|
||||
/ ${creditBalance.topUpBaselineUsd.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{creditBalance.topUpBaselineUsd != null &&
|
||||
creditBalance.topUpBaselineUsd > 0 && (
|
||||
<div className="h-1 bg-stone-700/60 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 ${
|
||||
creditBalance.topUpBalanceUsd <= 0
|
||||
? 'bg-coral-500'
|
||||
: creditBalance.topUpBalanceUsd / creditBalance.topUpBaselineUsd <
|
||||
0.2
|
||||
? 'bg-amber-500'
|
||||
: 'bg-primary-500'
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min(
|
||||
100,
|
||||
(creditBalance.topUpBalanceUsd / creditBalance.topUpBaselineUsd) *
|
||||
100
|
||||
)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : isLoadingCredits ? (
|
||||
<div className="space-y-1.5 mb-3">
|
||||
<div className="h-3 w-full rounded bg-stone-700/60 animate-pulse" />
|
||||
<div className="h-3 w-3/4 rounded bg-stone-700/60 animate-pulse" />
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-stone-500 mb-3">Unable to load balance</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
{[5, 10, 25].map(amount => (
|
||||
<button
|
||||
key={amount}
|
||||
onClick={() => handleTopUp(amount)}
|
||||
disabled={isToppingUp}
|
||||
className="flex-1 py-1.5 rounded-lg bg-primary-500/20 hover:bg-primary-500/30 text-primary-400 text-xs font-medium border border-primary-500/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{isToppingUp ? '…' : `+$${amount}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Interval toggle ──────────────────────────────────── */}
|
||||
@@ -407,6 +700,340 @@ const BillingPanel = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Auto-Recharge Credits ─────────────────────────────── */}
|
||||
<div className="px-4 pt-2">
|
||||
<div className="rounded-2xl border border-stone-700/50 bg-stone-800/40 overflow-hidden">
|
||||
{/* Header row */}
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-white">Auto-Recharge Credits</p>
|
||||
<p className="text-[11px] text-stone-400 mt-0.5">
|
||||
Automatically top up when your balance runs low
|
||||
</p>
|
||||
</div>
|
||||
{arLoading ? (
|
||||
<div className="w-10 h-5 rounded-full bg-stone-700/60 animate-pulse" />
|
||||
) : (
|
||||
<button
|
||||
onClick={handleArToggle}
|
||||
disabled={arSaving}
|
||||
role="switch"
|
||||
aria-checked={arSettings?.enabled ?? false}
|
||||
aria-label="Toggle auto-recharge"
|
||||
className={`relative w-10 h-5 rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 focus-visible:ring-offset-stone-900 ${
|
||||
arSaving ? 'opacity-50 cursor-not-allowed' : ''
|
||||
} ${arSettings?.enabled ? 'bg-primary-500' : 'bg-stone-600'}`}>
|
||||
<span
|
||||
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${
|
||||
arSettings?.enabled ? 'translate-x-5' : 'translate-x-0'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Error banner */}
|
||||
{arError && (
|
||||
<div className="mx-3 mb-2 flex items-start gap-2 rounded-lg bg-coral-500/10 border border-coral-500/20 px-2.5 py-2">
|
||||
<svg
|
||||
className="w-3.5 h-3.5 text-coral-400 flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-[11px] text-coral-300 leading-relaxed">{arError}</p>
|
||||
<button
|
||||
onClick={() => setArError(null)}
|
||||
className="ml-auto text-coral-400 hover:text-coral-300 flex-shrink-0"
|
||||
aria-label="Dismiss error">
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings — only shown when enabled */}
|
||||
{!arLoading && arSettings?.enabled && (
|
||||
<div className="border-t border-stone-700/50 px-3 pt-3 pb-2 space-y-3">
|
||||
{/* Status row */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{arSettings.inFlight && (
|
||||
<span className="flex items-center gap-1 text-[10px] text-amber-300 bg-amber-500/10 border border-amber-500/20 rounded-full px-2 py-0.5">
|
||||
<svg className="w-2.5 h-2.5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||
/>
|
||||
</svg>
|
||||
Recharge in progress
|
||||
</span>
|
||||
)}
|
||||
{arSettings.spentThisWeekUsd > 0 && (
|
||||
<span className="text-[10px] text-stone-400">
|
||||
${arSettings.spentThisWeekUsd.toFixed(2)} of ${arSettings.weeklyLimitUsd}{' '}
|
||||
used this week
|
||||
</span>
|
||||
)}
|
||||
{arSettings.lastRechargeAt && (
|
||||
<span className="text-[10px] text-stone-500">
|
||||
Last recharged{' '}
|
||||
{new Date(arSettings.lastRechargeAt).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Last error from recharge attempt */}
|
||||
{arSettings.lastError && (
|
||||
<div className="flex items-start gap-1.5 rounded-lg bg-coral-500/10 border border-coral-500/20 px-2.5 py-2">
|
||||
<svg
|
||||
className="w-3 h-3 text-coral-400 flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-[10px] text-coral-300">
|
||||
Last recharge failed: {arSettings.lastError}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trigger threshold */}
|
||||
<div>
|
||||
<p className="text-[11px] text-stone-400 mb-1.5">
|
||||
Recharge when balance drops below
|
||||
</p>
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{THRESHOLD_OPTIONS.map(v => (
|
||||
<button
|
||||
key={v}
|
||||
onClick={() => setArThreshold(v)}
|
||||
className={`px-2.5 py-1 text-xs rounded-lg border transition-colors ${
|
||||
arThreshold === v
|
||||
? 'bg-primary-500/20 text-primary-400 border-primary-500/40'
|
||||
: 'bg-stone-700/40 text-stone-400 border-stone-600/40 hover:text-stone-300'
|
||||
}`}>
|
||||
${v}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recharge amount */}
|
||||
<div>
|
||||
<p className="text-[11px] text-stone-400 mb-1.5">Add this amount</p>
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{RECHARGE_OPTIONS.map(v => (
|
||||
<button
|
||||
key={v}
|
||||
onClick={() => setArAmount(v)}
|
||||
className={`px-2.5 py-1 text-xs rounded-lg border transition-colors ${
|
||||
arAmount === v
|
||||
? 'bg-primary-500/20 text-primary-400 border-primary-500/40'
|
||||
: 'bg-stone-700/40 text-stone-400 border-stone-600/40 hover:text-stone-300'
|
||||
}`}>
|
||||
${v}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Weekly limit */}
|
||||
<div>
|
||||
<p className="text-[11px] text-stone-400 mb-1.5">Weekly spending limit</p>
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{WEEKLY_LIMIT_OPTIONS.map(v => (
|
||||
<button
|
||||
key={v}
|
||||
onClick={() => setArWeeklyLimit(v)}
|
||||
className={`px-2.5 py-1 text-xs rounded-lg border transition-colors ${
|
||||
arWeeklyLimit === v
|
||||
? 'bg-primary-500/20 text-primary-400 border-primary-500/40'
|
||||
: 'bg-stone-700/40 text-stone-400 border-stone-600/40 hover:text-stone-300'
|
||||
}`}>
|
||||
${v}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation hint */}
|
||||
{arAmount <= arThreshold && (
|
||||
<p className="text-[10px] text-amber-400">
|
||||
Recharge amount should be greater than the trigger threshold.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Save button */}
|
||||
{arDirty && (
|
||||
<button
|
||||
onClick={handleArSave}
|
||||
disabled={arSaving || arAmount <= arThreshold}
|
||||
className={`w-full py-1.5 text-xs font-medium rounded-lg transition-colors ${
|
||||
arSaving || arAmount <= arThreshold
|
||||
? 'bg-stone-700/40 text-stone-500 cursor-not-allowed'
|
||||
: 'bg-primary-500 hover:bg-primary-600 text-white'
|
||||
}`}>
|
||||
{arSaving ? 'Saving…' : 'Save Settings'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment methods */}
|
||||
<div className="border-t border-stone-700/50 px-3 py-2.5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-[11px] font-medium text-stone-300">Payment Methods</p>
|
||||
<button
|
||||
onClick={handleAddCard}
|
||||
className="text-[11px] text-primary-400 hover:text-primary-300 font-medium transition-colors">
|
||||
+ Add card
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{cardsLoading ? (
|
||||
<div className="space-y-1.5">
|
||||
{[0, 1].map(i => (
|
||||
<div key={i} className="h-9 rounded-lg bg-stone-700/30 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : cards.length === 0 ? (
|
||||
<div className="flex items-center gap-2 rounded-lg bg-stone-700/20 border border-stone-700/30 p-2.5">
|
||||
<svg
|
||||
className="w-4 h-4 text-stone-500 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-[11px] text-stone-500">
|
||||
No saved cards. Add one to enable auto-recharge.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{cards.map(card => {
|
||||
const isDeleting = deletingCardId === card.id;
|
||||
const isSettingDefault = settingDefaultId === card.id;
|
||||
const isConfirming = confirmDeleteId === card.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={card.id}
|
||||
className="flex items-center gap-2 rounded-lg bg-stone-700/20 border border-stone-700/30 px-2.5 py-2">
|
||||
{/* Card icon */}
|
||||
<svg
|
||||
className="w-4 h-4 text-stone-400 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Card info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-xs text-white font-medium">
|
||||
{cardBrandLabel(card.brand)} ••••{card.last4}
|
||||
</span>
|
||||
{card.isDefault && (
|
||||
<span className="text-[9px] px-1.5 py-0.5 rounded-full bg-primary-500/20 text-primary-400 border border-primary-500/30 font-medium">
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-stone-500 mt-0.5">
|
||||
Expires {String(card.expMonth).padStart(2, '0')}/
|
||||
{String(card.expYear).slice(-2)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{!card.isDefault && (
|
||||
<button
|
||||
onClick={() => handleSetDefault(card.id)}
|
||||
disabled={!!settingDefaultId || !!deletingCardId}
|
||||
className="text-[10px] text-stone-400 hover:text-stone-300 transition-colors disabled:opacity-40 disabled:cursor-not-allowed px-1.5 py-1">
|
||||
{isSettingDefault ? '…' : 'Set default'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isConfirming ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => handleDeleteCard(card.id)}
|
||||
disabled={isDeleting}
|
||||
className="text-[10px] text-coral-400 hover:text-coral-300 font-medium transition-colors disabled:opacity-40 px-1.5 py-1">
|
||||
{isDeleting ? '…' : 'Confirm'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDeleteId(null)}
|
||||
className="text-[10px] text-stone-500 hover:text-stone-400 transition-colors px-1 py-1">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setConfirmDeleteId(card.id)}
|
||||
disabled={isDeleting || !!settingDefaultId}
|
||||
className="text-[10px] text-stone-500 hover:text-coral-400 transition-colors disabled:opacity-40 disabled:cursor-not-allowed px-1.5 py-1">
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Upgrade benefits ───────────────────────────────────── */}
|
||||
<div className="px-4 pb-4 pt-2">
|
||||
<div className="rounded-xl bg-gradient-to-br from-primary-500/10 to-sage-500/10 border border-primary-500/20 p-4">
|
||||
|
||||
+190
-16
@@ -1,25 +1,104 @@
|
||||
import type { ApiResponse } from '../../types/api';
|
||||
import { apiClient } from '../apiClient';
|
||||
|
||||
interface CreditBalance {
|
||||
balance: number;
|
||||
currency: string;
|
||||
export interface CreditBalance {
|
||||
balanceUsd: number;
|
||||
topUpBalanceUsd: number;
|
||||
topUpBaselineUsd: number | null;
|
||||
}
|
||||
|
||||
interface CreditTransaction {
|
||||
export interface TeamUsage {
|
||||
remainingUsd: number;
|
||||
cycleBudgetUsd: number;
|
||||
dailyUsage: number;
|
||||
totalInputTokensThisCycle: number;
|
||||
totalOutputTokensThisCycle: number;
|
||||
}
|
||||
|
||||
export interface TopUpResult {
|
||||
url: string;
|
||||
gatewayTransactionId: string;
|
||||
amountUsd: number;
|
||||
gateway: string;
|
||||
}
|
||||
|
||||
export interface CreditTransaction {
|
||||
id: string;
|
||||
amount: number;
|
||||
type: 'credit' | 'debit';
|
||||
description: string;
|
||||
type: 'EARN' | 'SPEND';
|
||||
action: string;
|
||||
amountUsd: number;
|
||||
balanceAfterUsd: number;
|
||||
createdAt: string;
|
||||
// Add other fields based on backend schema
|
||||
}
|
||||
|
||||
interface PaginatedTransactions {
|
||||
export interface PaginatedTransactions {
|
||||
transactions: CreditTransaction[];
|
||||
totalCount: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ── Auto-Recharge types ──────────────────────────────────────────────────────
|
||||
|
||||
export interface AutoRechargeSettings {
|
||||
enabled: boolean;
|
||||
thresholdUsd: number;
|
||||
rechargeAmountUsd: number;
|
||||
weeklyLimitUsd: number;
|
||||
spentThisWeekUsd: number;
|
||||
weekStartDate: string;
|
||||
inFlight: boolean;
|
||||
hasSavedPaymentMethod: boolean;
|
||||
lastTriggeredAt: string | null;
|
||||
lastRechargeAt: string | null;
|
||||
lastPaymentIntentId: string | null;
|
||||
lastError: string | null;
|
||||
}
|
||||
|
||||
export interface AutoRechargeUpdatePayload {
|
||||
enabled?: boolean;
|
||||
thresholdUsd?: number;
|
||||
rechargeAmountUsd?: number;
|
||||
weeklyLimitUsd?: number;
|
||||
}
|
||||
|
||||
export interface BillingAddress {
|
||||
line1?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
postalCode?: string;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export interface CardBillingDetails {
|
||||
name?: string;
|
||||
email?: string;
|
||||
address?: BillingAddress;
|
||||
}
|
||||
|
||||
export interface SavedCard {
|
||||
id: string;
|
||||
brand: string;
|
||||
expMonth: number;
|
||||
expYear: number;
|
||||
isDefault: boolean;
|
||||
last4: string;
|
||||
billingDetails: CardBillingDetails;
|
||||
}
|
||||
|
||||
export interface CardsData {
|
||||
customerId: string;
|
||||
defaultPaymentMethodId: string;
|
||||
cards: SavedCard[];
|
||||
}
|
||||
|
||||
export interface SetupIntentData {
|
||||
clientSecret: string;
|
||||
customerId: string;
|
||||
setupIntentId: string;
|
||||
}
|
||||
|
||||
export interface UpdateCardPayload {
|
||||
isDefault?: boolean;
|
||||
billingDetails?: CardBillingDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,11 +106,35 @@ interface PaginatedTransactions {
|
||||
*/
|
||||
export const creditsApi = {
|
||||
/**
|
||||
* Get the current user's credit balance
|
||||
* Get the current user's credit balance (general + top-up)
|
||||
* GET /credits/balance
|
||||
*/
|
||||
getBalance: async (): Promise<CreditBalance> => {
|
||||
const response = await apiClient.get<ApiResponse<CreditBalance>>('/credits/balance');
|
||||
const response = await apiClient.get<ApiResponse<CreditBalance>>('/payments/credits/balance');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get team inference budget usage for the current billing cycle
|
||||
* GET /teams/me/usage
|
||||
*/
|
||||
getTeamUsage: async (): Promise<TeamUsage> => {
|
||||
const response = await apiClient.get<ApiResponse<TeamUsage>>('/teams/me/usage');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Start a top-up (get Stripe or Coinbase payment URL)
|
||||
* POST /credits/top-up
|
||||
*/
|
||||
topUp: async (
|
||||
amountUsd: number,
|
||||
gateway: 'stripe' | 'coinbase' = 'stripe'
|
||||
): Promise<TopUpResult> => {
|
||||
const response = await apiClient.post<ApiResponse<TopUpResult>>('/payments/credits/top-up', {
|
||||
amountUsd,
|
||||
gateway,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -39,9 +142,80 @@ export const creditsApi = {
|
||||
* Get paginated credit transaction history
|
||||
* GET /credits/transactions
|
||||
*/
|
||||
getTransactions: async (page = 1, limit = 50): Promise<PaginatedTransactions> => {
|
||||
getTransactions: async (limit = 20, offset = 0): Promise<PaginatedTransactions> => {
|
||||
const response = await apiClient.get<ApiResponse<PaginatedTransactions>>(
|
||||
`/credits/transactions?page=${page}&limit=${limit}`
|
||||
`/credits/transactions?limit=${limit}&offset=${offset}`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ── Auto-Recharge ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get auto-recharge settings
|
||||
* GET /payments/credits/auto-recharge
|
||||
*/
|
||||
getAutoRecharge: async (): Promise<AutoRechargeSettings> => {
|
||||
const response = await apiClient.get<ApiResponse<AutoRechargeSettings>>(
|
||||
'/payments/credits/auto-recharge'
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update auto-recharge settings. Enabling requires a saved card.
|
||||
* PATCH /payments/credits/auto-recharge
|
||||
*/
|
||||
updateAutoRecharge: async (payload: AutoRechargeUpdatePayload): Promise<AutoRechargeSettings> => {
|
||||
const response = await apiClient.patch<ApiResponse<AutoRechargeSettings>>(
|
||||
'/payments/credits/auto-recharge',
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* List saved cards for auto-recharge
|
||||
* GET /payments/credits/auto-recharge/cards
|
||||
*/
|
||||
getCards: async (): Promise<CardsData> => {
|
||||
const response = await apiClient.get<ApiResponse<CardsData>>(
|
||||
'/payments/credits/auto-recharge/cards'
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a Stripe SetupIntent for adding a new card.
|
||||
* The returned clientSecret must be confirmed with Stripe.js.
|
||||
* POST /payments/credits/auto-recharge/cards/setup-intent
|
||||
*/
|
||||
createSetupIntent: async (): Promise<SetupIntentData> => {
|
||||
const response = await apiClient.post<ApiResponse<SetupIntentData>>(
|
||||
'/payments/credits/auto-recharge/cards/setup-intent'
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a saved card (set as default or update billing details)
|
||||
* PATCH /payments/credits/auto-recharge/cards/:paymentMethodId
|
||||
*/
|
||||
updateCard: async (paymentMethodId: string, payload: UpdateCardPayload): Promise<CardsData> => {
|
||||
const response = await apiClient.patch<ApiResponse<CardsData>>(
|
||||
`/payments/credits/auto-recharge/cards/${encodeURIComponent(paymentMethodId)}`,
|
||||
payload
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a saved card. If it was the default, another card becomes default.
|
||||
* DELETE /payments/credits/auto-recharge/cards/:paymentMethodId
|
||||
*/
|
||||
deleteCard: async (paymentMethodId: string): Promise<CardsData> => {
|
||||
const response = await apiClient.delete<ApiResponse<CardsData>>(
|
||||
`/payments/credits/auto-recharge/cards/${encodeURIComponent(paymentMethodId)}`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -654,7 +654,7 @@
|
||||
|
||||
"@heroicons/react@^2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.2.0.tgz#0c05124af50434a800773abec8d3af6a297d904b"
|
||||
integrity sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==
|
||||
|
||||
"@humanfs/core@^0.19.1":
|
||||
@@ -928,27 +928,22 @@
|
||||
outvariant "^1.4.3"
|
||||
strict-event-emitter "^0.5.1"
|
||||
|
||||
"@noble/curves@~1.9.0":
|
||||
version "1.9.7"
|
||||
resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz"
|
||||
integrity sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==
|
||||
"@noble/curves@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-2.0.1.tgz#64ba8bd5e8564a02942655602515646df1cdb3ad"
|
||||
integrity sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==
|
||||
dependencies:
|
||||
"@noble/hashes" "1.8.0"
|
||||
|
||||
"@noble/hashes@1.8.0", "@noble/hashes@~1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz"
|
||||
integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==
|
||||
"@noble/hashes" "2.0.1"
|
||||
|
||||
"@noble/hashes@2.0.1", "@noble/hashes@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.0.1.tgz#fc1a928061d1232b0a52bb754393c37a5216c89e"
|
||||
integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==
|
||||
|
||||
"@noble/secp256k1@^2.0.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.3.0.tgz"
|
||||
integrity sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw==
|
||||
"@noble/secp256k1@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-3.0.0.tgz#29711361db8f37b1b7e0b8d80c933013fc887475"
|
||||
integrity sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -1181,26 +1176,21 @@
|
||||
|
||||
"@scure/base@2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-2.0.0.tgz#ba6371fddf92c2727e88ad6ab485db6e624f9a98"
|
||||
integrity sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==
|
||||
|
||||
"@scure/base@~1.2.5":
|
||||
version "1.2.6"
|
||||
resolved "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz"
|
||||
integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==
|
||||
|
||||
"@scure/bip32@^1.5.1":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz"
|
||||
integrity sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==
|
||||
"@scure/bip32@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-2.0.1.tgz#4ceea207cee8626d3fe8f0b6ab68b6af8f81c482"
|
||||
integrity sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==
|
||||
dependencies:
|
||||
"@noble/curves" "~1.9.0"
|
||||
"@noble/hashes" "~1.8.0"
|
||||
"@scure/base" "~1.2.5"
|
||||
"@noble/curves" "2.0.1"
|
||||
"@noble/hashes" "2.0.1"
|
||||
"@scure/base" "2.0.0"
|
||||
|
||||
"@scure/bip39@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-2.0.1.tgz#47a6dc15e04faf200041239d46ae3bb7c3c96add"
|
||||
integrity sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==
|
||||
dependencies:
|
||||
"@noble/hashes" "2.0.1"
|
||||
@@ -7405,16 +7395,7 @@ strict-event-emitter@^0.5.1:
|
||||
resolved "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz"
|
||||
integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -7513,14 +7494,8 @@ stringify-entities@^4.0.0:
|
||||
character-entities-html4 "^2.0.0"
|
||||
character-entities-legacy "^3.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
name strip-ansi-cjs
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -8365,7 +8340,8 @@ workerpool@^6.5.1:
|
||||
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz"
|
||||
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
name wrap-ansi-cjs
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -8383,15 +8359,6 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user