Continue refactoring

This commit is contained in:
Manav Rathi 2024-04-03 19:20:22 +05:30
parent d0f1bbfca7
commit 1411ca6fad
No known key found for this signature in database
4 changed files with 98 additions and 296 deletions

View file

@ -1,7 +1,7 @@
import { Container } from "components/Container";
import { Spinner } from "components/Spinner";
import * as React from "react";
import { parseAndHandleRequest } from "services/billingService";
import { parseAndHandleRequest } from "services/billing-service";
import constants from "utils/strings";
export default function Home() {

View file

@ -1,185 +0,0 @@
// TODO: Audit
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/consistent-indexed-object-style */
/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, { AxiosRequestConfig } from "axios";
interface IHTTPHeaders {
[headerKey: string]: any;
}
interface IQueryPrams {
[paramName: string]: any;
}
/**
* Service to manage all HTTP calls.
*/
class HTTPService {
constructor() {
axios.interceptors.response.use(
(response) => Promise.resolve(response),
(err) => {
if (!err.response) {
return Promise.reject(err);
}
const { response } = err;
return Promise.reject(response);
},
);
}
/**
* header object to be append to all api calls.
*/
private headers: IHTTPHeaders = {
"content-type": "application/json",
};
/**
* Sets the headers to the given object.
*/
public setHeaders(headers: IHTTPHeaders) {
this.headers = headers;
}
/**
* Adds a header to list of headers.
*/
public appendHeader(key: string, value: string) {
this.headers = {
...this.headers,
[key]: value,
};
}
/**
* Removes the given header.
*/
public removeHeader(key: string) {
this.headers[key] = undefined;
}
/**
* Returns axios interceptors.
*/
// eslint-disable-next-line class-methods-use-this
public getInterceptors() {
return axios.interceptors;
}
/**
* Generic HTTP request.
* This is done so that developer can use any functionality
* provided by axios. Here, only the set headers are spread
* over what was sent in config.
*/
public async request(config: AxiosRequestConfig, customConfig?: any) {
// eslint-disable-next-line no-param-reassign
config.headers = {
...this.headers,
...config.headers,
};
if (customConfig?.cancel) {
config.cancelToken = new axios.CancelToken(
(c) => (customConfig.cancel.exec = c),
);
}
return await axios({ ...config, ...customConfig });
}
/**
* Get request.
*/
public get(
url: string,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any,
) {
return this.request(
{
headers,
method: "GET",
params,
url,
},
customConfig,
);
}
/**
* Post request
*/
public post(
url: string,
data?: any,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any,
) {
return this.request(
{
data,
headers,
method: "POST",
params,
url,
},
customConfig,
);
}
/**
* Put request
*/
public put(
url: string,
data: any,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any,
) {
return this.request(
{
data,
headers,
method: "PUT",
params,
url,
},
customConfig,
);
}
/**
* Delete request
*/
public delete(
url: string,
data: any,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any,
) {
return this.request(
{
data,
headers,
method: "DELETE",
params,
url,
},
customConfig,
);
}
}
// Creates a Singleton Service.
// This will help me maintain common headers / functionality
// at a central place.
export default new HTTPService();

View file

@ -7,7 +7,6 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import { loadStripe } from "@stripe/stripe-js";
import HTTPService from "./HTTPService";
/**
* Communicate with Stripe using their JS SDK, and redirect back to the client
@ -65,36 +64,20 @@ const isStripeAccountCountry = (c: unknown): c is StripeAccountCountry => {
};
const stripePublishableKey = (accountCountry: StripeAccountCountry) => {
if (accountCountry == "IN") {
return (
process.env.NEXT_PUBLIC_STRIPE_IN_PUBLISHABLE_KEY ??
"pk_live_51HAhqDK59oeucIMOiTI6MDDM2UWUbCAJXJCGsvjJhiO8nYJz38rQq5T4iyQLDMKxqEDUfU5Hopuj4U5U4dff23oT00fHvZeodC"
);
} else if (accountCountry == "US") {
return (
process.env.NEXT_PUBLIC_STRIPE_US_PUBLISHABLE_KEY ??
"pk_live_51LZ9P4G1ITnQlpAnrP6pcS7NiuJo3SnJ7gibjJlMRatkrd2EY1zlMVTVQG5RkSpLPbsHQzFfnEtgHnk1PiylIFkk00tC0LWXwi"
);
} else {
throw Error("stripe account not found");
switch (accountCountry) {
case "IN":
return (
process.env.NEXT_PUBLIC_STRIPE_IN_PUBLISHABLE_KEY ??
"pk_live_51HAhqDK59oeucIMOiTI6MDDM2UWUbCAJXJCGsvjJhiO8nYJz38rQq5T4iyQLDMKxqEDUfU5Hopuj4U5U4dff23oT00fHvZeodC"
);
case "US":
return (
process.env.NEXT_PUBLIC_STRIPE_US_PUBLISHABLE_KEY ??
"pk_live_51LZ9P4G1ITnQlpAnrP6pcS7NiuJo3SnJ7gibjJlMRatkrd2EY1zlMVTVQG5RkSpLPbsHQzFfnEtgHnk1PiylIFkk00tC0LWXwi"
);
}
};
enum PAYMENT_INTENT_STATUS {
SUCCESS = "success",
REQUIRE_ACTION = "requires_action",
REQUIRE_PAYMENT_METHOD = "requires_payment_method",
}
enum STRIPE_ERROR_TYPE {
CARD_ERROR = "card_error",
AUTHENTICATION_ERROR = "authentication_error",
}
enum STRIPE_ERROR_CODE {
AUTHENTICATION_ERROR = "payment_intent_authentication_failure",
}
type RedirectStatus = "success" | "fail";
type FailureReason =
@ -117,13 +100,6 @@ type FailureReason =
| "canceled"
| "server_error";
interface SubscriptionUpdateResponse {
result: {
status: PAYMENT_INTENT_STATUS;
clientSecret: string;
};
}
/** Return the {@link StripeAccountCountry} for the user */
const getUserStripeAccountCountry = async (
paymentToken: string,
@ -212,80 +188,102 @@ export async function updateSubscription(
try {
const accountCountry = await getUserStripeAccountCountry(paymentToken);
const stripe = await getStripe(redirectURL, accountCountry);
const { result } = await subscriptionUpdateRequest(
const { status, clientSecret } = await updateStripeSubscription(
paymentToken,
productID,
);
switch (result.status) {
case PAYMENT_INTENT_STATUS.SUCCESS:
// subscription updated successfully
// no-op required
return redirectToApp(redirectURL, RESPONSE_STATUS.success);
switch (status) {
case "success":
// Subscription was updated successfully, nothing more required
return redirectToApp(redirectURL, "success");
case PAYMENT_INTENT_STATUS.REQUIRE_PAYMENT_METHOD:
case "requires_payment_method":
return redirectToApp(
redirectURL,
RESPONSE_STATUS.fail,
FAILURE_REASON.REQUIRE_PAYMENT_METHOD,
"fail",
"requires_payment_method",
);
case PAYMENT_INTENT_STATUS.REQUIRE_ACTION: {
const { error } = await stripe.confirmCardPayment(
result.clientSecret,
);
if (error) {
logError(
error,
`${error.message} - subscription update failed`,
);
if (error.type === STRIPE_ERROR_TYPE.CARD_ERROR) {
case "requires_action": {
const { error } = await stripe.confirmCardPayment(clientSecret);
if (!error) {
return redirectToApp(redirectURL, "success");
} else {
console.error("Failed to confirm card payment", error);
if (error.type == "card_error") {
return redirectToApp(
redirectURL,
RESPONSE_STATUS.fail,
FAILURE_REASON.REQUIRE_PAYMENT_METHOD,
"fail",
"requires_payment_method",
);
} else if (
error.type === STRIPE_ERROR_TYPE.AUTHENTICATION_ERROR ||
error.code === STRIPE_ERROR_CODE.AUTHENTICATION_ERROR
error.type == "authentication_error" ||
error.code == "payment_intent_authentication_failure"
) {
return redirectToApp(
redirectURL,
RESPONSE_STATUS.fail,
FAILURE_REASON.AUTHENTICATION_FAILED,
"fail",
"authentication_failed",
);
} else {
return redirectToApp(redirectURL, RESPONSE_STATUS.fail);
return redirectToApp(redirectURL, "fail");
}
} else {
return redirectToApp(redirectURL, RESPONSE_STATUS.success);
}
}
}
} catch (e) {
logError(e, "subscription update failed");
redirectToApp(
redirectURL,
RESPONSE_STATUS.fail,
FAILURE_REASON.SERVER_ERROR,
);
console.log("Subscription update failed", e);
redirectToApp(redirectURL, "fail", "server_error");
throw e;
}
}
async function subscriptionUpdateRequest(
type PaymentStatus = "success" | "requires_action" | "requires_payment_method";
const isPaymentStatus = (s: unknown): s is PaymentStatus =>
s == "success" || s == "requires_action" || s == "requires_payment_method";
interface UpdateStripeSubscriptionResponse {
status: PaymentStatus;
clientSecret: string;
}
/**
* Make a request to museum to update an existing Stript subscription with
* {@link productID} for the user.
*/
async function updateStripeSubscription(
paymentToken: string,
productID: string,
): Promise<SubscriptionUpdateResponse> {
const response = await HTTPService.post(
`${getEndpoint()}/billing/stripe/update-subscription`,
{
productID,
},
undefined,
{
): Promise<UpdateStripeSubscriptionResponse> {
const url = `${apiHost}/billing/stripe/update-subscription`;
const res = await fetch(url, {
method: "POST",
headers: {
"X-Auth-Token": paymentToken,
},
);
return response.data;
body: JSON.stringify({
productID,
}),
});
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
const json: unknown = await res.json();
if (json && typeof json == "object" && "result" in json) {
const result = json.result;
if (
result &&
typeof result == "object" &&
"status" in result &&
"clientSecret" in result
) {
const status = result.status;
const clientSecret = result.clientSecret;
if (isPaymentStatus(status) && typeof clientSecret == "string") {
return { status, clientSecret };
}
}
}
throw new Error(`Unexpected response for ${url}: ${JSON.stringify(json)}`);
}
const redirectToApp = (

View file

@ -1,32 +1,21 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": "./src",
"incremental": true,
"allowJs": true
},
"include": [
"next-env.d.ts",
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"next.config.js"
]
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": "./src",
"incremental": true,
"allowJs": true
},
"include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "next.config.js"]
}