Two launch-day cleanups:
**Footer rebrand** — full rewrite of modules/marketing/layout/footer.tsx
from TurboStarter boilerplate (Twitter/Facebook/LinkedIn socials,
Chrome/Firefox/Edge extension links, turbostarter repo links, broken
/legal routes) to lean claudemesh structure:
- claudemesh wordmark (mesh glyph + serif) + tagline
- 2 columns: Product (Docs / Pricing / Changelog / Contact) +
Protocol (GitHub / claude-intercom OSS / Protocol spec / Self-host
broker)
- GitHub social icon linking to github.com/alezmad/claudemesh
- I18n controls
- Bottom bar: "© 2026 claudemesh · MIT licensed" + the existing
BuiltWith credit pointing at claude-intercom (from cdd7931)
No trash links. No turbostarter refs. Matches landing design tokens
(--cm-*).
**Manage-plan CTA guard** — settings/billing → ManagePlan previously
always rendered an active "Visit billing portal" button that would
500 on launch day because Stripe isn't set up. For FREE-tier users
(everyone at v0.1.0) the button is now disabled + labelled
"Paid tiers coming soon". When someone is on a paid tier (future)
the real portal flow re-engages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
88 lines
2.6 KiB
TypeScript
88 lines
2.6 KiB
TypeScript
"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>
|
|
{plan.id === PricingPlanType.FREE ? (
|
|
// v0.1.0: only the free tier is live. Paid-tier checkout +
|
|
// Stripe customer portal land post-launch; surface that
|
|
// honestly instead of a button that would hit a 500.
|
|
<div className="flex items-center gap-2">
|
|
<Button className="w-fit gap-1" disabled>
|
|
{t("manage.billing.visitPortal")}
|
|
<Icons.ArrowUpRight className="size-4" />
|
|
</Button>
|
|
<span className="text-muted-foreground text-xs">
|
|
Paid tiers coming soon
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<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>
|
|
);
|
|
};
|