feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
72
apps/web/src/modules/user/settings/billing/manage-plan.tsx
Normal file
72
apps/web/src/modules/user/settings/billing/manage-plan.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { config, PricingPlanType } from "@turbostarter/billing";
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { Button } from "@turbostarter/ui-web/button";
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
|
||||
import { useCustomer } from "~/modules/billing/hooks/use-customer";
|
||||
import { billing } from "~/modules/billing/lib/api";
|
||||
import {
|
||||
SettingsCard,
|
||||
SettingsCardContent,
|
||||
SettingsCardDescription,
|
||||
SettingsCardHeader,
|
||||
SettingsCardTitle,
|
||||
} from "~/modules/common/layout/dashboard/settings-card";
|
||||
|
||||
export const ManagePlan = () => {
|
||||
const { t } = useTranslation("billing");
|
||||
const router = useRouter();
|
||||
const { data: customer } = useCustomer();
|
||||
|
||||
const getPortal = useMutation({
|
||||
...billing.mutations.portal.get,
|
||||
onSuccess: ({ url }) => {
|
||||
router.push(url);
|
||||
},
|
||||
});
|
||||
|
||||
const plan = config.plans.find(
|
||||
(plan) => plan.id === (customer?.plan ?? PricingPlanType.FREE),
|
||||
);
|
||||
|
||||
if (!plan) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader>
|
||||
<SettingsCardTitle>{t("manage.billing.title")}</SettingsCardTitle>
|
||||
<SettingsCardDescription>
|
||||
{t("manage.billing.description")}
|
||||
</SettingsCardDescription>
|
||||
</SettingsCardHeader>
|
||||
|
||||
<SettingsCardContent>
|
||||
<Button
|
||||
className="w-fit gap-1"
|
||||
disabled={getPortal.isPending}
|
||||
onClick={() =>
|
||||
getPortal.mutate({
|
||||
query: {
|
||||
redirectUrl: window.location.href,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("manage.billing.visitPortal")}
|
||||
{getPortal.isPending ? (
|
||||
<Icons.Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Icons.ArrowUpRight className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</SettingsCardContent>
|
||||
</SettingsCard>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user