ts: Convert `web/src/billing` module to TypeScript.

Converted all files inside `web/src/billing` to TypeScript.
This commit is contained in:
Lalit 2023-02-26 16:08:01 +05:30 committed by Tim Abbott
parent 1f4dd0705d
commit 13187ff8f6
10 changed files with 221 additions and 134 deletions

View File

@ -54,9 +54,9 @@ EXEMPT_FILES = make_set(
"web/src/assets.d.ts", "web/src/assets.d.ts",
"web/src/attachments_ui.js", "web/src/attachments_ui.js",
"web/src/avatar.js", "web/src/avatar.js",
"web/src/billing/event_status.js", "web/src/billing/event_status.ts",
"web/src/billing/helpers.js", "web/src/billing/helpers.ts",
"web/src/billing/upgrade.js", "web/src/billing/upgrade.ts",
"web/src/blueslip.ts", "web/src/blueslip.ts",
"web/src/blueslip_stacktrace.ts", "web/src/blueslip_stacktrace.ts",
"web/src/click_handlers.js", "web/src/click_handlers.js",

View File

@ -2,7 +2,7 @@ import $ from "jquery";
import * as helpers from "./helpers"; import * as helpers from "./helpers";
export function create_update_license_request() { export function create_update_license_request(): void {
helpers.create_ajax_request( helpers.create_ajax_request(
"/json/billing/plan", "/json/billing/plan",
"licensechange", "licensechange",
@ -12,31 +12,31 @@ export function create_update_license_request() {
); );
} }
export function initialize() { export function initialize(): void {
helpers.set_tab("billing"); helpers.set_tab("billing");
helpers.set_sponsorship_form(); helpers.set_sponsorship_form();
$("#update-card-button").on("click", (e) => { $("#update-card-button").on("click", (e) => {
const success_callback = (response) => {
window.location.replace(response.stripe_session_url);
};
helpers.create_ajax_request( helpers.create_ajax_request(
"/json/billing/session/start_card_update_session", "/json/billing/session/start_card_update_session",
"cardchange", "cardchange",
[], [],
"POST", "POST",
success_callback, (response) => {
const response_data = helpers.stripe_session_url_schema.parse(response);
window.location.replace(response_data.stripe_session_url);
},
); );
e.preventDefault(); e.preventDefault();
}); });
$("#update-licenses-button").on("click", (e) => { $("#update-licenses-button").on("click", (e) => {
if (helpers.is_valid_input($("#new_licenses_input")) === false) { if (!helpers.is_valid_input($("#new_licenses_input"))) {
return; return;
} }
e.preventDefault(); e.preventDefault();
const current_licenses = $("#licensechange-input-section").data("licenses"); const current_licenses: number = $("#licensechange-input-section").data("licenses");
const new_licenses = $("#new_licenses_input").val(); const new_licenses: number = Number.parseInt($("#new_licenses_input").val() as string, 10);
if (new_licenses > current_licenses) { if (new_licenses > current_licenses) {
$("#new_license_count_holder").text(new_licenses); $("#new_license_count_holder").text(new_licenses);
$("#current_license_count_holder").text(current_licenses); $("#current_license_count_holder").text(current_licenses);

View File

@ -1,10 +1,31 @@
import $ from "jquery"; import $ from "jquery";
import {z} from "zod";
import * as loading from "../loading"; import * as loading from "../loading";
import * as helpers from "./helpers"; import * as helpers from "./helpers";
function update_status_and_redirect(status_message, redirect_to) { const stripe_response_schema = z.object({
session: z.object({
type: z.string(),
stripe_payment_intent_id: z.string().optional(),
status: z.string(),
event_handler: z
.object({
status: z.string(),
error: z
.object({
message: z.string(),
})
.optional(),
})
.optional(),
}),
});
type StripeSession = z.infer<typeof stripe_response_schema>["session"];
function update_status_and_redirect(status_message: string, redirect_to: string): void {
$("#webhook-loading").hide(); $("#webhook-loading").hide();
$("#webhook-success").show(); $("#webhook-success").show();
$("#webhook-success").text(status_message); $("#webhook-success").text(status_message);
@ -13,26 +34,26 @@ function update_status_and_redirect(status_message, redirect_to) {
}, 5000); }, 5000);
} }
function show_error_message(message) { function show_error_message(message: string): void {
$("#webhook-loading").hide(); $("#webhook-loading").hide();
$("#webhook-error").show(); $("#webhook-error").show();
$("#webhook-error").text(message); $("#webhook-error").text(message);
} }
function show_html_error_message(rendered_message) { function show_html_error_message(rendered_message: string): void {
$("#webhook-loading").hide(); $("#webhook-loading").hide();
$("#webhook-error").show(); $("#webhook-error").show();
$("#webhook-error").html(rendered_message); $("#webhook-error").html(rendered_message);
} }
function handle_session_complete_event(session) { function handle_session_complete_event(session: StripeSession): void {
let message = ""; let message = "";
let redirect_to = ""; let redirect_to = "";
switch (session.type) { switch (session.type) {
case "upgrade_from_billing_page": case "upgrade_from_billing_page":
case "retry_upgrade_with_another_payment_method": case "retry_upgrade_with_another_payment_method":
message = "We have received your billing details. Attempting to create charge..."; message = "We have received your billing details. Attempting to create charge...";
redirect_to = `/billing/event_status?stripe_payment_intent_id=${session.stripe_payment_intent_id}`; redirect_to = `/billing/event_status?stripe_payment_intent_id=${session.stripe_payment_intent_id!}`;
break; break;
case "free_trial_upgrade_from_billing_page": case "free_trial_upgrade_from_billing_page":
message = message =
@ -52,27 +73,29 @@ function handle_session_complete_event(session) {
update_status_and_redirect(message, redirect_to); update_status_and_redirect(message, redirect_to);
} }
async function stripe_checkout_session_status_check(stripe_session_id) { async function stripe_checkout_session_status_check(stripe_session_id: string): Promise<boolean> {
const response = await $.get("/json/billing/event/status", {stripe_session_id}); const response: unknown = await $.get("/json/billing/event/status", {stripe_session_id});
if (response.session.status === "created") { const response_data = stripe_response_schema.parse(response);
if (response_data.session.status === "created") {
return false; return false;
} }
if (response.session.event_handler.status === "started") { if (response_data.session.event_handler!.status === "started") {
return false; return false;
} }
if (response.session.event_handler.status === "succeeded") { if (response_data.session.event_handler!.status === "succeeded") {
handle_session_complete_event(response.session); handle_session_complete_event(response_data.session);
return true; return true;
} }
if (response.session.event_handler.status === "failed") { if (response_data.session.event_handler!.status === "failed") {
show_error_message(response.session.event_handler.error.message); show_error_message(response_data.session.event_handler!.error!.message);
return true; return true;
} }
return false; return false;
} }
export function initialize_retry_with_another_card_link_click_handler() { export function initialize_retry_with_another_card_link_click_handler(): void {
$("#retry-with-another-card-link").on("click", (e) => { $("#retry-with-another-card-link").on("click", (e) => {
e.preventDefault(); e.preventDefault();
$("#webhook-error").hide(); $("#webhook-error").hide();
@ -82,41 +105,67 @@ export function initialize_retry_with_another_card_link_click_handler() {
[], [],
"POST", "POST",
(response) => { (response) => {
window.location.replace(response.stripe_session_url); const response_data = helpers.stripe_session_url_schema.parse(response);
window.location.replace(response_data.stripe_session_url);
}, },
); );
}); });
} }
export async function stripe_payment_intent_status_check(stripe_payment_intent_id) { export async function stripe_payment_intent_status_check(
const response = await $.get("/json/billing/event/status", {stripe_payment_intent_id}); stripe_payment_intent_id: string,
): Promise<boolean> {
const response: unknown = await $.get("/json/billing/event/status", {stripe_payment_intent_id});
switch (response.payment_intent.status) { const response_schema = z.object({
payment_intent: z.object({
status: z.string(),
event_handler: z
.object({
status: z.string(),
error: z
.object({
message: z.string(),
})
.optional(),
})
.optional(),
last_payment_error: z
.object({
message: z.string(),
})
.optional(),
}),
});
const response_data = response_schema.parse(response);
switch (response_data.payment_intent.status) {
case "requires_payment_method": case "requires_payment_method":
if (response.payment_intent.event_handler.status === "succeeded") { if (response_data.payment_intent.event_handler!.status === "succeeded") {
show_html_error_message( show_html_error_message(
response.payment_intent.last_payment_error.message + response_data.payment_intent.last_payment_error!.message +
"<br>" + "<br>" +
'You can try adding <a id="retry-with-another-card-link"> another card or </a> or retry the upgrade.', 'You can try adding <a id="retry-with-another-card-link"> another card or </a> or retry the upgrade.',
); );
initialize_retry_with_another_card_link_click_handler(); initialize_retry_with_another_card_link_click_handler();
return true; return true;
} }
if (response.payment_intent.event_handler.status === "failed") { if (response_data.payment_intent.event_handler!.status === "failed") {
show_error_message(response.payment_intent.event_handler.error.message); show_error_message(response_data.payment_intent.event_handler!.error!.message);
return true; return true;
} }
return false; return false;
case "succeeded": case "succeeded":
if (response.payment_intent.event_handler.status === "succeeded") { if (response_data.payment_intent.event_handler!.status === "succeeded") {
update_status_and_redirect( update_status_and_redirect(
"Charge created successfully. Your organization has been upgraded. Redirecting to billing page...", "Charge created successfully. Your organization has been upgraded. Redirecting to billing page...",
"/billing/", "/billing/",
); );
return true; return true;
} }
if (response.payment_intent.event_handler.status === "failed") { if (response_data.payment_intent.event_handler!.status === "failed") {
show_error_message(response.payment_intent.event_handler.error.message); show_error_message(response_data.payment_intent.event_handler!.error!.message);
return true; return true;
} }
return false; return false;
@ -125,30 +174,30 @@ export async function stripe_payment_intent_status_check(stripe_payment_intent_i
} }
} }
export async function check_status() { export async function check_status(): Promise<boolean> {
if ($("#data").attr("data-stripe-session-id")) { if ($("#data").attr("data-stripe-session-id")) {
return await stripe_checkout_session_status_check( return await stripe_checkout_session_status_check(
$("#data").attr("data-stripe-session-id"), $("#data").attr("data-stripe-session-id")!,
); );
} }
return await stripe_payment_intent_status_check( return await stripe_payment_intent_status_check(
$("#data").attr("data-stripe-payment-intent-id"), $("#data").attr("data-stripe-payment-intent-id")!,
); );
} }
async function start_status_polling() { async function start_status_polling(): Promise<void> {
let completed = false; let completed = false;
try { try {
completed = await check_status(); completed = await check_status();
} catch { } catch {
setTimeout(start_status_polling, 5000); setTimeout(() => void start_status_polling(), 5000);
} }
if (!completed) { if (!completed) {
setTimeout(start_status_polling, 5000); setTimeout(() => void start_status_polling(), 5000);
} }
} }
async function initialize() { async function initialize(): Promise<void> {
const form_loading = "#webhook-loading"; const form_loading = "#webhook-loading";
const form_loading_indicator = "#webhook_loading_indicator"; const form_loading_indicator = "#webhook_loading_indicator";
@ -161,5 +210,5 @@ async function initialize() {
} }
$(() => { $(() => {
initialize(); void initialize();
}); });

View File

@ -1,16 +1,37 @@
import $ from "jquery"; import $ from "jquery";
import {z} from "zod";
import * as loading from "../loading"; import * as loading from "../loading";
import {page_params} from "./page_params"; import {page_params} from "./page_params";
type FormDataObject = Record<string, string>;
export type Prices = {
monthly: number;
annual: number;
};
export type DiscountDetails = {
opensource: string;
research: string;
nonprofit: string;
event: string;
education: string;
education_nonprofit: string;
};
export const stripe_session_url_schema = z.object({
stripe_session_url: z.string(),
});
export function create_ajax_request( export function create_ajax_request(
url, url: string,
form_name, form_name: string,
ignored_inputs = [], ignored_inputs: string[] = [],
type = "POST", type = "POST",
success_callback, success_callback: (response: unknown) => void,
) { ): void {
const $form = $(`#${CSS.escape(form_name)}-form`); const $form = $(`#${CSS.escape(form_name)}-form`);
const form_loading_indicator = `#${CSS.escape(form_name)}_loading_indicator`; const form_loading_indicator = `#${CSS.escape(form_name)}_loading_indicator`;
const form_input_section = `#${CSS.escape(form_name)}-input-section`; const form_input_section = `#${CSS.escape(form_name)}-input-section`;
@ -31,7 +52,7 @@ export function create_ajax_request(
$(zulip_limited_section).hide(); $(zulip_limited_section).hide();
$(free_trial_alert_message).hide(); $(free_trial_alert_message).hide();
const data = {}; const data: FormDataObject = {};
for (const item of $form.serializeArray()) { for (const item of $form.serializeArray()) {
if (ignored_inputs.includes(item.name)) { if (ignored_inputs.includes(item.name)) {
@ -40,11 +61,11 @@ export function create_ajax_request(
data[item.name] = item.value; data[item.name] = item.value;
} }
$.ajax({ void $.ajax({
type, type,
url, url,
data, data,
success(response) { success(response: unknown) {
$(form_loading).hide(); $(form_loading).hide();
$(form_error).hide(); $(form_error).hide();
$(form_success).show(); $(form_success).show();
@ -67,7 +88,7 @@ export function create_ajax_request(
}); });
} }
export function format_money(cents) { export function format_money(cents: number): string {
// allow for small floating point errors // allow for small floating point errors
cents = Math.ceil(cents - 0.001); cents = Math.ceil(cents - 0.001);
let precision; let precision;
@ -79,17 +100,17 @@ export function format_money(cents) {
return new Intl.NumberFormat("en-US", { return new Intl.NumberFormat("en-US", {
minimumFractionDigits: precision, minimumFractionDigits: precision,
maximumFractionDigits: precision, maximumFractionDigits: precision,
}).format((cents / 100).toFixed(precision)); }).format(Number.parseFloat((cents / 100).toFixed(precision)));
} }
export function update_charged_amount(prices, schedule) { export function update_charged_amount(prices: Prices, schedule: keyof Prices): void {
$("#charged_amount").text(format_money(page_params.seat_count * prices[schedule])); $("#charged_amount").text(format_money(page_params.seat_count * prices[schedule]));
} }
export function update_discount_details(organization_type) { export function update_discount_details(organization_type: keyof DiscountDetails): void {
let discount_notice = let discount_notice =
"Your organization may be eligible for a discount on Zulip Cloud Standard. Organizations whose members are not employees are generally eligible."; "Your organization may be eligible for a discount on Zulip Cloud Standard. Organizations whose members are not employees are generally eligible.";
const discount_details = { const discount_details: DiscountDetails = {
opensource: "Zulip Cloud Standard is free for open-source projects.", opensource: "Zulip Cloud Standard is free for open-source projects.",
research: "Zulip Cloud Standard is free for academic research.", research: "Zulip Cloud Standard is free for academic research.",
nonprofit: "Zulip Cloud Standard is discounted 85%+ for registered non-profits.", nonprofit: "Zulip Cloud Standard is discounted 85%+ for registered non-profits.",
@ -104,7 +125,7 @@ export function update_discount_details(organization_type) {
$("#sponsorship-discount-details").text(discount_notice); $("#sponsorship-discount-details").text(discount_notice);
} }
export function show_license_section(license) { export function show_license_section(license: string): void {
$("#license-automatic-section").hide(); $("#license-automatic-section").hide();
$("#license-manual-section").hide(); $("#license-manual-section").hide();
@ -117,14 +138,14 @@ export function show_license_section(license) {
$(input_id).prop("disabled", false); $(input_id).prop("disabled", false);
} }
let current_page; let current_page: string;
function handle_hashchange() { function handle_hashchange(): void {
$(`#${CSS.escape(current_page)}-tabs.nav a[href="${CSS.escape(location.hash)}"]`).tab("show"); $(`#${CSS.escape(current_page)}-tabs.nav a[href="${CSS.escape(location.hash)}"]`).tab("show");
$("html").scrollTop(0); $("html").scrollTop(0);
} }
export function set_tab(page) { export function set_tab(page: string): void {
const hash = location.hash; const hash = location.hash;
if (hash) { if (hash) {
$(`#${CSS.escape(page)}-tabs.nav a[href="${CSS.escape(hash)}"]`).tab("show"); $(`#${CSS.escape(page)}-tabs.nav a[href="${CSS.escape(hash)}"]`).tab("show");
@ -132,14 +153,14 @@ export function set_tab(page) {
} }
$(`#${CSS.escape(page)}-tabs.nav-tabs a`).on("click", function () { $(`#${CSS.escape(page)}-tabs.nav-tabs a`).on("click", function () {
location.hash = this.hash; location.hash = (this as HTMLAnchorElement).hash;
}); });
current_page = page; current_page = page;
window.addEventListener("hashchange", handle_hashchange); window.addEventListener("hashchange", handle_hashchange);
} }
export function set_sponsorship_form() { export function set_sponsorship_form(): void {
$("#sponsorship-button").on("click", (e) => { $("#sponsorship-button").on("click", (e) => {
if (!is_valid_input($("#sponsorship-form"))) { if (!is_valid_input($("#sponsorship-form"))) {
return; return;
@ -151,6 +172,6 @@ export function set_sponsorship_form() {
}); });
} }
export function is_valid_input(elem) { export function is_valid_input(elem: JQuery<HTMLFormElement>): boolean {
return elem[0].checkValidity(); return elem[0].checkValidity();
} }

View File

@ -1,68 +0,0 @@
import $ from "jquery";
import * as helpers from "./helpers";
import {page_params} from "./page_params";
export const initialize = () => {
helpers.set_tab("upgrade");
helpers.set_sponsorship_form();
$("#add-card-button").on("click", (e) => {
const license_management = $("input[type=radio][name=license_management]:checked").val();
if (
helpers.is_valid_input($(`#${CSS.escape(license_management)}_license_count`)) === false
) {
return;
}
e.preventDefault();
const success_callback = (response) => {
window.location.replace(response.stripe_session_url);
};
helpers.create_ajax_request(
"/json/billing/upgrade",
"autopay",
[],
"POST",
success_callback,
);
});
$("#invoice-button").on("click", (e) => {
if (helpers.is_valid_input($("#invoiced_licenses")) === false) {
return;
}
e.preventDefault();
helpers.create_ajax_request("/json/billing/upgrade", "invoice", [], "POST", () =>
window.location.replace("/billing/"),
);
});
const prices = {};
prices.annual = page_params.annual_price * (1 - page_params.percent_off / 100);
prices.monthly = page_params.monthly_price * (1 - page_params.percent_off / 100);
$("input[type=radio][name=license_management]").on("change", function () {
helpers.show_license_section(this.value);
});
$("input[type=radio][name=schedule]").on("change", function () {
helpers.update_charged_amount(prices, this.value);
});
$("select[name=organization-type]").on("change", (e) => {
const string_value = $(e.currentTarget.selectedOptions).attr("data-string-value");
helpers.update_discount_details(string_value);
});
$("#autopay_annual_price").text(helpers.format_money(prices.annual));
$("#autopay_annual_price_per_month").text(helpers.format_money(prices.annual / 12));
$("#autopay_monthly_price").text(helpers.format_money(prices.monthly));
$("#invoice_annual_price").text(helpers.format_money(prices.annual));
$("#invoice_annual_price_per_month").text(helpers.format_money(prices.annual / 12));
helpers.show_license_section($("input[type=radio][name=license_management]:checked").val());
helpers.update_charged_amount(prices, $("input[type=radio][name=schedule]:checked").val());
};
$(() => {
initialize();
});

View File

@ -0,0 +1,72 @@
import $ from "jquery";
import * as helpers from "./helpers";
import type {DiscountDetails, Prices} from "./helpers";
import {page_params} from "./page_params";
export const initialize = (): void => {
helpers.set_tab("upgrade");
helpers.set_sponsorship_form();
$("#add-card-button").on("click", (e) => {
const license_management: string = $(
"input[type=radio][name=license_management]:checked",
).val() as string;
if (!helpers.is_valid_input($(`#${CSS.escape(license_management)}_license_count`))) {
return;
}
e.preventDefault();
helpers.create_ajax_request("/json/billing/upgrade", "autopay", [], "POST", (response) => {
const response_data = helpers.stripe_session_url_schema.parse(response);
window.location.replace(response_data.stripe_session_url);
});
});
$("#invoice-button").on("click", (e) => {
if (!helpers.is_valid_input($("#invoiced_licenses"))) {
return;
}
e.preventDefault();
helpers.create_ajax_request("/json/billing/upgrade", "invoice", [], "POST", () =>
window.location.replace("/billing/"),
);
});
const prices: Prices = {
annual: page_params.annual_price * (1 - page_params.percent_off / 100),
monthly: page_params.monthly_price * (1 - page_params.percent_off / 100),
};
$("input[type=radio][name=license_management]").on("change", function (this: HTMLInputElement) {
helpers.show_license_section(this.value);
});
$("input[type=radio][name=schedule]").on("change", function (this: HTMLInputElement) {
helpers.update_charged_amount(prices, this.value as keyof Prices);
});
$("select[name=organization-type]").on("change", (e) => {
const string_value = $((e.currentTarget as HTMLSelectElement).selectedOptions).attr(
"data-string-value",
);
helpers.update_discount_details(string_value as keyof DiscountDetails);
});
$("#autopay_annual_price").text(helpers.format_money(prices.annual));
$("#autopay_annual_price_per_month").text(helpers.format_money(prices.annual / 12));
$("#autopay_monthly_price").text(helpers.format_money(prices.monthly));
$("#invoice_annual_price").text(helpers.format_money(prices.annual));
$("#invoice_annual_price_per_month").text(helpers.format_money(prices.annual / 12));
helpers.show_license_section(
$("input[type=radio][name=license_management]:checked").val() as string,
);
helpers.update_charged_amount(
prices,
$("input[type=radio][name=schedule]:checked").val() as keyof Prices,
);
};
$(() => {
initialize();
});

1
web/src/global.d.ts vendored
View File

@ -16,6 +16,7 @@ type JQueryCaretRange = {
interface JQuery { interface JQuery {
expectOne(): JQuery; expectOne(): JQuery;
tab(action?: string): this; // From web/third/bootstrap tab(action?: string): this; // From web/third/bootstrap
modal(action?: string): this; // From web/third/bootstrap
// Types for jquery-caret-plugin // Types for jquery-caret-plugin
caret(): number; caret(): number;

View File

@ -42,6 +42,11 @@ run_test("initialize", ({override}) => {
run_test("card_update", ({override}) => { run_test("card_update", ({override}) => {
override(helpers, "set_tab", () => {}); override(helpers, "set_tab", () => {});
override(helpers, "stripe_session_url_schema", {
parse(obj) {
return obj;
},
});
let create_ajax_request_called = false; let create_ajax_request_called = false;
function card_change_ajax(url, form_name, ignored_inputs, method, success_callback) { function card_change_ajax(url, form_name, ignored_inputs, method, success_callback) {
assert.equal(url, "/json/billing/session/start_card_update_session"); assert.equal(url, "/json/billing/session/start_card_update_session");

View File

@ -24,6 +24,11 @@ run_test("initialize_retry_with_another_card_link_click_handler", ({override}) =
}); });
callback({stripe_session_url: "stripe_session_url"}); callback({stripe_session_url: "stripe_session_url"});
}); });
override(helpers, "stripe_session_url_schema", {
parse(obj) {
return obj;
},
});
event_status.initialize_retry_with_another_card_link_click_handler(); event_status.initialize_retry_with_another_card_link_click_handler();
const retry_click_handler = $("#retry-with-another-card-link").get_on_handler("click"); const retry_click_handler = $("#retry-with-another-card-link").get_on_handler("click");
retry_click_handler({preventDefault() {}}); retry_click_handler({preventDefault() {}});
@ -37,6 +42,7 @@ run_test("check_status", async ({override}) => {
return { return {
session: { session: {
status: "created", status: "created",
type: "upgrade_from_billing_page",
}, },
}; };
}); });
@ -49,6 +55,7 @@ run_test("check_status", async ({override}) => {
return { return {
session: { session: {
status: "completed", status: "completed",
type: "upgrade_from_billing_page",
event_handler: { event_handler: {
status: "started", status: "started",
}, },

View File

@ -32,7 +32,7 @@
"./src/billing/page_params", "./src/billing/page_params",
"./src/bundles/portico", "./src/bundles/portico",
"./styles/portico/landing_page.css", "./styles/portico/landing_page.css",
"./src/billing/event_status.js", "./src/billing/event_status",
"./src/billing/helpers", "./src/billing/helpers",
"./styles/portico/billing.css" "./styles/portico/billing.css"
], ],