@unseen_fi/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components.d.ts +16 -0
- package/dist/components.js +306 -0
- package/dist/context.d.ts +31 -0
- package/dist/context.js +40 -0
- package/dist/hooks.d.ts +28 -0
- package/dist/hooks.js +87 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +3 -0
- package/dist/wallets.d.ts +3 -0
- package/dist/wallets.js +39 -0
- package/package.json +24 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { UnseenPayButtonProps } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* A drop-in "Pay with Unseen" button that opens a checkout modal.
|
|
4
|
+
* The modal shows wallet picker, QR code / deeplinks, and "I have paid" confirmation.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <UnseenPayButton
|
|
9
|
+
* amount={50_000_000}
|
|
10
|
+
* reference="order_123"
|
|
11
|
+
* description="Premium Plan"
|
|
12
|
+
* onSuccess={(p) => console.log("Paid!", p.id)}
|
|
13
|
+
* />
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function UnseenPayButton({ amount, mint, reference, description, expiresIn, onSuccess, onError, onCreated, label, className, disabled, }: UnseenPayButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UnseenPayButton = UnseenPayButton;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const context_1 = require("./context");
|
|
7
|
+
const hooks_1 = require("./hooks");
|
|
8
|
+
const wallets_1 = require("./wallets");
|
|
9
|
+
// ─── UnseenPayButton ─────────────────────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* A drop-in "Pay with Unseen" button that opens a checkout modal.
|
|
12
|
+
* The modal shows wallet picker, QR code / deeplinks, and "I have paid" confirmation.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <UnseenPayButton
|
|
17
|
+
* amount={50_000_000}
|
|
18
|
+
* reference="order_123"
|
|
19
|
+
* description="Premium Plan"
|
|
20
|
+
* onSuccess={(p) => console.log("Paid!", p.id)}
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function UnseenPayButton({ amount, mint, reference, description, expiresIn, onSuccess, onError, onCreated, label = "Pay with Unseen", className, disabled, }) {
|
|
25
|
+
const [modalOpen, setModalOpen] = (0, react_1.useState)(false);
|
|
26
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("button", { className: className, style: className ? undefined : defaultButtonStyle, onClick: () => setModalOpen(true), disabled: disabled, children: [(0, jsx_runtime_1.jsx)(ShieldIcon, {}), label] }), modalOpen && ((0, jsx_runtime_1.jsx)(UnseenModal, { amount: amount, mint: mint, reference: reference, description: description, expiresIn: expiresIn, onSuccess: (p) => {
|
|
27
|
+
onSuccess?.(p);
|
|
28
|
+
setModalOpen(false);
|
|
29
|
+
}, onError: onError, onCreated: onCreated, onClose: () => setModalOpen(false) }))] }));
|
|
30
|
+
}
|
|
31
|
+
function UnseenModal({ amount, mint, reference, description, expiresIn, onSuccess, onError, onCreated, onClose, }) {
|
|
32
|
+
const { baseUrl } = (0, context_1.useUnseenConfig)();
|
|
33
|
+
const { payment, createPayment } = (0, hooks_1.usePayment)();
|
|
34
|
+
const { verify } = (0, hooks_1.useVerify)();
|
|
35
|
+
const [step, setStep] = (0, react_1.useState)("creating");
|
|
36
|
+
const [errorMsg, setErrorMsg] = (0, react_1.useState)("");
|
|
37
|
+
const [selectedWallet, setSelectedWallet] = (0, react_1.useState)(null);
|
|
38
|
+
const [verifyCountdown, setVerifyCountdown] = (0, react_1.useState)(60);
|
|
39
|
+
// ─── Create payment on mount ────────────────────────────────────────────
|
|
40
|
+
(0, react_1.useEffect)(() => {
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
createPayment({ amount, reference, description, mint, expiresIn })
|
|
43
|
+
.then((p) => {
|
|
44
|
+
if (cancelled)
|
|
45
|
+
return;
|
|
46
|
+
onCreated?.(p);
|
|
47
|
+
setStep("wallet-picker");
|
|
48
|
+
})
|
|
49
|
+
.catch((err) => {
|
|
50
|
+
if (cancelled)
|
|
51
|
+
return;
|
|
52
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
53
|
+
setErrorMsg(e.message);
|
|
54
|
+
setStep("error");
|
|
55
|
+
onError?.(e);
|
|
56
|
+
});
|
|
57
|
+
return () => { cancelled = true; };
|
|
58
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
59
|
+
}, []);
|
|
60
|
+
// ─── Handle wallet selection ────────────────────────────────────────────
|
|
61
|
+
const handleWalletSelect = (0, react_1.useCallback)((wallet) => {
|
|
62
|
+
if (!payment)
|
|
63
|
+
return;
|
|
64
|
+
setSelectedWallet(wallet);
|
|
65
|
+
// Open the checkout page in the wallet's in-app browser via deeplink
|
|
66
|
+
const checkoutUrl = `${baseUrl}/pay/${payment.id}`;
|
|
67
|
+
const deeplink = wallet.deeplink(checkoutUrl);
|
|
68
|
+
window.open(deeplink, "_blank");
|
|
69
|
+
setStep("waiting");
|
|
70
|
+
}, [payment, baseUrl]);
|
|
71
|
+
// ─── Handle "I have paid" verification ──────────────────────────────────
|
|
72
|
+
const handleVerify = (0, react_1.useCallback)(async () => {
|
|
73
|
+
if (!payment)
|
|
74
|
+
return;
|
|
75
|
+
setStep("verifying");
|
|
76
|
+
setVerifyCountdown(60);
|
|
77
|
+
try {
|
|
78
|
+
// Keep verifying for up to 60s to improve UX and reduce manual retries.
|
|
79
|
+
const startedAt = Date.now();
|
|
80
|
+
while (Date.now() - startedAt < 60000) {
|
|
81
|
+
const result = await verify(payment.id);
|
|
82
|
+
if (result.status === "confirmed") {
|
|
83
|
+
setStep("confirmed");
|
|
84
|
+
onSuccess?.({
|
|
85
|
+
...payment,
|
|
86
|
+
status: "confirmed",
|
|
87
|
+
txSignature: result.txSignature ?? null,
|
|
88
|
+
txSignatures: result.txSignatures ?? [],
|
|
89
|
+
confirmedAt: result.confirmedAt ?? null,
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
94
|
+
}
|
|
95
|
+
setErrorMsg("Still waiting for confirmation. Please try again in a few seconds.");
|
|
96
|
+
setStep("waiting");
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
100
|
+
setErrorMsg(e.message);
|
|
101
|
+
setStep("error");
|
|
102
|
+
onError?.(e);
|
|
103
|
+
}
|
|
104
|
+
}, [payment, verify, onSuccess, onError]);
|
|
105
|
+
(0, react_1.useEffect)(() => {
|
|
106
|
+
if (step !== "verifying")
|
|
107
|
+
return;
|
|
108
|
+
const timer = setInterval(() => {
|
|
109
|
+
setVerifyCountdown((prev) => (prev > 0 ? prev - 1 : 0));
|
|
110
|
+
}, 1000);
|
|
111
|
+
return () => clearInterval(timer);
|
|
112
|
+
}, [step]);
|
|
113
|
+
// ─── Render ─────────────────────────────────────────────────────────────
|
|
114
|
+
return ((0, jsx_runtime_1.jsx)("div", { style: overlayStyle, onClick: onClose, children: (0, jsx_runtime_1.jsxs)("div", { style: modalStyle, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("div", { style: modalHeaderStyle, children: [(0, jsx_runtime_1.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [(0, jsx_runtime_1.jsx)(ShieldIcon, { size: 20 }), (0, jsx_runtime_1.jsx)("span", { style: { fontWeight: 600, fontSize: "16px", color: "#fff" }, children: "Unseen Pay" })] }), (0, jsx_runtime_1.jsx)("button", { style: closeButtonStyle, onClick: onClose, children: "\u2715" })] }), (0, jsx_runtime_1.jsxs)("div", { style: modalBodyStyle, children: [step === "creating" && ((0, jsx_runtime_1.jsxs)(StepContent, { children: [(0, jsx_runtime_1.jsx)(Spinner, {}), (0, jsx_runtime_1.jsx)(StepText, { children: "Creating payment session..." })] })), step === "wallet-picker" && ((0, jsx_runtime_1.jsxs)(StepContent, { children: [(0, jsx_runtime_1.jsx)(StepText, { children: "Choose your wallet" }), (0, jsx_runtime_1.jsx)(StepHint, { children: "Select a wallet to complete the private payment." }), (0, jsx_runtime_1.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: "8px", width: "100%", marginTop: "8px" }, children: wallets_1.WALLETS.map((w) => ((0, jsx_runtime_1.jsxs)("button", { style: walletButtonStyle, onClick: () => handleWalletSelect(w), children: [(0, jsx_runtime_1.jsx)("div", { style: walletIconStyle, children: (0, jsx_runtime_1.jsx)("img", { src: w.icon, alt: `${w.name} logo`, style: walletLogoImageStyle }) }), (0, jsx_runtime_1.jsx)("span", { style: { fontWeight: 500, color: "#fff" }, children: w.name }), (0, jsx_runtime_1.jsx)("span", { style: { marginLeft: "auto", color: "rgba(255,255,255,0.3)", fontSize: "13px" }, children: "\u2192" })] }, w.id))) })] })), step === "waiting" && ((0, jsx_runtime_1.jsxs)(StepContent, { children: [(0, jsx_runtime_1.jsxs)("div", { style: walletBadgeStyle, children: [selectedWallet && ((0, jsx_runtime_1.jsx)("div", { style: { ...walletIconStyle, width: "24px", height: "24px" }, children: (0, jsx_runtime_1.jsx)("img", { src: selectedWallet.icon, alt: `${selectedWallet.name} logo`, style: walletLogoImageStyle }) })), (0, jsx_runtime_1.jsxs)("span", { style: { fontSize: "13px", color: "rgba(255,255,255,0.5)" }, children: ["Opened in ", selectedWallet?.name ?? "wallet"] })] }), (0, jsx_runtime_1.jsx)(StepText, { children: "Complete payment in your wallet" }), (0, jsx_runtime_1.jsx)(StepHint, { children: "After signing the transaction in your wallet, come back here and confirm." }), errorMsg && ((0, jsx_runtime_1.jsx)("div", { style: inlineErrorStyle, children: errorMsg })), (0, jsx_runtime_1.jsx)("button", { style: confirmButtonStyle, onClick: handleVerify, children: "I have paid" }), (0, jsx_runtime_1.jsx)("button", { style: linkButtonStyle, onClick: () => { setStep("wallet-picker"); setErrorMsg(""); }, children: "Use a different wallet" })] })), step === "verifying" && ((0, jsx_runtime_1.jsxs)(StepContent, { children: [(0, jsx_runtime_1.jsx)(Spinner, {}), (0, jsx_runtime_1.jsx)(StepText, { children: "Verifying on Solana..." }), (0, jsx_runtime_1.jsxs)(StepHint, { children: ["Checking your transaction on-chain (", verifyCountdown, "s)."] })] })), step === "confirmed" && ((0, jsx_runtime_1.jsxs)(StepContent, { children: [(0, jsx_runtime_1.jsx)("div", { style: successCircle, children: (0, jsx_runtime_1.jsx)("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "#22c55e", strokeWidth: "2.5", children: (0, jsx_runtime_1.jsx)("polyline", { points: "20 6 9 17 4 12" }) }) }), (0, jsx_runtime_1.jsx)(StepText, { style: { color: "#22c55e" }, children: "Payment Confirmed!" }), (0, jsx_runtime_1.jsx)(StepHint, { children: "Your transaction has been verified on Solana." }), (0, jsx_runtime_1.jsx)("button", { style: confirmButtonStyle, onClick: onClose, children: "Done" })] })), step === "error" && ((0, jsx_runtime_1.jsxs)(StepContent, { children: [(0, jsx_runtime_1.jsx)("div", { style: errorCircle, children: (0, jsx_runtime_1.jsxs)("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#ef4444", strokeWidth: "2", children: [(0, jsx_runtime_1.jsx)("circle", { cx: "12", cy: "12", r: "10" }), (0, jsx_runtime_1.jsx)("line", { x1: "15", y1: "9", x2: "9", y2: "15" }), (0, jsx_runtime_1.jsx)("line", { x1: "9", y1: "9", x2: "15", y2: "15" })] }) }), (0, jsx_runtime_1.jsx)(StepText, { style: { color: "#ef4444" }, children: "Error" }), (0, jsx_runtime_1.jsx)(StepHint, { children: errorMsg }), (0, jsx_runtime_1.jsx)("button", { style: confirmButtonStyle, onClick: () => { setStep("wallet-picker"); setErrorMsg(""); }, children: "Try Again" })] }))] }), (0, jsx_runtime_1.jsx)("div", { style: modalFooterStyle, children: (0, jsx_runtime_1.jsx)("span", { style: { fontSize: "11px", color: "rgba(255,255,255,0.2)" }, children: "Powered by Unseen Finance \u2022 Privacy-preserving payments" }) })] }) }));
|
|
115
|
+
}
|
|
116
|
+
// ─── Sub-components ──────────────────────────────────────────────────────────
|
|
117
|
+
function StepContent({ children }) {
|
|
118
|
+
return (0, jsx_runtime_1.jsx)("div", { style: stepContainerStyle, children: children });
|
|
119
|
+
}
|
|
120
|
+
function StepText({ children, style }) {
|
|
121
|
+
return (0, jsx_runtime_1.jsx)("p", { style: { ...stepTextBase, ...style }, children: children });
|
|
122
|
+
}
|
|
123
|
+
function StepHint({ children }) {
|
|
124
|
+
return (0, jsx_runtime_1.jsx)("p", { style: hintStyle, children: children });
|
|
125
|
+
}
|
|
126
|
+
function Spinner() {
|
|
127
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("style", { children: `@keyframes unseen-spin { to { transform: rotate(360deg); } }` }), (0, jsx_runtime_1.jsx)("div", { style: spinnerStyle })] }));
|
|
128
|
+
}
|
|
129
|
+
function ShieldIcon({ size = 16 }) {
|
|
130
|
+
return ((0, jsx_runtime_1.jsx)("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", children: (0, jsx_runtime_1.jsx)("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }) }));
|
|
131
|
+
}
|
|
132
|
+
// ─── Styles ──────────────────────────────────────────────────────────────────
|
|
133
|
+
const defaultButtonStyle = {
|
|
134
|
+
display: "inline-flex",
|
|
135
|
+
alignItems: "center",
|
|
136
|
+
gap: "8px",
|
|
137
|
+
padding: "12px 24px",
|
|
138
|
+
border: "none",
|
|
139
|
+
borderRadius: "12px",
|
|
140
|
+
background: "linear-gradient(135deg, #7b2fff 0%, #6020cc 100%)",
|
|
141
|
+
color: "#fff",
|
|
142
|
+
fontSize: "15px",
|
|
143
|
+
fontWeight: 600,
|
|
144
|
+
cursor: "pointer",
|
|
145
|
+
fontFamily: "'Inter', system-ui, sans-serif",
|
|
146
|
+
transition: "all 0.2s ease",
|
|
147
|
+
};
|
|
148
|
+
const overlayStyle = {
|
|
149
|
+
position: "fixed",
|
|
150
|
+
inset: 0,
|
|
151
|
+
background: "rgba(0,0,0,0.7)",
|
|
152
|
+
backdropFilter: "blur(8px)",
|
|
153
|
+
display: "flex",
|
|
154
|
+
alignItems: "center",
|
|
155
|
+
justifyContent: "center",
|
|
156
|
+
zIndex: 999999,
|
|
157
|
+
fontFamily: "'Inter', system-ui, sans-serif",
|
|
158
|
+
color: "#fff",
|
|
159
|
+
padding: "16px",
|
|
160
|
+
};
|
|
161
|
+
const modalStyle = {
|
|
162
|
+
background: "rgba(18,16,31,0.98)",
|
|
163
|
+
border: "1px solid rgba(255,255,255,0.08)",
|
|
164
|
+
borderRadius: "20px",
|
|
165
|
+
width: "100%",
|
|
166
|
+
maxWidth: "400px",
|
|
167
|
+
overflow: "hidden",
|
|
168
|
+
};
|
|
169
|
+
const modalHeaderStyle = {
|
|
170
|
+
display: "flex",
|
|
171
|
+
justifyContent: "space-between",
|
|
172
|
+
alignItems: "center",
|
|
173
|
+
padding: "16px 20px",
|
|
174
|
+
borderBottom: "1px solid rgba(255,255,255,0.06)",
|
|
175
|
+
};
|
|
176
|
+
const closeButtonStyle = {
|
|
177
|
+
background: "none",
|
|
178
|
+
border: "none",
|
|
179
|
+
color: "rgba(255,255,255,0.4)",
|
|
180
|
+
fontSize: "16px",
|
|
181
|
+
cursor: "pointer",
|
|
182
|
+
padding: "4px 8px",
|
|
183
|
+
borderRadius: "6px",
|
|
184
|
+
};
|
|
185
|
+
const modalBodyStyle = {
|
|
186
|
+
padding: "24px 20px",
|
|
187
|
+
minHeight: "200px",
|
|
188
|
+
};
|
|
189
|
+
const modalFooterStyle = {
|
|
190
|
+
padding: "12px 20px",
|
|
191
|
+
borderTop: "1px solid rgba(255,255,255,0.04)",
|
|
192
|
+
textAlign: "center",
|
|
193
|
+
};
|
|
194
|
+
const stepContainerStyle = {
|
|
195
|
+
display: "flex",
|
|
196
|
+
flexDirection: "column",
|
|
197
|
+
alignItems: "center",
|
|
198
|
+
gap: "12px",
|
|
199
|
+
};
|
|
200
|
+
const stepTextBase = {
|
|
201
|
+
fontSize: "16px",
|
|
202
|
+
fontWeight: 600,
|
|
203
|
+
color: "#fff",
|
|
204
|
+
margin: 0,
|
|
205
|
+
};
|
|
206
|
+
const hintStyle = {
|
|
207
|
+
fontSize: "13px",
|
|
208
|
+
color: "rgba(255,255,255,0.35)",
|
|
209
|
+
textAlign: "center",
|
|
210
|
+
lineHeight: 1.6,
|
|
211
|
+
margin: 0,
|
|
212
|
+
maxWidth: "280px",
|
|
213
|
+
};
|
|
214
|
+
const spinnerStyle = {
|
|
215
|
+
width: "32px",
|
|
216
|
+
height: "32px",
|
|
217
|
+
border: "3px solid rgba(255,255,255,0.1)",
|
|
218
|
+
borderTopColor: "#7b2fff",
|
|
219
|
+
borderRadius: "50%",
|
|
220
|
+
animation: "unseen-spin 0.8s linear infinite",
|
|
221
|
+
};
|
|
222
|
+
const walletButtonStyle = {
|
|
223
|
+
display: "flex",
|
|
224
|
+
alignItems: "center",
|
|
225
|
+
gap: "12px",
|
|
226
|
+
padding: "14px 16px",
|
|
227
|
+
background: "rgba(255,255,255,0.04)",
|
|
228
|
+
border: "1px solid rgba(255,255,255,0.06)",
|
|
229
|
+
borderRadius: "12px",
|
|
230
|
+
cursor: "pointer",
|
|
231
|
+
transition: "all 0.15s ease",
|
|
232
|
+
width: "100%",
|
|
233
|
+
textAlign: "left",
|
|
234
|
+
fontSize: "15px",
|
|
235
|
+
color: "#fff",
|
|
236
|
+
fontFamily: "inherit",
|
|
237
|
+
};
|
|
238
|
+
const walletIconStyle = {
|
|
239
|
+
width: "32px",
|
|
240
|
+
height: "32px",
|
|
241
|
+
borderRadius: "8px",
|
|
242
|
+
overflow: "hidden",
|
|
243
|
+
flexShrink: 0,
|
|
244
|
+
};
|
|
245
|
+
const walletLogoImageStyle = {
|
|
246
|
+
width: "100%",
|
|
247
|
+
height: "100%",
|
|
248
|
+
objectFit: "cover",
|
|
249
|
+
display: "block",
|
|
250
|
+
};
|
|
251
|
+
const walletBadgeStyle = {
|
|
252
|
+
display: "flex",
|
|
253
|
+
alignItems: "center",
|
|
254
|
+
gap: "8px",
|
|
255
|
+
background: "rgba(255,255,255,0.04)",
|
|
256
|
+
borderRadius: "10px",
|
|
257
|
+
padding: "8px 14px",
|
|
258
|
+
};
|
|
259
|
+
const confirmButtonStyle = {
|
|
260
|
+
width: "100%",
|
|
261
|
+
padding: "14px 24px",
|
|
262
|
+
border: "none",
|
|
263
|
+
borderRadius: "14px",
|
|
264
|
+
background: "linear-gradient(135deg, #7b2fff 0%, #6020cc 100%)",
|
|
265
|
+
color: "#fff",
|
|
266
|
+
fontSize: "15px",
|
|
267
|
+
fontWeight: 600,
|
|
268
|
+
cursor: "pointer",
|
|
269
|
+
fontFamily: "inherit",
|
|
270
|
+
marginTop: "8px",
|
|
271
|
+
};
|
|
272
|
+
const linkButtonStyle = {
|
|
273
|
+
background: "none",
|
|
274
|
+
border: "none",
|
|
275
|
+
color: "rgba(255,255,255,0.35)",
|
|
276
|
+
fontSize: "13px",
|
|
277
|
+
cursor: "pointer",
|
|
278
|
+
padding: "4px",
|
|
279
|
+
fontFamily: "inherit",
|
|
280
|
+
textDecoration: "underline",
|
|
281
|
+
};
|
|
282
|
+
const inlineErrorStyle = {
|
|
283
|
+
fontSize: "13px",
|
|
284
|
+
color: "#ef4444",
|
|
285
|
+
textAlign: "center",
|
|
286
|
+
background: "rgba(239,68,68,0.08)",
|
|
287
|
+
border: "1px solid rgba(239,68,68,0.15)",
|
|
288
|
+
borderRadius: "8px",
|
|
289
|
+
padding: "8px 12px",
|
|
290
|
+
width: "100%",
|
|
291
|
+
};
|
|
292
|
+
const successCircle = {
|
|
293
|
+
width: "56px",
|
|
294
|
+
height: "56px",
|
|
295
|
+
borderRadius: "50%",
|
|
296
|
+
background: "rgba(34,197,94,0.1)",
|
|
297
|
+
border: "1px solid rgba(34,197,94,0.2)",
|
|
298
|
+
display: "flex",
|
|
299
|
+
alignItems: "center",
|
|
300
|
+
justifyContent: "center",
|
|
301
|
+
};
|
|
302
|
+
const errorCircle = {
|
|
303
|
+
...successCircle,
|
|
304
|
+
background: "rgba(239,68,68,0.1)",
|
|
305
|
+
border: "1px solid rgba(239,68,68,0.2)",
|
|
306
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { UnseenProviderConfig } from "./types";
|
|
3
|
+
interface UnseenContextValue {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Provides Unseen Pay configuration to all child components.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { UnseenProvider, UnseenPayButton } from "@unseen/ui";
|
|
13
|
+
*
|
|
14
|
+
* function App() {
|
|
15
|
+
* return (
|
|
16
|
+
* <UnseenProvider apiKey="usk_test_...">
|
|
17
|
+
* <UnseenPayButton
|
|
18
|
+
* amount={50_000_000}
|
|
19
|
+
* reference="order_123"
|
|
20
|
+
* onSuccess={(p) => console.log("Paid!", p)}
|
|
21
|
+
* />
|
|
22
|
+
* </UnseenProvider>
|
|
23
|
+
* );
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function UnseenProvider({ apiKey, baseUrl, children, }: UnseenProviderConfig & {
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
export declare function useUnseenConfig(): UnseenContextValue;
|
|
31
|
+
export {};
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UnseenProvider = UnseenProvider;
|
|
4
|
+
exports.useUnseenConfig = useUnseenConfig;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const UnseenContext = (0, react_1.createContext)(null);
|
|
8
|
+
// ─── Provider ────────────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Provides Unseen Pay configuration to all child components.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { UnseenProvider, UnseenPayButton } from "@unseen/ui";
|
|
15
|
+
*
|
|
16
|
+
* function App() {
|
|
17
|
+
* return (
|
|
18
|
+
* <UnseenProvider apiKey="usk_test_...">
|
|
19
|
+
* <UnseenPayButton
|
|
20
|
+
* amount={50_000_000}
|
|
21
|
+
* reference="order_123"
|
|
22
|
+
* onSuccess={(p) => console.log("Paid!", p)}
|
|
23
|
+
* />
|
|
24
|
+
* </UnseenProvider>
|
|
25
|
+
* );
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function UnseenProvider({ apiKey, baseUrl = "https://unseen.finance", children, }) {
|
|
30
|
+
return ((0, jsx_runtime_1.jsx)(UnseenContext.Provider, { value: { apiKey, baseUrl }, children: children }));
|
|
31
|
+
}
|
|
32
|
+
// ─── Hook ────────────────────────────────────────────────────────────────────
|
|
33
|
+
function useUnseenConfig() {
|
|
34
|
+
const ctx = (0, react_1.useContext)(UnseenContext);
|
|
35
|
+
if (!ctx) {
|
|
36
|
+
throw new Error("useUnseenConfig must be used inside <UnseenProvider>. " +
|
|
37
|
+
"Wrap your component tree with <UnseenProvider>.");
|
|
38
|
+
}
|
|
39
|
+
return ctx;
|
|
40
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PaymentResult, PaymentStatus } from "./types";
|
|
2
|
+
interface UsePaymentReturn {
|
|
3
|
+
payment: PaymentResult | null;
|
|
4
|
+
loading: boolean;
|
|
5
|
+
error: Error | null;
|
|
6
|
+
createPayment: (input: {
|
|
7
|
+
amount: number;
|
|
8
|
+
reference: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
mint?: string;
|
|
11
|
+
expiresIn?: number;
|
|
12
|
+
}) => Promise<PaymentResult>;
|
|
13
|
+
}
|
|
14
|
+
export declare function usePayment(): UsePaymentReturn;
|
|
15
|
+
interface UseVerifyReturn {
|
|
16
|
+
status: PaymentStatus | "amount_mismatch" | null;
|
|
17
|
+
loading: boolean;
|
|
18
|
+
error: Error | null;
|
|
19
|
+
verify: (paymentId: string, paymentToken?: string) => Promise<{
|
|
20
|
+
status: string;
|
|
21
|
+
txSignature?: string;
|
|
22
|
+
txSignatures?: string[];
|
|
23
|
+
confirmedAt?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
export declare function useVerify(): UseVerifyReturn;
|
|
28
|
+
export {};
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.usePayment = usePayment;
|
|
4
|
+
exports.useVerify = useVerify;
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const context_1 = require("./context");
|
|
7
|
+
function usePayment() {
|
|
8
|
+
const { apiKey, baseUrl } = (0, context_1.useUnseenConfig)();
|
|
9
|
+
const [payment, setPayment] = (0, react_1.useState)(null);
|
|
10
|
+
const [loading, setLoading] = (0, react_1.useState)(false);
|
|
11
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
12
|
+
const createPayment = (0, react_1.useCallback)(async (input) => {
|
|
13
|
+
setLoading(true);
|
|
14
|
+
setError(null);
|
|
15
|
+
try {
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
throw new Error("UnseenProvider apiKey is required to create payments from browser. " +
|
|
18
|
+
"For production, create payments server-side and pass paymentToken to verification.");
|
|
19
|
+
}
|
|
20
|
+
const res = await fetch(`${baseUrl}/api/v1/payments`, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${apiKey}`,
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify(input),
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const data = (await res.json());
|
|
30
|
+
throw new Error(String(data.error ?? "Failed to create payment"));
|
|
31
|
+
}
|
|
32
|
+
const result = (await res.json());
|
|
33
|
+
setPayment(result);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
38
|
+
setError(e);
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}, [apiKey, baseUrl]);
|
|
45
|
+
return { payment, loading, error, createPayment };
|
|
46
|
+
}
|
|
47
|
+
function useVerify() {
|
|
48
|
+
const { apiKey, baseUrl } = (0, context_1.useUnseenConfig)();
|
|
49
|
+
const [status, setStatus] = (0, react_1.useState)(null);
|
|
50
|
+
const [loading, setLoading] = (0, react_1.useState)(false);
|
|
51
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
52
|
+
const verify = (0, react_1.useCallback)(async (paymentId, paymentToken) => {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
setError(null);
|
|
55
|
+
try {
|
|
56
|
+
const usePublicVerify = Boolean(paymentToken);
|
|
57
|
+
const url = usePublicVerify
|
|
58
|
+
? `${baseUrl}/api/public/payments/${paymentId}/verify`
|
|
59
|
+
: `${baseUrl}/api/v1/payments/${paymentId}/verify`;
|
|
60
|
+
if (!usePublicVerify && !apiKey) {
|
|
61
|
+
throw new Error("Missing apiKey for merchant verify endpoint. " +
|
|
62
|
+
"Provide paymentToken to use public checkout verification.");
|
|
63
|
+
}
|
|
64
|
+
const res = await fetch(url, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
...(paymentToken
|
|
69
|
+
? { "x-unseen-payment-token": paymentToken }
|
|
70
|
+
: { Authorization: `Bearer ${apiKey}` }),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const data = (await res.json());
|
|
74
|
+
setStatus(data.status);
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
79
|
+
setError(e);
|
|
80
|
+
throw e;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
setLoading(false);
|
|
84
|
+
}
|
|
85
|
+
}, [apiKey, baseUrl]);
|
|
86
|
+
return { status, loading, error, verify };
|
|
87
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { UnseenPayButton } from "./components";
|
|
2
|
+
export { UnseenProvider } from "./context";
|
|
3
|
+
export { usePayment, useVerify } from "./hooks";
|
|
4
|
+
export { WALLETS, getWallet } from "./wallets";
|
|
5
|
+
export type { UnseenProviderConfig, UnseenPayButtonProps, PaymentResult, PaymentStatus, WalletType, WalletOption, ModalStep, } from "./types";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── @unseen/ui ──────────────────────────────────────────────────────────────
|
|
3
|
+
// React components for Unseen Pay — wallet picker, checkout modal, and hooks.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// import { UnseenProvider, UnseenPayButton } from "@unseen/ui";
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.getWallet = exports.WALLETS = exports.useVerify = exports.usePayment = exports.UnseenProvider = exports.UnseenPayButton = void 0;
|
|
9
|
+
// Components
|
|
10
|
+
var components_1 = require("./components");
|
|
11
|
+
Object.defineProperty(exports, "UnseenPayButton", { enumerable: true, get: function () { return components_1.UnseenPayButton; } });
|
|
12
|
+
var context_1 = require("./context");
|
|
13
|
+
Object.defineProperty(exports, "UnseenProvider", { enumerable: true, get: function () { return context_1.UnseenProvider; } });
|
|
14
|
+
// Hooks
|
|
15
|
+
var hooks_1 = require("./hooks");
|
|
16
|
+
Object.defineProperty(exports, "usePayment", { enumerable: true, get: function () { return hooks_1.usePayment; } });
|
|
17
|
+
Object.defineProperty(exports, "useVerify", { enumerable: true, get: function () { return hooks_1.useVerify; } });
|
|
18
|
+
// Utilities
|
|
19
|
+
var wallets_1 = require("./wallets");
|
|
20
|
+
Object.defineProperty(exports, "WALLETS", { enumerable: true, get: function () { return wallets_1.WALLETS; } });
|
|
21
|
+
Object.defineProperty(exports, "getWallet", { enumerable: true, get: function () { return wallets_1.getWallet; } });
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Wallet choices available in the picker */
|
|
2
|
+
export type WalletType = "phantom" | "solflare" | "backpack";
|
|
3
|
+
/** Payment status from the API */
|
|
4
|
+
export type PaymentStatus = "pending" | "confirmed" | "expired" | "cancelled";
|
|
5
|
+
/** Configuration for the UnseenProvider */
|
|
6
|
+
export interface UnseenProviderConfig {
|
|
7
|
+
/** Your merchant API key (required only if creating payments directly from browser). */
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
/** Base URL of the Unseen API. Defaults to https://unseen.finance */
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Props for the UnseenPayButton */
|
|
13
|
+
export interface UnseenPayButtonProps {
|
|
14
|
+
/** Amount in raw token units */
|
|
15
|
+
amount: number;
|
|
16
|
+
/** SPL token mint address */
|
|
17
|
+
mint?: string;
|
|
18
|
+
/** Your internal reference/order ID */
|
|
19
|
+
reference: string;
|
|
20
|
+
/** Description shown on checkout page */
|
|
21
|
+
description?: string;
|
|
22
|
+
/** Expiry time in seconds (default 3600) */
|
|
23
|
+
expiresIn?: number;
|
|
24
|
+
/** Called when payment is verified as confirmed */
|
|
25
|
+
onSuccess?: (payment: PaymentResult) => void;
|
|
26
|
+
/** Called on any error */
|
|
27
|
+
onError?: (error: Error) => void;
|
|
28
|
+
/** Called when payment session is created */
|
|
29
|
+
onCreated?: (payment: PaymentResult) => void;
|
|
30
|
+
/** Custom button label. Default: "Pay with Unseen" */
|
|
31
|
+
label?: string;
|
|
32
|
+
/** Custom className for the button */
|
|
33
|
+
className?: string;
|
|
34
|
+
/** Disable the button */
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** Optional server-side payment session initializer for secure storefront flows. */
|
|
37
|
+
createPaymentSession?: (input: {
|
|
38
|
+
amount: number;
|
|
39
|
+
reference: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
mint?: string;
|
|
42
|
+
expiresIn?: number;
|
|
43
|
+
}) => Promise<PaymentResult>;
|
|
44
|
+
}
|
|
45
|
+
/** Payment result returned to callbacks */
|
|
46
|
+
export interface PaymentResult {
|
|
47
|
+
id: string;
|
|
48
|
+
status: string;
|
|
49
|
+
amount: string;
|
|
50
|
+
checkoutUrl: string;
|
|
51
|
+
reference: string;
|
|
52
|
+
paymentToken?: string;
|
|
53
|
+
txSignature?: string | null;
|
|
54
|
+
txSignatures?: string[];
|
|
55
|
+
confirmedAt?: string | null;
|
|
56
|
+
}
|
|
57
|
+
/** Wallet info for the picker */
|
|
58
|
+
export interface WalletOption {
|
|
59
|
+
id: WalletType;
|
|
60
|
+
name: string;
|
|
61
|
+
icon: string;
|
|
62
|
+
deeplink: (checkoutUrl: string) => string;
|
|
63
|
+
}
|
|
64
|
+
/** Internal state of the payment modal */
|
|
65
|
+
export type ModalStep = "idle" | "creating" | "wallet-picker" | "qr-display" | "waiting" | "verifying" | "confirmed" | "error";
|
package/dist/types.js
ADDED
package/dist/wallets.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WALLETS = void 0;
|
|
4
|
+
exports.getWallet = getWallet;
|
|
5
|
+
// ─── Wallet Definitions ──────────────────────────────────────────────────────
|
|
6
|
+
const phantomIcon = "/wallets/phantom-logo.jpg";
|
|
7
|
+
const solflareIcon = "/wallets/solflare-logo.svg";
|
|
8
|
+
const backpackIcon = "/wallets/backpack-logo.svg";
|
|
9
|
+
function getRefFromCheckoutUrl(url) {
|
|
10
|
+
try {
|
|
11
|
+
return new URL(url).origin;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return "https://unseen.finance";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.WALLETS = [
|
|
18
|
+
{
|
|
19
|
+
id: "phantom",
|
|
20
|
+
name: "Phantom",
|
|
21
|
+
icon: phantomIcon,
|
|
22
|
+
deeplink: (url) => `https://phantom.app/ul/browse/${encodeURIComponent(url)}?ref=${encodeURIComponent(getRefFromCheckoutUrl(url))}`,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "solflare",
|
|
26
|
+
name: "Solflare",
|
|
27
|
+
icon: solflareIcon,
|
|
28
|
+
deeplink: (url) => `https://solflare.com/ul/v1/browse/${encodeURIComponent(url)}?ref=${encodeURIComponent(getRefFromCheckoutUrl(url))}`,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "backpack",
|
|
32
|
+
name: "Backpack",
|
|
33
|
+
icon: backpackIcon,
|
|
34
|
+
deeplink: (url) => `https://backpack.app/ul/v1/browse/${encodeURIComponent(url)}?ref=${encodeURIComponent(getRefFromCheckoutUrl(url))}`,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
function getWallet(id) {
|
|
38
|
+
return exports.WALLETS.find((w) => w.id === id);
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unseen_fi/ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React components for Unseen Pay — wallet picker modal, QR checkout, and payment button",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "tsc --watch"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"react": ">=18",
|
|
14
|
+
"react-dom": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/react": "^19",
|
|
18
|
+
"@types/react-dom": "^19",
|
|
19
|
+
"@types/node": "^20",
|
|
20
|
+
"typescript": "^5"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["solana", "payments", "privacy", "umbra", "unseen", "react"],
|
|
23
|
+
"license": "MIT"
|
|
24
|
+
}
|