90 lines
2.9 KiB
React
90 lines
2.9 KiB
React
/**
|
|
* 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;
|
|
};
|