Skip to content

Commit fc34ef6

Browse files
committed
set up additional minute checkout interface when subscription is
available
1 parent 1b9fc06 commit fc34ef6

File tree

5 files changed

+180
-12
lines changed

5 files changed

+180
-12
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { localized, msg } from "@lit/localize";
2+
import { Task, TaskStatus } from "@lit/task";
3+
import { type SlSelectEvent } from "@shoelace-style/shoelace";
4+
import { html } from "lit";
5+
import { customElement, state } from "lit/decorators.js";
6+
7+
import { BtrixElement } from "@/classes/BtrixElement";
8+
import { type BillingAddonCheckout } from "@/types/billing";
9+
import appState from "@/utils/state";
10+
11+
const PRESET_MINUTES = [600, 1500, 3000];
12+
const PRICE_PER_MINUTE: { value: number; currency: string } | undefined =
13+
undefined;
14+
15+
@customElement("btrix-org-settings-billing-addon-link")
16+
@localized()
17+
export class OrgSettingsBillingAddonLink extends BtrixElement {
18+
@state()
19+
private lastClickedMinutesPreset: number | undefined = undefined;
20+
21+
private readonly checkoutUrl = new Task(this, {
22+
task: async ([minutes]) => {
23+
if (!appState.settings?.billingEnabled || !appState.org?.subscription)
24+
return;
25+
26+
try {
27+
const { checkoutUrl } = await this.getCheckoutUrl(minutes);
28+
29+
if (checkoutUrl) {
30+
return checkoutUrl;
31+
} else {
32+
throw new Error("Missing checkoutUrl");
33+
}
34+
} catch (e) {
35+
console.debug(e);
36+
37+
throw new Error(
38+
msg("Sorry, couldn't retrieve current plan at this time."),
39+
);
40+
}
41+
},
42+
args: () => [undefined] as readonly [number | undefined],
43+
autoRun: false,
44+
});
45+
private async getCheckoutUrl(minutes?: number | undefined) {
46+
const params = new URLSearchParams();
47+
if (minutes) params.append("minutes", minutes.toString());
48+
return this.api.fetch<BillingAddonCheckout>(
49+
`/orgs/${this.orgId}/checkout/execution-minutes?${params.toString()}`,
50+
);
51+
}
52+
53+
private readonly localizeMinutes = (minutes: number) => {
54+
return this.localize.number(minutes, {
55+
style: "unit",
56+
unit: "minute",
57+
unitDisplay: "long",
58+
});
59+
};
60+
61+
private async checkout(minutes?: number | undefined) {
62+
await this.checkoutUrl.run([minutes]);
63+
if (this.checkoutUrl.value) {
64+
window.location.href = this.checkoutUrl.value;
65+
} else {
66+
this.notify.toast({
67+
message: msg("Sorry, checkout isn’t available at this time."),
68+
id: "checkout-unavailable",
69+
variant: "warning",
70+
});
71+
}
72+
}
73+
74+
render() {
75+
const priceForMinutes = (minutes: number) => {
76+
if (!PRICE_PER_MINUTE) return;
77+
return this.localize.number(minutes * PRICE_PER_MINUTE.value, {
78+
style: "currency",
79+
currency: PRICE_PER_MINUTE.currency,
80+
});
81+
};
82+
const price = priceForMinutes(1);
83+
return html`
84+
<sl-button
85+
@click=${async () => {
86+
this.lastClickedMinutesPreset = undefined;
87+
await this.checkout();
88+
}}
89+
size="small"
90+
variant="text"
91+
?loading=${this.checkoutUrl.status === TaskStatus.PENDING &&
92+
this.lastClickedMinutesPreset === undefined}
93+
?disabled=${this.checkoutUrl.status === TaskStatus.PENDING &&
94+
this.lastClickedMinutesPreset !== undefined}
95+
class="-ml-3"
96+
>
97+
${msg("Add More Execution Minutes")}
98+
</sl-button>
99+
<hr class="h-6 border-l" aria-orientation="vertical" />
100+
<sl-dropdown
101+
distance="4"
102+
placement="bottom-end"
103+
hoist
104+
stay-open-on-select
105+
@sl-select=${async (e: SlSelectEvent) => {
106+
this.lastClickedMinutesPreset = parseInt(e.detail.item.value);
107+
await this.checkout(this.lastClickedMinutesPreset);
108+
void e.detail.item.closest("sl-dropdown")!.hide();
109+
}}
110+
>
111+
<sl-button caret slot="trigger" variant="text" size="small" class="">
112+
<sl-visually-hidden>
113+
${msg("Preset minute amounts")}
114+
</sl-visually-hidden>
115+
</sl-button>
116+
<sl-menu>
117+
<sl-menu-label>${msg("Preset minute amounts")}</sl-menu-label>
118+
${PRESET_MINUTES.map((m) => {
119+
const minutes = this.localizeMinutes(m);
120+
return html`
121+
<sl-menu-item
122+
value=${m}
123+
?loading=${this.checkoutUrl.status === TaskStatus.PENDING &&
124+
this.lastClickedMinutesPreset === m}
125+
?disabled=${this.checkoutUrl.status === TaskStatus.PENDING &&
126+
this.lastClickedMinutesPreset !== m}
127+
>
128+
${minutes}
129+
${PRICE_PER_MINUTE &&
130+
html`<span class="text-xs text-stone-500" slot="suffix">
131+
${priceForMinutes(m)}
132+
</span>`}
133+
</sl-menu-item>
134+
`;
135+
})}
136+
</sl-menu>
137+
</sl-dropdown>
138+
${PRICE_PER_MINUTE &&
139+
html`<div class="ml-auto text-xs text-stone-500">
140+
${msg(html`${price} per minute`)}
141+
</div>`}
142+
`;
143+
}
144+
}

