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/attachments_ui.js",
"web/src/avatar.js",
"web/src/billing/event_status.js",
"web/src/billing/helpers.js",
"web/src/billing/upgrade.js",
"web/src/billing/event_status.ts",
"web/src/billing/helpers.ts",
"web/src/billing/upgrade.ts",
"web/src/blueslip.ts",
"web/src/blueslip_stacktrace.ts",
"web/src/click_handlers.js",

View File

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

View File

@ -1,10 +1,31 @@
import $ from "jquery";
import {z} from "zod";
import * as loading from "../loading";
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-success").show();
$("#webhook-success").text(status_message);
@ -13,26 +34,26 @@ function update_status_and_redirect(status_message, redirect_to) {
}, 5000);
}
function show_error_message(message) {
function show_error_message(message: string): void {
$("#webhook-loading").hide();
$("#webhook-error").show();
$("#webhook-error").text(message);
}
function show_html_error_message(rendered_message) {
function show_html_error_message(rendered_message: string): void {
$("#webhook-loading").hide();
$("#webhook-error").show();
$("#webhook-error").html(rendered_message);
}
function handle_session_complete_event(session) {
function handle_session_complete_event(session: StripeSession): void {
let message = "";
let redirect_to = "";
switch (session.type) {
case "upgrade_from_billing_page":
case "retry_upgrade_with_another_payment_method":
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;
case "free_trial_upgrade_from_billing_page":
message =
@ -52,27 +73,29 @@ function handle_session_complete_event(session) {
update_status_and_redirect(message, redirect_to);
}
async function stripe_checkout_session_status_check(stripe_session_id) {
const response = await $.get("/json/billing/event/status", {stripe_session_id});
if (response.session.status === "created") {
async function stripe_checkout_session_status_check(stripe_session_id: string): Promise<boolean> {
const response: unknown = await $.get("/json/billing/event/status", {stripe_session_id});
const response_data = stripe_response_schema.parse(response);
if (response_data.session.status === "created") {
return false;
}
if (response.session.event_handler.status === "started") {
if (response_data.session.event_handler!.status === "started") {
return false;
}
if (response.session.event_handler.status === "succeeded") {
handle_session_complete_event(response.session);
if (response_data.session.event_handler!.status === "succeeded") {
handle_session_complete_event(response_data.session);
return true;
}
if (response.session.event_handler.status === "failed") {
show_error_message(response.session.event_handler.error.message);
if (response_data.session.event_handler!.status === "failed") {
show_error_message(response_data.session.event_handler!.error!.message);
return true;
}
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) => {
e.preventDefault();
$("#webhook-error").hide();
@ -82,41 +105,67 @@ export function initialize_retry_with_another_card_link_click_handler() {
[],
"POST",
(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) {
const response = await $.get("/json/billing/event/status", {stripe_payment_intent_id});
export async function stripe_payment_intent_status_check(
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":
if (response.payment_intent.event_handler.status === "succeeded") {
if (response_data.payment_intent.event_handler!.status === "succeeded") {
show_html_error_message(
response.payment_intent.last_payment_error.message +
response_data.payment_intent.last_payment_error!.message +
"<br>" +
'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();
return true;
}
if (response.payment_intent.event_handler.status === "failed") {
show_error_message(response.payment_intent.event_handler.error.message);
if (response_data.payment_intent.event_handler!.status === "failed") {
show_error_message(response_data.payment_intent.event_handler!.error!.message);
return true;
}
return false;
case "succeeded":
if (response.payment_intent.event_handler.status === "succeeded") {
if (response_data.payment_intent.event_handler!.status === "succeeded") {
update_status_and_redirect(
"Charge created successfully. Your organization has been upgraded. Redirecting to billing page...",
"/billing/",
);
return true;
}
if (response.payment_intent.event_handler.status === "failed") {
show_error_message(response.payment_intent.event_handler.error.message);
if (response_data.payment_intent.event_handler!.status === "failed") {
show_error_message(response_data.payment_intent.event_handler!.error!.message);
return true;
}
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")) {
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(
$("#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;
try {
completed = await check_status();
} catch {
setTimeout(start_status_polling, 5000);
setTimeout(() => void start_status_polling(), 5000);
}
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_indicator = "#webhook_loading_indicator";
@ -161,5 +210,5 @@ async function initialize() {
}
$(() => {
initialize();
void initialize();
});

View File

@ -1,16 +1,37 @@
import $ from "jquery";
import {z} from "zod";
import * as loading from "../loading";
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(
url,
form_name,
ignored_inputs = [],
url: string,
form_name: string,
ignored_inputs: string[] = [],
type = "POST",
success_callback,
) {
success_callback: (response: unknown) => void,
): void {
const $form = $(`#${CSS.escape(form_name)}-form`);
const form_loading_indicator = `#${CSS.escape(form_name)}_loading_indicator`;
const form_input_section = `#${CSS.escape(form_name)}-input-section`;
@ -31,7 +52,7 @@ export function create_ajax_request(
$(zulip_limited_section).hide();
$(free_trial_alert_message).hide();
const data = {};
const data: FormDataObject = {};
for (const item of $form.serializeArray()) {
if (ignored_inputs.includes(item.name)) {
@ -40,11 +61,11 @@ export function create_ajax_request(
data[item.name] = item.value;
}
$.ajax({
void $.ajax({
type,
url,
data,
success(response) {
success(response: unknown) {
$(form_loading).hide();
$(form_error).hide();
$(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
cents = Math.ceil(cents - 0.001);
let precision;
@ -79,17 +100,17 @@ export function format_money(cents) {
return new Intl.NumberFormat("en-US", {
minimumFractionDigits: 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]));
}
export function update_discount_details(organization_type) {
export function update_discount_details(organization_type: keyof DiscountDetails): void {
let discount_notice =
"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.",
research: "Zulip Cloud Standard is free for academic research.",
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);
}
export function show_license_section(license) {
export function show_license_section(license: string): void {
$("#license-automatic-section").hide();
$("#license-manual-section").hide();
@ -117,14 +138,14 @@ export function show_license_section(license) {
$(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");
$("html").scrollTop(0);
}
export function set_tab(page) {
export function set_tab(page: string): void {
const hash = location.hash;
if (hash) {
$(`#${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 () {
location.hash = this.hash;
location.hash = (this as HTMLAnchorElement).hash;
});
current_page = page;
window.addEventListener("hashchange", handle_hashchange);
}
export function set_sponsorship_form() {
export function set_sponsorship_form(): void {
$("#sponsorship-button").on("click", (e) => {
if (!is_valid_input($("#sponsorship-form"))) {
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();
}

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 {
expectOne(): JQuery;
tab(action?: string): this; // From web/third/bootstrap
modal(action?: string): this; // From web/third/bootstrap
// Types for jquery-caret-plugin
caret(): number;

View File

@ -42,6 +42,11 @@ run_test("initialize", ({override}) => {
run_test("card_update", ({override}) => {
override(helpers, "set_tab", () => {});
override(helpers, "stripe_session_url_schema", {
parse(obj) {
return obj;
},
});
let create_ajax_request_called = false;
function card_change_ajax(url, form_name, ignored_inputs, method, success_callback) {
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"});
});
override(helpers, "stripe_session_url_schema", {
parse(obj) {
return obj;
},
});
event_status.initialize_retry_with_another_card_link_click_handler();
const retry_click_handler = $("#retry-with-another-card-link").get_on_handler("click");
retry_click_handler({preventDefault() {}});
@ -37,6 +42,7 @@ run_test("check_status", async ({override}) => {
return {
session: {
status: "created",
type: "upgrade_from_billing_page",
},
};
});
@ -49,6 +55,7 @@ run_test("check_status", async ({override}) => {
return {
session: {
status: "completed",
type: "upgrade_from_billing_page",
event_handler: {
status: "started",
},

View File

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