From aefa4afa59f5af49fbc309ff0e1bb3cd079a75a9 Mon Sep 17 00:00:00 2001 From: Kabin Date: Mon, 18 May 2026 19:22:50 +0545 Subject: [PATCH 1/7] feat(stripe): add stripe integration package with payment handling and UI components --- packages/stripe/.gitignore | 7 + packages/stripe/eslint.config.js | 19 +++ packages/stripe/package.json | 79 ++++++++++++ packages/stripe/src/api/axios.ts | 14 ++ packages/stripe/src/api/payment/index.ts | 41 ++++++ .../stripe/src/assets/css/cancelled-page.css | 98 ++++++++++++++ .../stripe/src/assets/css/success-page.css | 109 ++++++++++++++++ .../stripe/src/components/CancelledPage.tsx | 79 ++++++++++++ .../stripe/src/components/SuccessPage.tsx | 77 +++++++++++ packages/stripe/src/constants.ts | 4 + .../stripe/src/context/StripeProvider.tsx | 68 ++++++++++ .../stripe/src/hooks/useBackNavigation.ts | 15 +++ packages/stripe/src/hooks/usePayment.ts | 13 ++ packages/stripe/src/i18n.ts | 7 + packages/stripe/src/index.ts | 21 +++ packages/stripe/src/locales/en/index.ts | 5 + packages/stripe/src/locales/en/stripe.json | 18 +++ packages/stripe/src/locales/fr/index.ts | 5 + packages/stripe/src/locales/fr/stripe.json | 18 +++ packages/stripe/src/locales/index.ts | 7 + packages/stripe/src/routes/index.tsx | 45 +++++++ packages/stripe/src/types/config.ts | 9 ++ packages/stripe/src/types/index.ts | 3 + packages/stripe/src/types/payment.ts | 9 ++ packages/stripe/src/types/router.ts | 11 ++ packages/stripe/stylelint.config.js | 3 + packages/stripe/tsconfig.json | 13 ++ packages/stripe/tsconfig.vitest.json | 9 ++ packages/stripe/vite.config.ts | 52 ++++++++ packages/stripe/vitest.config.ts | 21 +++ pnpm-lock.yaml | 122 +++++++++++++----- 31 files changed, 971 insertions(+), 30 deletions(-) create mode 100644 packages/stripe/.gitignore create mode 100644 packages/stripe/eslint.config.js create mode 100644 packages/stripe/package.json create mode 100644 packages/stripe/src/api/axios.ts create mode 100644 packages/stripe/src/api/payment/index.ts create mode 100644 packages/stripe/src/assets/css/cancelled-page.css create mode 100644 packages/stripe/src/assets/css/success-page.css create mode 100644 packages/stripe/src/components/CancelledPage.tsx create mode 100644 packages/stripe/src/components/SuccessPage.tsx create mode 100644 packages/stripe/src/constants.ts create mode 100644 packages/stripe/src/context/StripeProvider.tsx create mode 100644 packages/stripe/src/hooks/useBackNavigation.ts create mode 100644 packages/stripe/src/hooks/usePayment.ts create mode 100644 packages/stripe/src/i18n.ts create mode 100644 packages/stripe/src/index.ts create mode 100644 packages/stripe/src/locales/en/index.ts create mode 100644 packages/stripe/src/locales/en/stripe.json create mode 100644 packages/stripe/src/locales/fr/index.ts create mode 100644 packages/stripe/src/locales/fr/stripe.json create mode 100644 packages/stripe/src/locales/index.ts create mode 100644 packages/stripe/src/routes/index.tsx create mode 100644 packages/stripe/src/types/config.ts create mode 100644 packages/stripe/src/types/index.ts create mode 100644 packages/stripe/src/types/payment.ts create mode 100644 packages/stripe/src/types/router.ts create mode 100644 packages/stripe/stylelint.config.js create mode 100644 packages/stripe/tsconfig.json create mode 100644 packages/stripe/tsconfig.vitest.json create mode 100644 packages/stripe/vite.config.ts create mode 100644 packages/stripe/vitest.config.ts diff --git a/packages/stripe/.gitignore b/packages/stripe/.gitignore new file mode 100644 index 000000000..3311aa29c --- /dev/null +++ b/packages/stripe/.gitignore @@ -0,0 +1,7 @@ +.env* +!.env.example +**/*.log +coverage +dist +dist-ssr +node_modules diff --git a/packages/stripe/eslint.config.js b/packages/stripe/eslint.config.js new file mode 100644 index 000000000..9fb397fb0 --- /dev/null +++ b/packages/stripe/eslint.config.js @@ -0,0 +1,19 @@ +import reactConfig from "@prefabs.tech/eslint-config/react.js"; + +export default [ + ...reactConfig, + { + rules: { + "unicorn/filename-case": [ + "error", + { + cases: { + camelCase: true, + kebabCase: true, + pascalCase: true, + }, + }, + ], + }, + }, +]; diff --git a/packages/stripe/package.json b/packages/stripe/package.json new file mode 100644 index 000000000..d97c67826 --- /dev/null +++ b/packages/stripe/package.json @@ -0,0 +1,79 @@ +{ + "name": "@prefabs.tech/react-stripe", + "version": "0.72.1", + "description": "React Stripe Plugin", + "type": "module", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/PrefabsTechReactStripe.es.js", + "require": "./dist/PrefabsTechReactStripe.umd.js" + }, + "./dist/PrefabsTechReactStripe.css": "./dist/react-stripe.css" + }, + "main": "./dist/PrefabsTechReactStripe.umd.js", + "module": "./dist/PrefabsTechReactStripe.es.js", + "types": "./dist/src/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "vite build && tsc --emitDeclarationOnly", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "snapshot:update": "vitest --environment jsdom run --update --passWithNoTests", + "sort-package": "npx sort-package-json", + "stylelint": "stylelint \"src/**/*.{css}\" --allow-empty-input", + "stylelint:fix": "stylelint \"src/**/*.{css}\" --fix --allow-empty-input", + "test": "pnpm build && vitest --environment jsdom run --coverage --passWithNoTests", + "test:component": "vitest --environment jsdom run component/ --passWithNoTests", + "test:snapshot": "vitest --environment jsdom run snapshot/ --passWithNoTests", + "test:unit": "vitest --environment jsdom run unit/ --passWithNoTests", + "test:watch": "vitest --environment jsdom", + "typecheck": "tsc --noEmit -p tsconfig.vitest.json --composite false" + }, + "dependencies": {}, + "devDependencies": { + "@prefabs.tech/eslint-config": "0.7.0", + "@prefabs.tech/react-config": "0.72.1", + "@prefabs.tech/react-i18n": "0.72.1", + "@prefabs.tech/react-ui": "0.72.1", + "@prefabs.tech/stylelint-config": "0.7.0", + "@prefabs.tech/tsconfig": "0.7.0", + "@testing-library/dom": "10.4.1", + "@testing-library/react": "16.3.2", + "@testing-library/user-event": "14.6.1", + "@types/jsdom": "21.1.7", + "@types/node": "25.6.0", + "@types/react": "18.3.28", + "@types/react-dom": "18.3.7", + "@vitejs/plugin-react": "5.2.0", + "@vitest/coverage-v8": "3.2.4", + "axios": "1.16.0", + "eslint": "9.39.4", + "jsdom": "27.4.0", + "prettier": "3.8.3", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-router-dom": "6.30.3", + "stylelint": "17.10.0", + "stylelint-config-standard": "40.0.0", + "stylelint-order": "8.1.1", + "typescript": "5.9.3", + "vite": "7.3.2", + "vitest": "3.2.4" + }, + "peerDependencies": { + "@prefabs.tech/react-config": "0.72.1", + "@prefabs.tech/react-i18n": "0.72.1", + "@prefabs.tech/react-ui": "0.72.1", + "axios": "1.16.0", + "react": ">=18.3", + "react-dom": ">=18.3", + "react-router-dom": ">=6.30" + }, + "engines": { + "node": ">=18", + "pnpm": ">=9" + } +} diff --git a/packages/stripe/src/api/axios.ts b/packages/stripe/src/api/axios.ts new file mode 100644 index 000000000..09592ab1a --- /dev/null +++ b/packages/stripe/src/api/axios.ts @@ -0,0 +1,14 @@ +import axios from "axios"; + +const client = (baseURL: string) => { + return axios.create({ + baseURL: baseURL, + headers: { + post: { + "Content-Type": "application/json", + }, + }, + }); +}; + +export default client; diff --git a/packages/stripe/src/api/payment/index.ts b/packages/stripe/src/api/payment/index.ts new file mode 100644 index 000000000..9c53ba222 --- /dev/null +++ b/packages/stripe/src/api/payment/index.ts @@ -0,0 +1,41 @@ +import type { + CheckoutSessionPayload, + PrefabsTechReactStripeConfig, +} from "../../types"; + +import { API_PATH_CHECKOUT_SESSION, API_PATH_STATUS } from "../../constants"; +import client from "../axios"; + +export const checkoutSession = async ( + payload: CheckoutSessionPayload, + apiBaseUrl: string, + config?: PrefabsTechReactStripeConfig, +) => { + const path = config?.apiRoutes?.checkoutSession || API_PATH_CHECKOUT_SESSION; + + const response = await client(apiBaseUrl).post(path, payload); + + if ("error" in response.data) { + throw new Error(response.data); + } + + const redirectUrl = response.data.url as string; + window.location.href = redirectUrl; + + return response.data; +}; + +export const getStatus = async ( + apiBaseUrl: string, + config?: PrefabsTechReactStripeConfig, +) => { + const path = config?.apiRoutes?.status || API_PATH_STATUS; + + const response = await client(apiBaseUrl).get(path); + + if ("error" in response.data) { + throw new Error(response.data); + } + + return response.data; +}; diff --git a/packages/stripe/src/assets/css/cancelled-page.css b/packages/stripe/src/assets/css/cancelled-page.css new file mode 100644 index 000000000..1a0c572fa --- /dev/null +++ b/packages/stripe/src/assets/css/cancelled-page.css @@ -0,0 +1,98 @@ +.cancelled-page .card { + animation: fadeInUp 0.6s ease-out; + display: flex; + flex-direction: column; + gap: 1.5rem; + max-width: 30rem; + padding: 2rem 1.5rem; + text-align: center; + width: 100%; +} + +.cancelled-page .card > .card-body { + display: flex; + flex-direction: column; + gap: 1rem; + min-height: 25rem; +} + +.cancelled-page .crossmark { + margin: 0 auto 0.5rem; + stroke-width: 2; + width: 5rem; +} + +.cancelled-page .crossmark-circle { + animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards; + stroke: #f44336; + stroke-dasharray: 166; + stroke-dashoffset: 166; + stroke-width: 2; +} + +.cancelled-page .crossmark-line { + animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.6s forwards; + stroke: var(--dz-danger-color, #dc3545); + stroke-dasharray: 28; + stroke-dashoffset: 28; + stroke-linecap: round; + stroke-width: 3; + transform-origin: 50% 50%; +} + +.cancelled-page h1.title { + color: var(--dz-secondary-color, #475569); + font-size: 1.5rem; + font-weight: 700; + margin-block: 1rem; +} + +.cancelled-page .message { + color: var(--dz-secondary-color, #475569);; + display: flex; + flex-direction: column; + font-size: 1rem; + gap: 0.5rem; + line-height: 1.5; + margin-block: auto; +} + +.cancelled-page .message p { + margin: 0; +} + +.cancelled-page button { + min-width: 12rem; +} + +@keyframes stroke { + 100% { + stroke-dashoffset: 0; + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (width >= 576px) { + .cancelled-page .card { + padding: 3rem 2rem; + } + + .cancelled-page .crossmark { + width: 7rem; + } + + .cancelled-page h1.title { + font-size: 2rem; + } +} diff --git a/packages/stripe/src/assets/css/success-page.css b/packages/stripe/src/assets/css/success-page.css new file mode 100644 index 000000000..7fb7188ac --- /dev/null +++ b/packages/stripe/src/assets/css/success-page.css @@ -0,0 +1,109 @@ +.success-page .card { + animation: fadeInUp 0.6s ease-out; + display: flex; + flex-direction: column; + gap: 1.5rem; + max-width: 30rem; + padding: 2rem 1.5rem; + text-align: center; + width: 100%; +} + +.success-page .card > .card-body { + display: flex; + flex-direction: column; + gap: 1rem; + min-height: 25rem; +} + +.success-page .checkmark { + margin: 0 auto 0.5rem; + stroke-width: 2; + width: 5rem; +} + +.success-page .checkmark-circle { + animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards; + stroke: var(--dz-success-color, #22c55e); + stroke-dasharray: 166; + stroke-dashoffset: 166; + stroke-width: 2; +} + +.success-page .checkmark-check { + animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.6s forwards; + stroke: var(--dz-success-color, #22c55e); + stroke-dasharray: 48; + stroke-dashoffset: 48; + stroke-linecap: round; + stroke-width: 3; + transform-origin: 50% 50%; +} + +.success-page h1.title { + color: var(--dz-secondary-color, #475569);; + font-size: 1.5rem; + font-weight: 700; + margin-block: 1rem; +} + +.success-page p.subtitle { + color: #4caf50; + font-size: 1.125rem; + font-weight: 600; + margin: 0; +} + +.success-page .message { + color: #666; + display: flex; + flex-direction: column; + font-size: 1rem; + gap: 0.5rem; + line-height: 1.5; + margin-block: auto; +} + +.success-page .message p { + margin: 0; +} + +.success-page button { + min-width: 12rem; +} + +@keyframes stroke { + 100% { + stroke-dashoffset: 0; + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (width >= 576px) { + .success-page .card { + padding: 3rem 2rem; + } + + .success-page .checkmark { + width: 7rem; + } + + .success-page h1.title { + font-size: 2rem; + } + + .success-page p.subtitle { + font-size: 1.25rem; + } +} diff --git a/packages/stripe/src/components/CancelledPage.tsx b/packages/stripe/src/components/CancelledPage.tsx new file mode 100644 index 000000000..5b5d1c46f --- /dev/null +++ b/packages/stripe/src/components/CancelledPage.tsx @@ -0,0 +1,79 @@ +import type { ReactNode } from "react"; + +import { useTranslation } from "@prefabs.tech/react-i18n"; +import { Button, Card, LoadingPage, Page } from "@prefabs.tech/react-ui"; + +import { useBackNavigation } from "../hooks/useBackNavigation"; +import "../assets/css/cancelled-page.css"; + +interface CancelledPageProperties { + actions?: ReactNode; + children?: ReactNode; + loading?: boolean; + onBack?: () => void; + subtitle?: ReactNode; + title?: ReactNode; +} + +const CancelledPage: React.FC = ({ + actions, + children, + loading = false, + onBack, + subtitle, + title, +}) => { + const { t } = useTranslation("stripe"); + const { handleBack } = useBackNavigation(onBack); + + return ( + + {!loading && ( + + + + + + + + {title ||

{t("payment.cancelled.heading")}

} + + {subtitle} + + {children || ( +
+

{t("payment.cancelled.message")}

+
+ )} + + {actions || ( +