Continue refactoring
This commit is contained in:
parent
d0f1bbfca7
commit
1411ca6fad
4 changed files with 98 additions and 296 deletions
|
@ -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() {
|
||||||
|
|
|
@ -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();
|
|
|
@ -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 = (
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue