@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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +36 -2
- package/dist/index.cjs +564 -0
- package/dist/index.d.cts +123 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +527 -0
- package/package.json +41 -4
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|