feat: complete release readiness work
- consolidate API ownership and remove dead vendor code - add Stripe billing, learning paths, and public CV hardening - add migration, recovery, security, audit, and browser gates
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Alert, Box, LinearProgress, Paper, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { Alert, Box, Button, LinearProgress, Paper, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { api } from "../api";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
@@ -12,6 +12,8 @@ type Usage = {
|
||||
storageLimitBytes: number;
|
||||
};
|
||||
|
||||
type BillingStatus = { enabled: boolean; canCheckout: boolean; canManage: boolean };
|
||||
|
||||
function formatBytes(bytes: number) {
|
||||
if (bytes < 1_000_000) return `${Math.round(bytes / 1_000)} KB`;
|
||||
if (bytes < 1_000_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;
|
||||
@@ -21,6 +23,9 @@ function formatBytes(bytes: number) {
|
||||
export default function AiUsageCard() {
|
||||
const { t } = useI18n();
|
||||
const [usage, setUsage] = useState<Usage | null>(null);
|
||||
const [billing, setBilling] = useState<BillingStatus | null>(null);
|
||||
const [billingBusy, setBillingBusy] = useState(false);
|
||||
const [billingFailed, setBillingFailed] = useState(false);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -28,9 +33,24 @@ export default function AiUsageCard() {
|
||||
api.get<Usage>("/ai/usage")
|
||||
.then((response) => { if (active) setUsage(response.data); })
|
||||
.catch(() => { if (active) setFailed(true); });
|
||||
api.get<BillingStatus>("/billing/status")
|
||||
.then((response) => { if (active) setBilling(response.data); })
|
||||
.catch(() => { /* Billing is optional; usage remains available. */ });
|
||||
return () => { active = false; };
|
||||
}, []);
|
||||
|
||||
const openBilling = async (path: "checkout" | "portal") => {
|
||||
setBillingBusy(true);
|
||||
setBillingFailed(false);
|
||||
try {
|
||||
const response = await api.post<{ url: string }>(`/billing/${path}`);
|
||||
window.location.assign(response.data.url);
|
||||
} catch {
|
||||
setBillingFailed(true);
|
||||
setBillingBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (failed) return <Alert severity="warning">{t("settingsUsageUnavailable")}</Alert>;
|
||||
if (!usage) return <Skeleton variant="rounded" height={150} />;
|
||||
|
||||
@@ -57,6 +77,9 @@ export default function AiUsageCard() {
|
||||
<LinearProgress variant="determinate" value={storagePercent} aria-label="Attachment storage used" sx={{ mt: 0.75, height: 7, borderRadius: 99 }} />
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 1.5 }}>{t("settingsUsageReset")}</Typography>
|
||||
{billing?.canCheckout ? <Button sx={{ mt: 2 }} variant="contained" disabled={billingBusy} onClick={() => void openBilling("checkout")}>{t("settingsBillingUpgrade")}</Button> : null}
|
||||
{billing?.canManage ? <Button sx={{ mt: 2 }} variant="outlined" disabled={billingBusy} onClick={() => void openBilling("portal")}>{t("settingsBillingManage")}</Button> : null}
|
||||
{billingFailed ? <Alert severity="error" sx={{ mt: 2 }}>{t("settingsBillingUnavailable")}</Alert> : null}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user