From 42b4768b59171a72467bf385ecefab34cd941c12 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 17 Jul 2024 17:13:28 -0700 Subject: [PATCH] feat: Billing UI fast-follows (#1936) ### Changes - Updates customer portal link label - Opens billing portal in the same tab - Shows separate cancel date - Makes payment failed appear as error - Fixes crawl time quota --- frontend/src/pages/org/index.ts | 13 -- .../src/pages/org/payment-portal-redirect.ts | 87 ------------- .../pages/org/settings/components/billing.ts | 115 ++++++++++++------ frontend/src/routes.ts | 1 - 4 files changed, 81 insertions(+), 135 deletions(-) delete mode 100644 frontend/src/pages/org/payment-portal-redirect.ts diff --git a/frontend/src/pages/org/index.ts b/frontend/src/pages/org/index.ts index 1b87e7f8..d6add73f 100644 --- a/frontend/src/pages/org/index.ts +++ b/frontend/src/pages/org/index.ts @@ -38,7 +38,6 @@ import "./browser-profiles-list"; import "./browser-profiles-new"; import "./settings/settings"; import "./dashboard"; -import "./payment-portal-redirect"; const RESOURCE_NAMES = ["workflow", "collection", "browser-profile", "upload"]; type ResourceName = (typeof RESOURCE_NAMES)[number]; @@ -77,7 +76,6 @@ export type OrgParams = { settings: { settingsTab?: "information" | "members"; }; - "payment-portal-redirect": {}; }; export type OrgTab = keyof OrgParams; @@ -310,9 +308,6 @@ export class Org extends LiteElement { } // falls through } - case "payment-portal-redirect": - tabPanelContent = this.renderPaymentPortalRedirect(); - break; default: tabPanelContent = html``; } - private renderPaymentPortalRedirect() { - return html``; - } - private async onSelectNewDialog(e: SelectNewDialogEvent) { e.stopPropagation(); this.isCreateDialogVisible = true; diff --git a/frontend/src/pages/org/payment-portal-redirect.ts b/frontend/src/pages/org/payment-portal-redirect.ts deleted file mode 100644 index 4b69c33d..00000000 --- a/frontend/src/pages/org/payment-portal-redirect.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { localized, msg } from "@lit/localize"; -import { Task } from "@lit/task"; -import { html } from "lit"; -import { customElement, property } from "lit/decorators.js"; - -import { TailwindElement } from "@/classes/TailwindElement"; -import { APIController } from "@/controllers/api"; -import { NavigateController } from "@/controllers/navigate"; -import type { BillingPortal } from "@/types/billing"; -import type { Auth, AuthState } from "@/utils/AuthService"; - -@localized() -@customElement("btrix-org-payment-portal-redirect") -export class OrgPaymentPortalRedirect extends TailwindElement { - @property({ type: Object }) - authState?: AuthState; - - @property({ type: String }) - orgId?: string; - - private readonly api = new APIController(this); - private readonly navigate = new NavigateController(this); - - private readonly portalUrl = new Task(this, { - task: async ([orgId, authState]) => { - if (!orgId || !authState) throw new Error("Missing args"); - try { - const { portalUrl } = await this.getPortalUrl(orgId, authState); - - if (portalUrl) { - window.location.href = portalUrl; - } else { - throw new Error("Missing portalUrl"); - } - } catch (e) { - console.debug(e); - - throw new Error( - msg("Sorry, couldn't retrieve current plan at this time."), - ); - } - }, - args: () => [this.orgId, this.authState] as const, - }); - - render() { - return html`
- ${this.portalUrl.render({ - pending: () => html` - -

- ${msg("Redirecting to billing portal...")} -

- `, - error: () => html` - -

- ${msg("Sorry, the billing portal is unavailable at this time.")} -

- { - if (window.opener) { - window.opener.focus(); - window.close(); - } else { - this.navigate.to( - `${this.navigate.orgBasePath}/settings/billing`, - ); - } - }} - >${msg("Back to Org Settings")} - `, - })} -
`; - } - - private async getPortalUrl(orgId: string, auth: Auth) { - return this.api.fetch(`/orgs/${orgId}/billing-portal`, auth); - } -} diff --git a/frontend/src/pages/org/settings/components/billing.ts b/frontend/src/pages/org/settings/components/billing.ts index e79e570c..96088800 100644 --- a/frontend/src/pages/org/settings/components/billing.ts +++ b/frontend/src/pages/org/settings/components/billing.ts @@ -1,16 +1,18 @@ import { localized, msg, str } from "@lit/localize"; +import { Task } from "@lit/task"; import clsx from "clsx"; import { css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; import { when } from "lit/directives/when.js"; import { columns } from "../ui/columns"; import { TailwindElement } from "@/classes/TailwindElement"; -import { NavigateController } from "@/controllers/navigate"; -import { SubscriptionStatus } from "@/types/billing"; +import { APIController } from "@/controllers/api"; +import { SubscriptionStatus, type BillingPortal } from "@/types/billing"; import type { OrgData, OrgQuotas } from "@/types/org"; -import type { AuthState } from "@/utils/AuthService"; +import type { Auth, AuthState } from "@/utils/AuthService"; import { humanizeSeconds } from "@/utils/executionTimeFormatter"; import { formatNumber, getLocale } from "@/utils/localization"; import { pluralOf } from "@/utils/pluralize"; @@ -43,14 +45,14 @@ export class OrgSettingsBilling extends TailwindElement { @property({ type: String, noAccessor: true }) salesEmail?: string; - private readonly navigate = new NavigateController(this); + private readonly api = new APIController(this); get portalUrlLabel() { const subscription = this.org?.subscription; if (!subscription) return; - let label = msg("Manage Plan"); + let label = msg("Manage Billing"); switch (subscription.status) { case SubscriptionStatus.PausedPaymentFailed: { @@ -68,6 +70,28 @@ export class OrgSettingsBilling extends TailwindElement { return label; } + private readonly portalUrl = new Task(this, { + task: async ([org, authState]) => { + if (!org || !authState) throw new Error("Missing args"); + try { + const { portalUrl } = await this.getPortalUrl(org.id, authState); + + if (portalUrl) { + return portalUrl; + } else { + throw new Error("Missing portalUrl"); + } + } catch (e) { + console.debug(e); + + throw new Error( + msg("Sorry, couldn't retrieve current plan at this time."), + ); + } + }, + args: () => [this.org, this.authState] as const, + }); + render() { return html`
@@ -95,6 +119,32 @@ export class OrgSettingsBilling extends TailwindElement { ? this.renderContactSalesLink(this.salesEmail) : nothing}
+ ${org.subscription?.futureCancelDate + ? html` +
+ + + ${msg( + html`Your plan will be canceled on + + `, + )} + +
+ ` + : nothing} ${this.renderQuotas(org.quotas)} `, )} @@ -157,41 +207,20 @@ export class OrgSettingsBilling extends TailwindElement { switch (subscription.status) { case SubscriptionStatus.Active: { - if (subscription.futureCancelDate) { - statusLabel = html` - ${msg( - html`Canceling on - - `, - )} - `; - } else { - statusLabel = html` - ${msg("Active")} - `; - } + statusLabel = html` + ${msg("Active")} + `; break; } case SubscriptionStatus.PausedPaymentFailed: { statusLabel = html` - - ${msg("Paused due to failed payment")} - + ${msg("Paused, payment failed")} `; break; } case SubscriptionStatus.Cancelled: { statusLabel = html` - ${msg("Canceled")} + ${msg("Canceled")} `; break; } @@ -235,7 +264,7 @@ export class OrgSettingsBilling extends TailwindElement {
  • ${msg( - str`${quotas.maxExecMinutesPerMonth ? humanizeSeconds(quotas.maxExecMinutesPerMonth, undefined, undefined, "long") : msg("Unlimited minutes")} of base crawling time per month`, + str`${quotas.maxExecMinutesPerMonth ? humanizeSeconds(quotas.maxExecMinutesPerMonth * 60, undefined, undefined, "long") : msg("Unlimited minutes")} of base crawling time per month`, )}
  • @@ -245,8 +274,22 @@ export class OrgSettingsBilling extends TailwindElement { return html` { + e.preventDefault(); + + // Navigate to freshest portal URL + try { + await this.portalUrl.run(); + + if (this.portalUrl.value) { + window.location.href = this.portalUrl.value; + } + } catch (e) { + console.debug(e); + } + }} > ${this.portalUrlLabel} @@ -266,4 +309,8 @@ export class OrgSettingsBilling extends TailwindElement { `; } + + private async getPortalUrl(orgId: string, auth: Auth) { + return this.api.fetch(`/orgs/${orgId}/billing-portal`, auth); + } } diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 6e22589f..0f8d36e7 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -18,7 +18,6 @@ export const ROUTES = { "(/collections(/new)(/view/:collectionId(/:collectionTab)))", "(/browser-profiles(/profile(/browser/:browserId)(/:browserProfileId)))", "(/settings(/:settingsTab))", - "(/payment-portal-redirect)", ].join(""), users: "/users", usersInvite: "/users/invite",