@goodcash/react 0.0.0-stage → 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/index.js ADDED
@@ -0,0 +1,527 @@
1
+ 'use client';
2
+
3
+ // src/context.tsx
4
+ import { createContext, useContext, useMemo } from "react";
5
+ import { GoodCash } from "@goodcash/sdk";
6
+ import { jsx } from "react/jsx-runtime";
7
+ var GoodCashContext = createContext(null);
8
+ function GoodCashProvider({ publishableKey, appId, baseUrl, rpcUrl, fetch, theme, client, children }) {
9
+ const value = useMemo(
10
+ () => ({ client: client ?? new GoodCash({ publishableKey, appId, baseUrl, rpcUrl, fetch }), theme: theme ?? {} }),
11
+ [client, publishableKey, appId, baseUrl, rpcUrl, fetch, theme]
12
+ );
13
+ return /* @__PURE__ */ jsx(GoodCashContext.Provider, { value, children });
14
+ }
15
+ function useGoodCash() {
16
+ const ctx = useContext(GoodCashContext);
17
+ if (!ctx) throw new Error('Wrap your app (or the cash out button) in <GoodCashProvider publishableKey="gc_pk_...">.');
18
+ return ctx;
19
+ }
20
+
21
+ // src/hooks.ts
22
+ import { useCallback, useEffect, useRef, useState } from "react";
23
+ import { GoodCashError } from "@goodcash/sdk";
24
+ var asError = (e) => e instanceof GoodCashError ? e : new GoodCashError("unavailable", { detail: String(e?.message ?? e) });
25
+ function useQuote(amountG, opts = {}) {
26
+ const { client } = useGoodCash();
27
+ const [quote, setQuote] = useState(null);
28
+ const [error, setError] = useState(null);
29
+ const [loading, setLoading] = useState(false);
30
+ const amount = amountG === null || amountG === void 0 ? "" : String(amountG).trim();
31
+ useEffect(() => {
32
+ if (!/^\d+(\.\d{1,6})?$/.test(amount) || Number(amount) <= 0) {
33
+ setQuote(null);
34
+ setError(null);
35
+ return;
36
+ }
37
+ let live = true;
38
+ let timer;
39
+ const run = async () => {
40
+ setLoading(true);
41
+ try {
42
+ const q = await client.quote({ amountG: amount });
43
+ if (live) {
44
+ setQuote(q);
45
+ setError(null);
46
+ }
47
+ } catch (e) {
48
+ if (live) setError(asError(e));
49
+ } finally {
50
+ if (live) setLoading(false);
51
+ }
52
+ if (live) timer = setTimeout(run, opts.refreshMs ?? 25e3);
53
+ };
54
+ timer = setTimeout(run, opts.debounceMs ?? 400);
55
+ return () => {
56
+ live = false;
57
+ clearTimeout(timer);
58
+ };
59
+ }, [client, amount, opts.debounceMs, opts.refreshMs]);
60
+ return { quote, error, loading };
61
+ }
62
+ function useBanks() {
63
+ const { client } = useGoodCash();
64
+ const [banks, setBanks] = useState([]);
65
+ const [error, setError] = useState(null);
66
+ const [loading, setLoading] = useState(true);
67
+ useEffect(() => {
68
+ let live = true;
69
+ client.banks().then((b) => live && setBanks(b)).catch((e) => live && setError(asError(e))).finally(() => live && setLoading(false));
70
+ return () => {
71
+ live = false;
72
+ };
73
+ }, [client]);
74
+ return { banks, error, loading };
75
+ }
76
+ function useVerifyAccount(bankCode, accountNumber) {
77
+ const { client } = useGoodCash();
78
+ const [accountName, setName] = useState(null);
79
+ const [error, setError] = useState(null);
80
+ const [loading, setLoading] = useState(false);
81
+ useEffect(() => {
82
+ setName(null);
83
+ setError(null);
84
+ if (!bankCode || !/^\d{10}$/.test(accountNumber)) return;
85
+ let live = true;
86
+ setLoading(true);
87
+ client.verifyAccount({ bankCode, accountNumber }).then((r) => live && setName(r.accountName)).catch((e) => live && setError(asError(e))).finally(() => live && setLoading(false));
88
+ return () => {
89
+ live = false;
90
+ };
91
+ }, [client, bankCode, accountNumber]);
92
+ return { accountName, error, loading };
93
+ }
94
+ function useCashOut() {
95
+ const { client } = useGoodCash();
96
+ const [step, setStep] = useState(null);
97
+ const [cashOut, setCashOut] = useState(null);
98
+ const [error, setError] = useState(null);
99
+ const running = useRef(false);
100
+ const start = useCallback(
101
+ async (args, onStep) => {
102
+ if (running.current) return null;
103
+ running.current = true;
104
+ setError(null);
105
+ try {
106
+ const out = await client.cashOut({
107
+ ...args,
108
+ onStep: (s, c) => {
109
+ setStep(s);
110
+ if (c) setCashOut(c);
111
+ onStep?.(s, c);
112
+ }
113
+ });
114
+ setCashOut(out.cashOut);
115
+ return out;
116
+ } catch (e) {
117
+ setError(asError(e));
118
+ setStep(null);
119
+ return null;
120
+ } finally {
121
+ running.current = false;
122
+ }
123
+ },
124
+ [client]
125
+ );
126
+ const reset = useCallback(() => {
127
+ setStep(null);
128
+ setCashOut(null);
129
+ setError(null);
130
+ }, []);
131
+ return { start, step, cashOut, error, reset };
132
+ }
133
+ function useCashOutStatus(id) {
134
+ const { client } = useGoodCash();
135
+ const [cashOut, setCashOut] = useState(null);
136
+ const [error, setError] = useState(null);
137
+ useEffect(() => {
138
+ if (!id) return;
139
+ const ac = new AbortController();
140
+ client.watchCashOut(id, setCashOut, { signal: ac.signal }).catch((e) => !ac.signal.aborted && setError(asError(e)));
141
+ return () => ac.abort();
142
+ }, [client, id]);
143
+ return { cashOut, error };
144
+ }
145
+
146
+ // src/CashOutFlow.tsx
147
+ import { useEffect as useEffect2, useId, useMemo as useMemo2, useState as useState2 } from "react";
148
+
149
+ // src/styles.ts
150
+ var CSS = `
151
+ .gcw{font-family:var(--gcw-font);color:var(--gcw-ink);background:var(--gcw-bg);border-radius:calc(var(--gcw-radius) + 6px);padding:16px;box-sizing:border-box;max-width:440px;width:100%;line-height:1.35;-webkit-font-smoothing:antialiased}
152
+ .gcw *{box-sizing:border-box}
153
+ .gcw-h{font-size:22px;font-weight:800;margin:0 0 4px;letter-spacing:-.3px}
154
+ .gcw-sub{font-size:14px;margin:0 0 12px;opacity:.85}
155
+ .gcw-card{background:var(--gcw-surface);border:2.5px solid var(--gcw-ink);border-radius:var(--gcw-radius);padding:14px;margin:0 0 10px;box-shadow:4px 4px 0 var(--gcw-ink)}
156
+ .gcw-card--ok{background:var(--gcw-success)}
157
+ .gcw-lab{font-size:11px;font-weight:800;letter-spacing:.6px;text-transform:uppercase;opacity:.8;margin:0 0 6px;display:block}
158
+ .gcw-big{font-size:30px;font-weight:800;letter-spacing:-.6px}
159
+ .gcw-row{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:14px;padding:3px 0}
160
+ .gcw-input{width:100%;font:inherit;font-size:18px;font-weight:700;color:var(--gcw-ink);background:var(--gcw-surface);border:2.5px solid var(--gcw-ink);border-radius:calc(var(--gcw-radius) - 6px);padding:12px 14px;outline:none}
161
+ .gcw-input:focus-visible,.gcw-btn:focus-visible,.gcw-bank:focus-visible,.gcw-x:focus-visible{outline:3px solid var(--gcw-accent);outline-offset:2px}
162
+ .gcw-btn{display:flex;align-items:center;justify-content:center;width:100%;min-height:52px;font:inherit;font-size:17px;font-weight:800;color:var(--gcw-accent-ink);background:var(--gcw-accent);border:2.5px solid var(--gcw-ink);border-radius:999px;box-shadow:4px 4px 0 var(--gcw-ink);cursor:pointer;margin-top:6px}
163
+ .gcw-btn:active{transform:translate(3px,3px);box-shadow:1px 1px 0 var(--gcw-ink)}
164
+ .gcw-btn[disabled]{opacity:.55;cursor:not-allowed}
165
+ .gcw-btn--ghost{background:var(--gcw-surface);color:var(--gcw-ink)}
166
+ .gcw-note{font-size:14px;background:var(--gcw-surface);border:2px dashed var(--gcw-ink);border-radius:calc(var(--gcw-radius) - 6px);padding:10px 12px;margin:8px 0}
167
+ .gcw-err{font-size:14px;font-weight:700;background:#ffe1e1;color:#5a0000;border:2.5px solid #5a0000;border-radius:calc(var(--gcw-radius) - 6px);padding:10px 12px;margin:8px 0}
168
+ .gcw-steps{display:flex;gap:6px;margin:0 0 12px}
169
+ .gcw-step{flex:1;height:8px;border-radius:99px;border:2px solid var(--gcw-ink);background:var(--gcw-surface)}
170
+ .gcw-step--done{background:var(--gcw-success)}
171
+ .gcw-step--on{background:var(--gcw-accent)}
172
+ .gcw-banks{max-height:260px;overflow-y:auto;display:flex;flex-direction:column;gap:6px;margin-top:8px}
173
+ .gcw-bank{display:flex;align-items:center;gap:10px;width:100%;text-align:left;font:inherit;font-weight:700;color:var(--gcw-ink);background:var(--gcw-surface);border:2.5px solid var(--gcw-ink);border-radius:calc(var(--gcw-radius) - 6px);padding:10px 12px;cursor:pointer}
174
+ .gcw-bank[aria-selected=true]{background:var(--gcw-success)}
175
+ .gcw-chip{display:inline-block;font-size:12px;font-weight:800;background:var(--gcw-highlight);color:#111;border:2px solid var(--gcw-ink);border-radius:99px;padding:2px 8px}
176
+ .gcw-top{display:flex;justify-content:space-between;align-items:center;gap:10px;margin:0 0 10px}
177
+ .gcw-top .gcw-h{flex:1;min-width:0;margin:0}
178
+ .gcw-x{flex:none;font:inherit;font-size:18px;font-weight:800;width:40px;height:40px;border-radius:99px;border:2.5px solid var(--gcw-ink);background:var(--gcw-surface);color:var(--gcw-ink);cursor:pointer}
179
+ .gcw-foot{font-size:12px;text-align:center;opacity:.75;margin-top:10px}
180
+ .gcw-spin{width:40px;height:40px;border-radius:99px;border:4px solid var(--gcw-ink);border-top-color:transparent;margin:10px auto;animation:gcw-spin 1s linear infinite}
181
+ @keyframes gcw-spin{to{transform:rotate(360deg)}}
182
+ @media (prefers-reduced-motion:reduce){.gcw-spin{animation:none;border-top-color:var(--gcw-ink)}.gcw-btn:active{transform:none}}
183
+ .gcw-overlay{position:fixed;inset:0;background:rgba(17,17,17,.55);display:flex;align-items:flex-end;justify-content:center;z-index:2147483000;padding:12px}
184
+ @media (min-width:560px){.gcw-overlay{align-items:center}}
185
+ .gcw-dialog{max-height:calc(100vh - 24px);overflow-y:auto;width:100%;max-width:440px;border:2.5px solid #111;border-radius:24px;box-shadow:6px 6px 0 #111}
186
+ .gcw-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
187
+ `;
188
+ var injected = false;
189
+ function injectStyles() {
190
+ if (injected || typeof document === "undefined") return;
191
+ if (document.querySelector("style[data-goodcash]")) {
192
+ injected = true;
193
+ return;
194
+ }
195
+ const el = document.createElement("style");
196
+ el.setAttribute("data-goodcash", "");
197
+ el.textContent = CSS;
198
+ document.head.appendChild(el);
199
+ injected = true;
200
+ }
201
+
202
+ // src/theme.ts
203
+ var LIGHT = {
204
+ background: "#cdb8ff",
205
+ surface: "#ffffff",
206
+ ink: "#111111",
207
+ accent: "#7b39fc",
208
+ accentInk: "#ffffff",
209
+ highlight: "#ffd23f",
210
+ success: "#8cf0c0",
211
+ radius: 18,
212
+ font: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Inter, Arial, sans-serif'
213
+ };
214
+ var DARK = {
215
+ background: "#1c1533",
216
+ surface: "#2a2246",
217
+ ink: "#f5f1ff",
218
+ accent: "#9b6bff",
219
+ accentInk: "#ffffff",
220
+ highlight: "#ffd23f",
221
+ success: "#5fd6a0"
222
+ };
223
+ function themeVars(t = {}, dark = false) {
224
+ const base = { ...LIGHT, ...dark ? DARK : {}, ...stripUndefined(t) };
225
+ return {
226
+ "--gcw-bg": base.background,
227
+ "--gcw-surface": base.surface,
228
+ "--gcw-ink": base.ink,
229
+ "--gcw-accent": base.accent,
230
+ "--gcw-accent-ink": base.accentInk,
231
+ "--gcw-highlight": base.highlight,
232
+ "--gcw-success": base.success,
233
+ "--gcw-radius": `${base.radius}px`,
234
+ "--gcw-font": base.font
235
+ };
236
+ }
237
+ function stripUndefined(o) {
238
+ return Object.fromEntries(Object.entries(o).filter(([k, v]) => v !== void 0 && k !== "mode"));
239
+ }
240
+
241
+ // src/CashOutFlow.tsx
242
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
243
+ var naira = (n) => `\u20A6${Math.round(n).toLocaleString("en-US")}`;
244
+ var gText = (n) => `${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })} G$`;
245
+ var WORKING = {
246
+ checking: "Checking your wallet\u2026",
247
+ creating: "Preparing your payout\u2026",
248
+ confirm_in_wallet: "Confirm in your wallet. Open your wallet app, tap Confirm, then come back here.",
249
+ confirming: "Sending\u2026",
250
+ reporting: "Sending\u2026",
251
+ done: "Sent."
252
+ };
253
+ function defaultSigner() {
254
+ const eth = typeof window !== "undefined" ? window.ethereum : void 0;
255
+ return eth ?? void 0;
256
+ }
257
+ function CashOutFlow({ signer, wallet, defaultAmountG, partnerRef, onEvent, onClose, titleId }) {
258
+ injectStyles();
259
+ const { theme, client } = useGoodCash();
260
+ const [dark, setDark] = useState2(false);
261
+ useEffect2(() => {
262
+ if (theme.mode !== "auto" || typeof window === "undefined" || !window.matchMedia) return setDark(theme.mode === "dark");
263
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
264
+ setDark(mq.matches);
265
+ const on = () => setDark(mq.matches);
266
+ mq.addEventListener?.("change", on);
267
+ return () => mq.removeEventListener?.("change", on);
268
+ }, [theme.mode]);
269
+ const vars = useMemo2(() => themeVars(theme, dark), [theme, dark]);
270
+ const [stage, setStage] = useState2("amount");
271
+ const [amount, setAmount] = useState2(defaultAmountG === void 0 ? "" : String(defaultAmountG));
272
+ const [bank, setBank] = useState2(null);
273
+ const [account, setAccount] = useState2("");
274
+ const [search, setSearch] = useState2("");
275
+ const { quote, error: quoteError, loading: quoting } = useQuote(amount);
276
+ const { banks, error: banksError, loading: banksLoading } = useBanks();
277
+ const { accountName, error: nameError, loading: naming } = useVerifyAccount(bank?.code ?? "", account);
278
+ const run = useCashOut();
279
+ const [trackId, setTrackId] = useState2(null);
280
+ const status = useCashOutStatus(trackId);
281
+ const ownTitle = useId();
282
+ const amountId = useId();
283
+ const acctId = useId();
284
+ const searchId = useId();
285
+ const ids = { title: titleId ?? ownTitle, amount: amountId, acct: acctId, search: searchId };
286
+ const live = status.cashOut ?? run.cashOut;
287
+ useEffect2(() => {
288
+ if (!status.cashOut) return;
289
+ if (status.cashOut.status === "paid") onEvent?.({ type: "cashout.paid", cashOut: status.cashOut });
290
+ if (["failed", "refunded", "expired"].includes(status.cashOut.status)) onEvent?.({ type: "cashout.failed", cashOut: status.cashOut });
291
+ }, [status.cashOut?.status]);
292
+ const shownBanks = useMemo2(() => {
293
+ const q = search.trim().toLowerCase();
294
+ return banks.filter((b) => !q || b.name.toLowerCase().includes(q));
295
+ }, [banks, search]);
296
+ const enough = !!quote && quote.enough;
297
+ const stepIndex = { amount: 0, bank: 1, review: 2, working: 3, tracking: 3 }[stage];
298
+ async function confirm() {
299
+ if (!quote || !bank || !accountName) return;
300
+ const s = signer ?? defaultSigner();
301
+ setStage("working");
302
+ const out = await run.start(
303
+ { signer: s, wallet, amountG: amount, recipient: { bankCode: bank.code, accountNumber: account, accountName }, quote, partnerRef },
304
+ (step, c) => {
305
+ if (step === "confirm_in_wallet" && c) onEvent?.({ type: "cashout.created", cashOut: c });
306
+ }
307
+ );
308
+ if (out) {
309
+ onEvent?.({ type: "cashout.sent", cashOut: out.cashOut });
310
+ setTrackId(out.cashOut.id);
311
+ setStage("tracking");
312
+ } else {
313
+ setStage("review");
314
+ }
315
+ }
316
+ const noWallet = !signer && !defaultSigner() && client.mode === "live";
317
+ return /* @__PURE__ */ jsxs("div", { className: "gcw", style: vars, "data-goodcash-flow": "", children: [
318
+ /* @__PURE__ */ jsxs("div", { className: "gcw-top", children: [
319
+ /* @__PURE__ */ jsx2("h2", { className: "gcw-h", id: ids.title, children: "Cash out to your bank" }),
320
+ onClose && /* @__PURE__ */ jsx2("button", { type: "button", className: "gcw-x", "aria-label": "Close", onClick: onClose, children: "\u2715" })
321
+ ] }),
322
+ /* @__PURE__ */ jsx2("div", { className: "gcw-steps", "aria-hidden": true, children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsx2("span", { className: `gcw-step ${i < stepIndex ? "gcw-step--done" : i === stepIndex ? "gcw-step--on" : ""}` }, i)) }),
323
+ client.mode === "test" && /* @__PURE__ */ jsx2("p", { className: "gcw-note", children: "Test mode: no real money moves." }),
324
+ stage === "amount" && /* @__PURE__ */ jsxs(Fragment, { children: [
325
+ /* @__PURE__ */ jsxs("div", { className: "gcw-card", children: [
326
+ /* @__PURE__ */ jsx2("label", { className: "gcw-lab", htmlFor: ids.amount, children: "How much G$?" }),
327
+ /* @__PURE__ */ jsx2(
328
+ "input",
329
+ {
330
+ id: ids.amount,
331
+ className: "gcw-input",
332
+ inputMode: "decimal",
333
+ autoComplete: "off",
334
+ placeholder: "0",
335
+ value: amount,
336
+ onChange: (e) => setAmount(e.target.value.replace(/[^\d.]/g, ""))
337
+ }
338
+ )
339
+ ] }),
340
+ /* @__PURE__ */ jsxs("div", { className: "gcw-card gcw-card--ok", "aria-live": "polite", children: [
341
+ /* @__PURE__ */ jsx2("span", { className: "gcw-lab", children: "You get about" }),
342
+ /* @__PURE__ */ jsx2("div", { className: "gcw-big", children: quote ? naira(quote.receive.naira) : quoting ? "\u2026" : "\u20A60" }),
343
+ quote && /* @__PURE__ */ jsxs("div", { className: "gcw-row", children: [
344
+ "at least ",
345
+ naira(quote.receive.minimumNaira)
346
+ ] })
347
+ ] }),
348
+ quote && !quote.enough && /* @__PURE__ */ jsxs("p", { className: "gcw-note", children: [
349
+ "The smallest cash out is about ",
350
+ gText(quote.minimumG),
351
+ "."
352
+ ] }),
353
+ quoteError && /* @__PURE__ */ jsx2("p", { className: "gcw-err", children: quoteError.message }),
354
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "gcw-btn", disabled: !enough, onClick: () => setStage("bank"), children: "Next" })
355
+ ] }),
356
+ stage === "bank" && /* @__PURE__ */ jsxs(Fragment, { children: [
357
+ /* @__PURE__ */ jsxs("div", { className: "gcw-card", children: [
358
+ /* @__PURE__ */ jsx2("label", { className: "gcw-lab", htmlFor: ids.search, children: "Your bank" }),
359
+ /* @__PURE__ */ jsx2("input", { id: ids.search, className: "gcw-input", type: "search", placeholder: "Search banks", value: search, onChange: (e) => setSearch(e.target.value) }),
360
+ /* @__PURE__ */ jsxs("div", { className: "gcw-banks", role: "listbox", "aria-label": "Banks", children: [
361
+ banksLoading && /* @__PURE__ */ jsx2("span", { children: "Loading banks\u2026" }),
362
+ shownBanks.map((b) => /* @__PURE__ */ jsxs("button", { type: "button", role: "option", "aria-selected": bank?.code === b.code, className: "gcw-bank", onClick: () => setBank(b), children: [
363
+ b.name,
364
+ bank?.code === b.code && /* @__PURE__ */ jsx2("span", { "aria-hidden": true, children: " \u2713" })
365
+ ] }, b.code)),
366
+ !banksLoading && shownBanks.length === 0 && /* @__PURE__ */ jsxs("span", { children: [
367
+ "No bank matches \u201C",
368
+ search,
369
+ "\u201D. Check the spelling."
370
+ ] })
371
+ ] }),
372
+ banksError && /* @__PURE__ */ jsx2("p", { className: "gcw-err", children: banksError.message })
373
+ ] }),
374
+ /* @__PURE__ */ jsxs("div", { className: "gcw-card", children: [
375
+ /* @__PURE__ */ jsx2("label", { className: "gcw-lab", htmlFor: ids.acct, children: "Account number" }),
376
+ /* @__PURE__ */ jsx2(
377
+ "input",
378
+ {
379
+ id: ids.acct,
380
+ className: "gcw-input",
381
+ inputMode: "numeric",
382
+ autoComplete: "off",
383
+ maxLength: 10,
384
+ placeholder: "10 digits",
385
+ value: account,
386
+ onChange: (e) => setAccount(e.target.value.replace(/\D/g, "").slice(0, 10))
387
+ }
388
+ ),
389
+ /* @__PURE__ */ jsxs("div", { "aria-live": "polite", children: [
390
+ naming && /* @__PURE__ */ jsx2("p", { className: "gcw-sub", children: "Checking the account name\u2026" }),
391
+ accountName && /* @__PURE__ */ jsx2("p", { className: "gcw-sub", children: /* @__PURE__ */ jsx2("b", { children: accountName }) }),
392
+ nameError && /* @__PURE__ */ jsx2("p", { className: "gcw-err", children: nameError.message })
393
+ ] })
394
+ ] }),
395
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "gcw-btn", disabled: !accountName, onClick: () => setStage("review"), children: "Review" }),
396
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "gcw-btn gcw-btn--ghost", onClick: () => setStage("amount"), children: "Back" })
397
+ ] }),
398
+ stage === "review" && quote && bank && accountName && /* @__PURE__ */ jsxs(Fragment, { children: [
399
+ /* @__PURE__ */ jsxs("div", { className: "gcw-card gcw-card--ok", children: [
400
+ /* @__PURE__ */ jsx2("span", { className: "gcw-lab", children: "You get about" }),
401
+ /* @__PURE__ */ jsx2("div", { className: "gcw-big", children: naira(quote.receive.naira) }),
402
+ /* @__PURE__ */ jsxs("div", { className: "gcw-row", children: [
403
+ "for ",
404
+ gText(amount),
405
+ " \xB7 at least ",
406
+ naira(quote.receive.minimumNaira)
407
+ ] })
408
+ ] }),
409
+ /* @__PURE__ */ jsxs("div", { className: "gcw-card", children: [
410
+ /* @__PURE__ */ jsx2("span", { className: "gcw-lab", children: "Paid to" }),
411
+ /* @__PURE__ */ jsx2("div", { children: /* @__PURE__ */ jsx2("b", { children: accountName }) }),
412
+ /* @__PURE__ */ jsxs("div", { className: "gcw-row", children: [
413
+ bank.name,
414
+ " \xB7 \u2022\u2022\u2022\u2022",
415
+ account.slice(-4)
416
+ ] })
417
+ ] }),
418
+ /* @__PURE__ */ jsxs("div", { className: "gcw-card", children: [
419
+ /* @__PURE__ */ jsx2("span", { className: "gcw-lab", children: "Fees" }),
420
+ quote.fees.map((f) => /* @__PURE__ */ jsxs("div", { className: "gcw-row", children: [
421
+ /* @__PURE__ */ jsx2("span", { children: f.label }),
422
+ /* @__PURE__ */ jsx2("span", { children: f.amountG ? `\u2212${gText(f.amountG)}` : f.naira !== void 0 ? naira(f.naira) : "" })
423
+ ] }, f.type)),
424
+ /* @__PURE__ */ jsxs("div", { className: "gcw-row", children: [
425
+ /* @__PURE__ */ jsx2("span", { children: "Network fee" }),
426
+ /* @__PURE__ */ jsx2("span", { children: "paid in CELO from your wallet" })
427
+ ] })
428
+ ] }),
429
+ run.error && /* @__PURE__ */ jsx2("p", { className: "gcw-err", children: run.error.message }),
430
+ noWallet && /* @__PURE__ */ jsx2("p", { className: "gcw-err", children: "Connect a wallet first." }),
431
+ /* @__PURE__ */ jsxs("p", { className: "gcw-note", children: [
432
+ /* @__PURE__ */ jsx2("b", { children: "You'll confirm once in your wallet." }),
433
+ " Your G$ only leaves your wallet when you confirm."
434
+ ] }),
435
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "gcw-btn", disabled: noWallet, onClick: confirm, children: "Confirm cash out" }),
436
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "gcw-btn gcw-btn--ghost", onClick: () => setStage("bank"), children: "Back" })
437
+ ] }),
438
+ stage === "working" && /* @__PURE__ */ jsxs("div", { className: "gcw-card", "aria-live": "polite", role: "status", children: [
439
+ /* @__PURE__ */ jsx2("div", { className: "gcw-spin", "aria-hidden": true }),
440
+ /* @__PURE__ */ jsx2("p", { className: "gcw-sub", style: { textAlign: "center" }, children: run.step ? WORKING[run.step] : "Working on it\u2026" })
441
+ ] }),
442
+ stage === "tracking" && live && /* @__PURE__ */ jsxs("div", { "aria-live": "polite", role: "status", children: [
443
+ live.status === "paid" ? /* @__PURE__ */ jsxs("div", { className: "gcw-card gcw-card--ok", children: [
444
+ /* @__PURE__ */ jsx2("span", { className: "gcw-lab", children: "Paid" }),
445
+ /* @__PURE__ */ jsx2("div", { className: "gcw-big", children: naira(live.receive.naira) }),
446
+ /* @__PURE__ */ jsx2("p", { className: "gcw-sub", children: "is in your bank account. Thank you!" })
447
+ ] }) : ["failed", "refunded", "expired"].includes(live.status) ? /* @__PURE__ */ jsxs("div", { className: "gcw-card", children: [
448
+ /* @__PURE__ */ jsx2("span", { className: "gcw-lab", children: "Not paid" }),
449
+ /* @__PURE__ */ jsx2("p", { className: "gcw-sub", children: live.status === "refunded" ? "The payout could not be completed, so your money is being returned to your wallet." : "The payout could not be completed. Please contact support with your reference." }),
450
+ /* @__PURE__ */ jsxs("div", { className: "gcw-row", children: [
451
+ "Reference: ",
452
+ live.id.slice(0, 8)
453
+ ] })
454
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "gcw-card", children: [
455
+ /* @__PURE__ */ jsx2("div", { className: "gcw-spin", "aria-hidden": true }),
456
+ /* @__PURE__ */ jsx2("p", { className: "gcw-sub", style: { textAlign: "center" }, children: "Your money is on its way to your bank. This usually takes a few minutes. You can close this; it will still arrive." })
457
+ ] }),
458
+ onClose && /* @__PURE__ */ jsx2("button", { type: "button", className: "gcw-btn gcw-btn--ghost", onClick: onClose, children: "Done" })
459
+ ] }),
460
+ /* @__PURE__ */ jsx2("p", { className: "gcw-foot", children: "Powered by GoodCash" })
461
+ ] });
462
+ }
463
+
464
+ // src/CashOutButton.tsx
465
+ import { useCallback as useCallback2, useEffect as useEffect3, useId as useId2, useRef as useRef2, useState as useState3 } from "react";
466
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
467
+ var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])';
468
+ function CashOutButton({ label = "Cash out to bank", className, style, onEvent, ...flow }) {
469
+ injectStyles();
470
+ const [open, setOpen] = useState3(false);
471
+ const titleId = useId2();
472
+ const panel = useRef2(null);
473
+ const opener = useRef2(null);
474
+ const close = useCallback2(() => {
475
+ setOpen(false);
476
+ onEvent?.({ type: "closed" });
477
+ setTimeout(() => opener.current?.focus(), 0);
478
+ }, [onEvent]);
479
+ useEffect3(() => {
480
+ if (!open) return;
481
+ const first = panel.current?.querySelector(FOCUSABLE);
482
+ first?.focus();
483
+ const onKey = (e) => {
484
+ if (e.key === "Escape") {
485
+ e.preventDefault();
486
+ close();
487
+ }
488
+ if (e.key !== "Tab" || !panel.current) return;
489
+ const items = [...panel.current.querySelectorAll(FOCUSABLE)];
490
+ if (!items.length) return;
491
+ const firstEl = items[0];
492
+ const lastEl = items[items.length - 1];
493
+ if (e.shiftKey && document.activeElement === firstEl) {
494
+ e.preventDefault();
495
+ lastEl.focus();
496
+ } else if (!e.shiftKey && document.activeElement === lastEl) {
497
+ e.preventDefault();
498
+ firstEl.focus();
499
+ }
500
+ };
501
+ document.addEventListener("keydown", onKey);
502
+ const prev = document.body.style.overflow;
503
+ document.body.style.overflow = "hidden";
504
+ return () => {
505
+ document.removeEventListener("keydown", onKey);
506
+ document.body.style.overflow = prev;
507
+ };
508
+ }, [open, close]);
509
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
510
+ /* @__PURE__ */ jsx3("button", { ref: opener, type: "button", className: className ?? "gcw-btn", style, onClick: () => setOpen(true), "aria-haspopup": "dialog", children: label }),
511
+ open && /* @__PURE__ */ jsx3("div", { className: "gcw-overlay", onMouseDown: (e) => e.target === e.currentTarget && close(), children: /* @__PURE__ */ jsx3("div", { ref: panel, className: "gcw-dialog", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, children: /* @__PURE__ */ jsx3(CashOutFlow, { ...flow, onEvent, onClose: close, titleId }) }) })
512
+ ] });
513
+ }
514
+ export {
515
+ CashOutButton,
516
+ CashOutFlow,
517
+ CSS as GOODCASH_CSS,
518
+ GoodCashProvider,
519
+ injectStyles,
520
+ themeVars,
521
+ useBanks,
522
+ useCashOut,
523
+ useCashOutStatus,
524
+ useGoodCash,
525
+ useQuote,
526
+ useVerifyAccount
527
+ };
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@goodcash/react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A ready-made 'Cash out to bank' button and hooks for GoodDollar (G$) to naira, for React apps.",
5
+ "license": "MIT",
6
+ "homepage": "https://goodcash.xyz",
7
+ "repository": { "type": "git", "url": "https://github.com/OtowoSamuel/goodcash.git", "directory": "packages/react" },
8
+ "keywords": ["gooddollar", "celo", "naira", "cash out", "react", "widget"],
9
+ "type": "module",
10
+ "sideEffects": false,
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": ["dist", "README.md", "CHANGELOG.md", "LICENSE"],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "test": "vitest run",
22
+ "typecheck": "tsc -p tsconfig.json --noEmit",
23
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
24
+ },
25
+ "peerDependencies": {
26
+ "@goodcash/sdk": "^0.1.0",
27
+ "react": ">=18",
28
+ "react-dom": ">=18"
29
+ },
30
+ "devDependencies": {
31
+ "@goodcash/sdk": "0.1.0",
32
+ "@testing-library/react": "^16.3.3",
33
+ "@testing-library/user-event": "^14.6.8",
34
+ "@types/react": "^18.3.12",
35
+ "@types/react-dom": "^18.3.1",
36
+ "jsdom": "^30.1.3",
37
+ "react": "^18.3.1",
38
+ "react-dom": "^18.3.1",
39
+ "tsup": "^8.3.5",
40
+ "typescript": "^5.7.2",
41
+ "vitest": "^2.1.8"
42
+ }
43
+ }