frontend/src/pages/org/settings/components/billing.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@ import { BtrixElement } from "@/classes/BtrixElement";
1212
import { columns } from "@/layouts/columns";
1313
import { SubscriptionStatus, type BillingPortal } from "@/types/billing";
1414
import type { OrgData, OrgQuotas } from "@/types/org";
15-
import { humanizeSeconds } from "@/utils/executionTimeFormatter";
1615
import { pluralOf } from "@/utils/pluralize";
1716
import { tw } from "@/utils/tailwind";
1817

19-
const linkClassList = tw`transition-color text-primary hover:text-primary-500`;
18+
const linkClassList = tw`text-primary transition-colors hover:text-primary-600`;
2019
const manageLinkClasslist = clsx(
2120
linkClassList,
2221
tw`flex cursor-pointer items-center gap-2 p-2 text-sm font-semibold leading-none`,
@@ -95,7 +94,12 @@ export class OrgSettingsBilling extends BtrixElement {
9594
${columns([
9695
[
9796
html`
98-
<div class="mt-5 rounded-lg border px-4 pb-4">
97+
<div
98+
class=${clsx(
99+
tw`mt-5 rounded-lg border px-4`,
100+
!this.org?.subscription && tw`pb-4`,
101+
)}
102+
>
99103
<div
100104
class="mb-3 flex items-center justify-between border-b py-2"
101105
>
@@ -194,6 +198,13 @@ export class OrgSettingsBilling extends BtrixElement {
194198
<sl-skeleton class="mb-2"></sl-skeleton>
195199
<sl-skeleton class="mb-2"></sl-skeleton>`,
196200
)}
201+
${when(
202+
this.org?.subscription,
203+
() =>
204+
html`<btrix-org-settings-billing-addon-link
205+
class="mt-3 flex items-center border-t py-2"
206+
></btrix-org-settings-billing-addon-link>`,
207+
)}
197208
</div>
198209
`,
199210
html`
@@ -346,12 +357,11 @@ export class OrgSettingsBilling extends BtrixElement {
346357
private readonly renderQuotas = (quotas: OrgQuotas) => {
347358
const maxExecMinutesPerMonth =
348359
quotas.maxExecMinutesPerMonth &&
349-
humanizeSeconds(
350-
quotas.maxExecMinutesPerMonth * 60,
351-
this.localize.lang(),
352-
undefined,
353-
"long",
354-
);
360+
this.localize.number(quotas.maxExecMinutesPerMonth, {
361+
style: "unit",
362+
unit: "minute",
363+
unitDisplay: "long",
364+
});
355365
const maxPagesPerCrawl =
356366
quotas.maxPagesPerCrawl &&
357367
`${this.localize.number(quotas.maxPagesPerCrawl)} ${pluralOf("pages", quotas.maxPagesPerCrawl)}`;
@@ -368,7 +378,7 @@ export class OrgSettingsBilling extends BtrixElement {
368378
<ul class="leading-relaxed text-neutral-700">
369379
<li>
370380
${msg(
371-
str`${maxExecMinutesPerMonth || msg("Unlimited minutes")} of crawling time`,
381+
str`${maxExecMinutesPerMonth || msg("Unlimited minutes")} of execution time`,
372382
)}
373383
</li>
374384
<li>${msg(str`${storageBytesText} of disk space`)}</li>

frontend/src/pages/org/settings/settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { tw } from "@/utils/tailwind";
2626
import "./components/general";
2727
import "./components/billing";
2828
import "./components/crawling-defaults";
29+
import "./components/billing-addon-link";
2930

3031
const styles = unsafeCSS(stylesheet);
3132

frontend/src/theme.stylesheet.css

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@
101101
/* Transition */
102102
--sl-transition-x-fast: 100ms;
103103

104-
/*
105-
*
104+
/*
105+
*
106106
* Browsertrix theme tokens
107107
*
108108
*/
@@ -178,6 +178,15 @@
178178
background-color: theme(colors.primary.500);
179179
}
180180

181+
sl-button[variant="text"]::part(base) {
182+
color: theme(colors.primary.500);
183+
@apply transition-colors;
184+
}
185+
186+
sl-button[variant="text"]::part(base):hover {
187+
color: theme(colors.primary.600);
188+
}
189+
181190
sl-radio-button[checked]::part(button) {
182191
@apply border-primary-300 bg-primary-50 text-primary-600;
183192
}

frontend/src/types/billing.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ export const billingPortalSchema = z.object({
2525
portalUrl: z.string().url(),
2626
});
2727
export type BillingPortal = z.infer<typeof billingPortalSchema>;
28+
export const billingAddonCheckoutSchema = z.object({
29+
checkoutUrl: z.string().url(),
30+
});
31+
export type BillingAddonCheckout = z.infer<typeof billingAddonCheckoutSchema>;

0 commit comments

Comments
 (0)