···
last_used_at: number | null;
+
interface Subscription {
+
current_period_start: number | null;
+
current_period_end: number | null;
+
cancel_at_period_end: number;
+
canceled_at: number | null;
type SettingsPage = "account" | "sessions" | "passkeys" | "billing" | "danger";
@customElement("user-settings")
···
@state() user: User | null = null;
@state() sessions: Session[] = [];
@state() passkeys: Passkey[] = [];
+
@state() subscription: Subscription | null = null;
@state() loadingSessions = true;
@state() loadingPasskeys = true;
+
@state() loadingSubscription = true;
@state() showDeleteConfirm = false;
@state() currentPage: SettingsPage = "account";
···
+
background: var(--primary);
+
border-color: var(--primary);
+
.btn-affirmative:hover:not(:disabled) {
+
background: transparent;
+
background: var(--success);
+
border-color: var(--success);
+
.btn-success:hover:not(:disabled) {
+
background: transparent;
···
this.passkeySupported = isPasskeySupported();
await this.loadSessions();
+
await this.loadSubscription();
if (this.passkeySupported) {
await this.loadPasskeys();
···
+
async loadSubscription() {
+
const response = await fetch("/api/billing/subscription");
+
const data = await response.json();
+
this.subscription = data.subscription;
+
this.loadingSubscription = false;
async handleAddPasskey() {
this.addingPasskey = true;
···
const { url } = await response.json();
+
window.open(url, "_blank");
this.error = "Failed to create checkout session";
···
+
async handleOpenPortal() {
+
const response = await fetch("/api/billing/portal", {
+
headers: { "Content-Type": "application/json" },
+
const data = await response.json();
+
this.error = data.error || "Failed to open customer portal";
+
const { url } = await response.json();
+
window.open(url, "_blank");
+
this.error = "Failed to open customer portal";
// Generate a random string for the avatar
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
···
+
if (this.loadingSubscription) {
+
<div class="content-inner">
+
<div class="loading">Loading subscription...</div>
+
const hasActiveSubscription = this.subscription && (
+
this.subscription.status === "active" ||
+
this.subscription.status === "trialing"
+
if (this.subscription && !hasActiveSubscription) {
+
// Has a subscription but it's not active (canceled, expired, etc.)
+
this.subscription.status === "canceled" ? "var(--accent)" :
+
<div class="content-inner">
+
<h2 class="section-title">Subscription</h2>
+
<div class="field-group">
+
<label class="field-label">Status</label>
+
<div style="display: flex; align-items: center; gap: 0.75rem;">
+
padding: 0.25rem 0.75rem;
+
background: ${statusColor};
+
text-transform: uppercase;
+
${this.subscription.status}
+
${this.subscription.canceled_at ? html`
+
<div class="field-group">
+
<label class="field-label">Canceled At</label>
+
<div class="field-value" style="color: var(--accent);">
+
${this.formatDate(this.subscription.canceled_at)}
+
<div class="field-group" style="margin-top: 2rem;">
+
class="btn btn-success"
+
@click=${this.handleCreateCheckout}
+
?disabled=${this.loading}
+
${this.loading ? "Loading..." : "Activate Your Subscription"}
+
<p class="field-description" style="margin-top: 0.75rem;">
+
Reactivate your subscription to unlock unlimited transcriptions.
+
${this.error ? html`<p class="error" style="margin-top: 1rem;">${this.error}</p>` : ""}
+
if (hasActiveSubscription) {
+
<div class="content-inner">
+
<h2 class="section-title">Subscription</h2>
+
<div class="field-group">
+
<label class="field-label">Status</label>
+
<div style="display: flex; align-items: center; gap: 0.75rem;">
+
padding: 0.25rem 0.75rem;
+
background: var(--success);
+
text-transform: uppercase;
+
${this.subscription.status}
+
${this.subscription.cancel_at_period_end ? html`
+
<span style="color: var(--accent); font-size: 0.875rem;">
+
(Cancels at end of period)
+
${this.subscription.current_period_start && this.subscription.current_period_end ? html`
+
<div class="field-group">
+
<label class="field-label">Current Period</label>
+
<div class="field-value">
+
${this.formatDate(this.subscription.current_period_start)} -
+
${this.formatDate(this.subscription.current_period_end)}
+
<div class="field-group" style="margin-top: 2rem;">
+
class="btn btn-affirmative"
+
@click=${this.handleOpenPortal}
+
?disabled=${this.loading}
+
${this.loading ? "Loading..." : "Manage Subscription"}
+
<p class="field-description" style="margin-top: 0.75rem;">
+
Opens the customer portal where you can update payment methods, view invoices, and manage your subscription.
+
${this.error ? html`<p class="error" style="margin-top: 1rem;">${this.error}</p>` : ""}
<div class="content-inner">
<h2 class="section-title">Billing & Subscription</h2>
<p class="field-description" style="margin-bottom: 1.5rem;">
+
Activate your subscription to unlock unlimited transcriptions. Note: We currently offer a single subscription tier.
+
class="btn btn-success"
@click=${this.handleCreateCheckout}
?disabled=${this.loading}
+
${this.loading ? "Loading..." : "Activate Your Subscription"}
${this.error ? html`<p class="error" style="margin-top: 1rem;">${this.error}</p>` : ""}