feat: integrate Stripe payment module with API + UI components
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* PaymentProvider — module-local state, fully independent from the app's global
|
||||
* mockStore so the Payments module can be added/removed in isolation.
|
||||
*
|
||||
* Transaction history is sourced from two places and merged by id:
|
||||
* 1. The server (api/payments/transactions → Vercel KV, written by the
|
||||
* webhook) — the production source of truth.
|
||||
* 2. A local record (localStorage) written when a customer returns to the
|
||||
* success page — gives instant feedback and works without KV in the demo.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { INVOICES } from '../data/invoices';
|
||||
import { stripeConfig } from '../config/stripeConfig';
|
||||
|
||||
const STORAGE_KEY = 'lynkeduppro.payments.transactions';
|
||||
|
||||
const loadLocal = () => {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const persistLocal = (txns) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(txns));
|
||||
} catch {
|
||||
/* localStorage unavailable (private mode) — keep in-memory only. */
|
||||
}
|
||||
};
|
||||
|
||||
const mergeById = (a, b) => {
|
||||
const map = new Map();
|
||||
[...a, ...b].forEach((t) => {
|
||||
if (t && t.id) map.set(t.id, { ...map.get(t.id), ...t });
|
||||
});
|
||||
return Array.from(map.values()).sort(
|
||||
(x, y) => new Date(y.createdAt || 0) - new Date(x.createdAt || 0)
|
||||
);
|
||||
};
|
||||
|
||||
const PaymentContext = createContext(null);
|
||||
|
||||
export const PaymentProvider = ({ children }) => {
|
||||
const [invoices] = useState(INVOICES);
|
||||
const [transactions, setTransactions] = useState(loadLocal);
|
||||
|
||||
// Pull the server-side (webhook-written) history and merge it in.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(`${stripeConfig.apiBase}/transactions`);
|
||||
if (!res.ok) return;
|
||||
const { transactions: server = [] } = await res.json();
|
||||
if (cancelled || !server.length) return;
|
||||
setTransactions((prev) => mergeById(prev, server));
|
||||
} catch {
|
||||
/* offline / endpoint unavailable — local history is fine. */
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
/** Record a transaction confirmed on the success return page. */
|
||||
const recordTransaction = useCallback((txn) => {
|
||||
if (!txn?.id) return;
|
||||
setTransactions((prev) => {
|
||||
const next = mergeById(prev, [txn]);
|
||||
persistLocal(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const value = { invoices, transactions, recordTransaction };
|
||||
|
||||
return <PaymentContext.Provider value={value}>{children}</PaymentContext.Provider>;
|
||||
};
|
||||
|
||||
export const usePayments = () => {
|
||||
const ctx = useContext(PaymentContext);
|
||||
if (!ctx) throw new Error('usePayments must be used within a <PaymentProvider>.');
|
||||
return ctx;
|
||||
};
|
||||
Reference in New Issue
Block a user