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 { Container } from "components/Container";
import { Spinner } from "components/Spinner"; import { Spinner } from "components/Spinner";
import * as React from "react"; import * as React from "react";
import { parseAndHandleRequest } from "services/billingService"; import { parseAndHandleRequest } from "services/billing-service";
import constants from "utils/strings"; import constants from "utils/strings";
export default function Home() { 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 */ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
import { loadStripe } from "@stripe/stripe-js"; import { loadStripe } from "@stripe/stripe-js";
import HTTPService from "./HTTPService";
/** /**
* Communicate with Stripe using their JS SDK, and redirect back to the client * 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) => { const stripePublishableKey = (accountCountry: StripeAccountCountry) => {
if (accountCountry == "IN") { switch (accountCountry) {
return ( case "IN":
process.env.NEXT_PUBLIC_STRIPE_IN_PUBLISHABLE_KEY ?? return (
"pk_live_51HAhqDK59oeucIMOiTI6MDDM2UWUbCAJXJCGsvjJhiO8nYJz38rQq5T4iyQLDMKxqEDUfU5Hopuj4U5U4dff23oT00fHvZeodC" process.env.NEXT_PUBLIC_STRIPE_IN_PUBLISHABLE_KEY ??
); "pk_live_51HAhqDK59oeucIMOiTI6MDDM2UWUbCAJXJCGsvjJhiO8nYJz38rQq5T4iyQLDMKxqEDUfU5Hopuj4U5U4dff23oT00fHvZeodC"
} else if (accountCountry == "US") { );
return ( case "US":
process.env.NEXT_PUBLIC_STRIPE_US_PUBLISHABLE_KEY ?? return (
"pk_live_51LZ9P4G1ITnQlpAnrP6pcS7NiuJo3SnJ7gibjJlMRatkrd2EY1zlMVTVQG5RkSpLPbsHQzFfnEtgHnk1PiylIFkk00tC0LWXwi" process.env.NEXT_PUBLIC_STRIPE_US_PUBLISHABLE_KEY ??
); "pk_live_51LZ9P4G1ITnQlpAnrP6pcS7NiuJo3SnJ7gibjJlMRatkrd2EY1zlMVTVQG5RkSpLPbsHQzFfnEtgHnk1PiylIFkk00tC0LWXwi"
} else { );
throw Error("stripe account not found");
} }
}; };
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 RedirectStatus = "success" | "fail";
type FailureReason = type FailureReason =
@ -117,13 +100,6 @@ type FailureReason =
| "canceled" | "canceled"
| "server_error"; | "server_error";
interface SubscriptionUpdateResponse {
result: {
status: PAYMENT_INTENT_STATUS;
clientSecret: string;
};
}
/** Return the {@link StripeAccountCountry} for the user */ /** Return the {@link StripeAccountCountry} for the user */
const getUserStripeAccountCountry = async ( const getUserStripeAccountCountry = async (
paymentToken: string, paymentToken: string,
@ -212,80 +188,102 @@ export async function updateSubscription(
try { try {
const accountCountry = await getUserStripeAccountCountry(paymentToken); const accountCountry = await getUserStripeAccountCountry(paymentToken);
const stripe = await getStripe(redirectURL, accountCountry); const stripe = await getStripe(redirectURL, accountCountry);
const { result } = await subscriptionUpdateRequest( const { status, clientSecret } = await updateStripeSubscription(
paymentToken, paymentToken,
productID, productID,
); );
switch (result.status) { switch (status) {
case PAYMENT_INTENT_STATUS.SUCCESS: case "success":
// subscription updated successfully // Subscription was updated successfully, nothing more required
// no-op required return redirectToApp(redirectURL, "success");
return redirectToApp(redirectURL, RESPONSE_STATUS.success);
case PAYMENT_INTENT_STATUS.REQUIRE_PAYMENT_METHOD: case "requires_payment_method":
return redirectToApp( return redirectToApp(
redirectURL, redirectURL,
RESPONSE_STATUS.fail, "fail",
FAILURE_REASON.REQUIRE_PAYMENT_METHOD, "requires_payment_method",
); );
case PAYMENT_INTENT_STATUS.REQUIRE_ACTION: {
const { error } = await stripe.confirmCardPayment( case "requires_action": {
result.clientSecret, const { error } = await stripe.confirmCardPayment(clientSecret);
); if (!error) {
if (error) { return redirectToApp(redirectURL, "success");
logError( } else {
error, console.error("Failed to confirm card payment", error);
`${error.message} - subscription update failed`, if (error.type == "card_error") {
);
if (error.type === STRIPE_ERROR_TYPE.CARD_ERROR) {
return redirectToApp( return redirectToApp(
redirectURL, redirectURL,
RESPONSE_STATUS.fail, "fail",
FAILURE_REASON.REQUIRE_PAYMENT_METHOD, "requires_payment_method",
); );
} else if ( } else if (
error.type === STRIPE_ERROR_TYPE.AUTHENTICATION_ERROR || error.type == "authentication_error" ||
error.code === STRIPE_ERROR_CODE.AUTHENTICATION_ERROR error.code == "payment_intent_authentication_failure"
) { ) {
return redirectToApp( return redirectToApp(
redirectURL, redirectURL,
RESPONSE_STATUS.fail, "fail",
FAILURE_REASON.AUTHENTICATION_FAILED, "authentication_failed",
); );
} else { } else {
return redirectToApp(redirectURL, RESPONSE_STATUS.fail); return redirectToApp(redirectURL, "fail");
} }
} else {
return redirectToApp(redirectURL, RESPONSE_STATUS.success);
} }
} }
} }
} catch (e) { } catch (e) {
logError(e, "subscription update failed"); console.log("Subscription update failed", e);
redirectToApp( redirectToApp(redirectURL, "fail", "server_error");
redirectURL,
RESPONSE_STATUS.fail,
FAILURE_REASON.SERVER_ERROR,
);
throw e; 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, paymentToken: string,
productID: string, productID: string,
): Promise<SubscriptionUpdateResponse> { ): Promise<UpdateStripeSubscriptionResponse> {
const response = await HTTPService.post( const url = `${apiHost}/billing/stripe/update-subscription`;
`${getEndpoint()}/billing/stripe/update-subscription`, const res = await fetch(url, {
{ method: "POST",
productID, headers: {
},
undefined,
{
"X-Auth-Token": paymentToken, "X-Auth-Token": paymentToken,
}, },
); body: JSON.stringify({
return response.data; 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 = ( const redirectToApp = (

View file

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