Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 175 additions & 92 deletions Hyperswitch-React-Demo-App/package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion Hyperswitch-React-Demo-App/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"build": "npm run build-base",
"build:v2": "SDK_VERSION=v2 npm run build-base",
"build-base": "webpack --config webpack.common.js",
"format": "prettier --write \"**/*.{js,jsx}\""
"format": "prettier --write \"**/*.{js,jsx,ts,tsx}\""
},
"eslintConfig": {
"extends": [
Expand All @@ -52,13 +52,17 @@
"@babel/core": "^7.23.3",
"@babel/preset-env": "^7.23.3",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.28.5",
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7",
"babel-loader": "^9.1.3",
"concurrently": "^9.2.0",
"copy-webpack-plugin": "^11.0.0",
"file-loader": "^6.2.0",
"prettier": "^2.7.1",
"style-loader": "^4.0.0",
"terser-webpack-plugin": "^5.3.10",
"typescript": "^5.9.3",
"webpack": "^5.93.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^5.1.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ import logo from "../public/assets/hyperswitchLogo.svg";
import shirt from "../public/assets/shirt.png";
import cap from "../public/assets/cap.png";

const cartItems = [
interface CartItem {
id: number;
name: string;
price: number;
image: string;
size: string;
qty: number;
color: string;
}

const cartItems: CartItem[] = [
{
id: 1,
name: "HS Tshirt",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ export default function CheckoutForm() {
const elements = useWidgets();

const [isSuccess, setIsSuccess] = useState(false);
const [message, setMessage] = useState(null);
const [message, setMessage] = useState<string | null>(null);
const [isProcessing, setIsProcessing] = useState(false);

const clientSecret = getClientSecretFromUrl();

// Handle form submission
const handleSubmit = async (e) => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

// Prevent submission if Hyper isn't ready or already processing
Expand All @@ -52,8 +52,10 @@ export default function CheckoutForm() {
if (status) {
handlePaymentStatus(status, setMessage, setIsSuccess);
}
} catch (err) {
setMessage(`Error confirming payment: ${err.message}`);
} catch (err: unknown) {
const errorMessage =
err instanceof Error ? err.message : "An unknown error occurred";
setMessage(`Error confirming payment: ${errorMessage}`);
} finally {
setIsProcessing(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function Completion() {
<h2 className="ConfirmText">Thanks for your order!</h2>

<p className="ConfirmDes">
Yayyy! You successfully made a payment with Hyperswitch. If its a real
Yayyy! You successfully made a payment with Hyperswitch. If it's a real
store, your items would have been on their way.
</p>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import { HyperElements } from "@juspay-tech/react-hyper-js";
import type { HyperInstance } from "@juspay-tech/hyper-js";
import CheckoutForm from "./CheckoutForm";
import {
getQueryParam,
Expand All @@ -11,10 +12,10 @@ import {
} from "./utils";

function Payment() {
const [hyperPromise, setHyperPromise] = useState(null);
const [hyperPromise, setHyperPromise] = useState<Promise<HyperInstance> | null>(null);
const [clientSecret, setClientSecret] = useState("");
const [paymentId, setPaymentId] = useState("");
const [error, setError] = useState(null);
const [error, setError] = useState<string | null>(null);
const [isScriptLoaded, setIsScriptLoaded] = useState(false);

const isCypressTestMode = getQueryParam("isCypressTestMode") === "true";
Expand Down Expand Up @@ -55,7 +56,7 @@ function Payment() {
if (isMounted) {
setClientSecret(paymentIntentData.clientSecret);
if (SDK_VERSION === "v2") {
setPaymentId(paymentIntentData.paymentId);
setPaymentId(paymentIntentData.paymentId ?? "");
}
setHyperPromise(Promise.resolve(hyper));
}
Expand Down
108 changes: 108 additions & 0 deletions Hyperswitch-React-Demo-App/src/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Webpack DefinePlugin globals (from webpack.common.js)
declare const ENDPOINT: string;
declare const SCRIPT_SRC: string;
declare const SELF_SERVER_URL: string;
declare const SDK_VERSION: string;

// Window augmentation for dynamically loaded Hyper SDK
interface Window {
Hyper: (
options: { publishableKey: string; profileId?: string },
config?: { customBackendUrl?: string }
) => HyperInstance;
}

// Forward-declare HyperInstance at global scope so Window.Hyper can reference it.
// The full definition lives in the @juspay-tech/hyper-js ambient module below.
type HyperInstance = import("@juspay-tech/hyper-js").HyperInstance;

// Asset modules
declare module "*.svg" {
const content: string;
export default content;
}
declare module "*.png" {
const content: string;
export default content;
}
declare module "*.css" {}

// ---------------------------------------------------------------------------
// @juspay-tech/hyper-js — subset used by the demo app
//
// TODO: These ambient module declarations are TEMPORARY. They exist because the
// published npm packages (@juspay-tech/hyper-js, @juspay-tech/react-hyper-js)
// do not yet ship .d.ts files. Once PRs juspay/hyper-js#18 and
// juspay/react-hyper-js#11 are merged and released, DELETE these declare module
// blocks — the real .d.ts files from node_modules will take over automatically.
// ---------------------------------------------------------------------------
declare module "@juspay-tech/hyper-js" {
export interface HyperInstance {
confirmPayment(params: any): Promise<any>;
elements(options: any): Element;
confirmCardPayment(
clientSecret: string,
data?: object,
options?: object
): Promise<object>;
retrievePaymentIntent(paymentIntentId: string): Promise<any>;
widgets(options: any): Element;
paymentRequest(options: object): object;
}

export interface Element {
getElement(componentName: string): any;
update(options: any): void;
fetchUpdates(): Promise<object>;
create(componentType: string, options?: object): any;
}
}

// ---------------------------------------------------------------------------
// @juspay-tech/react-hyper-js — subset used by the demo app
//
// TODO: Same as above — delete this block once juspay/react-hyper-js#11 ships.
// ---------------------------------------------------------------------------
declare module "@juspay-tech/react-hyper-js" {
import type { ReactNode } from "react";
import type { HyperInstance } from "@juspay-tech/hyper-js";

export interface UseHyperReturn {
clientSecret: string;
confirmPayment(params: any): Promise<any>;
confirmCardPayment(
clientSecret: string,
data?: any,
options?: any
): Promise<any>;
retrievePaymentIntent(paymentIntentId: string): Promise<any>;
paymentRequest(options: any): any;
}

export interface UseWidgetsReturn {
options: Record<string, any>;
update(options: any): void;
getElement(componentName: string): any | null;
fetchUpdates(): Promise<any>;
create(componentType: string, options: any): any;
}

export function useHyper(): UseHyperReturn;
export function useWidgets(): UseWidgetsReturn;

export function HyperElements(props: {
hyper: Promise<HyperInstance>;
options: Record<string, any>;
children: ReactNode;
}): JSX.Element | null;

export function PaymentElement(props: {
id?: string;
options?: Record<string, any>;
onChange?: (data?: any) => void;
onReady?: (data?: any) => void;
onFocus?: (data?: any) => void;
onBlur?: (data?: any) => void;
onClick?: (data?: any) => void;
}): JSX.Element | null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("app"));
const root = ReactDOM.createRoot(document.getElementById("app")!);
root.render(
<React.StrictMode>
<App />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import type { HyperInstance } from "@juspay-tech/hyper-js";
import type React from "react";

interface PaymentStatusMessages {
[key: string]: string;
}

export const getPaymentIntentData = async ({
baseUrl,
isCypressTestMode,
clientSecretQueryParam,
setError,
}) => {
}: {
baseUrl: string;
isCypressTestMode: boolean;
clientSecretQueryParam: string | null;
setError: React.Dispatch<React.SetStateAction<string | null>>;
}): Promise<{ clientSecret: string; paymentId?: string } | null> => {
try {
if (isCypressTestMode) {
return { clientSecret: clientSecretQueryParam };
return { clientSecret: clientSecretQueryParam ?? "" };
}

const res = await fetch(`${baseUrl}/create-intent`);
Expand All @@ -20,10 +32,12 @@ export const getPaymentIntentData = async ({
}
};

export const getQueryParam = (param) =>
export const getQueryParam = (param: string): string | null =>
new URLSearchParams(window.location.search).get(param);

export const fetchConfigAndUrls = async (baseUrl) => {
export const fetchConfigAndUrls = async (
baseUrl: string
): Promise<{ configData: any; urlsData: any }> => {
const [configRes, urlsRes] = await Promise.all([
fetch(`${baseUrl}/config`),
fetch(`${baseUrl}/urls`),
Expand All @@ -39,31 +53,39 @@ export const fetchConfigAndUrls = async (baseUrl) => {
return { configData, urlsData };
};

// Cached singleton — avoids creating multiple Hyper instances when the merchant
// (or React strict-mode) triggers loadHyperScript more than once.
let hyperInstance: HyperInstance | null = null;

export const loadHyperScript = ({
clientUrl,
publishableKey,
customBackendUrl,
profileId,
isScriptLoaded,
setIsScriptLoaded,
}) => {
}: {
clientUrl: string;
publishableKey: string;
customBackendUrl?: string;
profileId?: string;
isScriptLoaded: boolean;
setIsScriptLoaded: React.Dispatch<React.SetStateAction<boolean>>;
}): Promise<HyperInstance> => {
return new Promise((resolve, reject) => {
if (isScriptLoaded) return resolve(window.Hyper);
if (isScriptLoaded && hyperInstance) return resolve(hyperInstance);

const script = document.createElement("script");
script.src = `${clientUrl}/HyperLoader.js`;
script.async = true;

script.onload = () => {
setIsScriptLoaded(true);
resolve(
window.Hyper(
{ publishableKey, profileId },
{
customBackendUrl,
}
)
hyperInstance = window.Hyper(
{ publishableKey, profileId },
{ customBackendUrl }
);
resolve(hyperInstance);
};

script.onerror = () => {
Expand All @@ -74,13 +96,17 @@ export const loadHyperScript = ({
});
};

export const getClientSecretFromUrl = () =>
export const getClientSecretFromUrl = (): string | null =>
new URLSearchParams(window.location.search).get(
"payment_intent_client_secret"
);

export const handlePaymentStatus = (status, setMessage, setIsSuccess) => {
const statusMessages = {
export const handlePaymentStatus = (
status: string,
setMessage: React.Dispatch<React.SetStateAction<string | null>>,
setIsSuccess: React.Dispatch<React.SetStateAction<boolean>>
): void => {
const statusMessages: PaymentStatusMessages = {
succeeded: "Payment successful.",
processing: "Your payment is processing.",
requires_payment_method:
Expand Down Expand Up @@ -110,7 +136,7 @@ export const paymentElementOptions = {
},
};

export const hyperOptionsV1 = (clientSecret) => {
export const hyperOptionsV1 = (clientSecret: string) => {
return {
clientSecret,
appearance: {
Expand All @@ -119,7 +145,7 @@ export const hyperOptionsV1 = (clientSecret) => {
};
};

export const hyperOptionsV2 = (clientSecret, paymentId) => {
export const hyperOptionsV2 = (clientSecret: string, paymentId: string) => {
return {
clientSecret,
paymentId,
Expand Down
17 changes: 17 additions & 0 deletions Hyperswitch-React-Demo-App/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Loading
Loading