@fragmentpay/react 0.3.1 → 0.4.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/README.md +28 -3
- package/dist/index.d.mts +47 -3
- package/dist/index.d.ts +47 -3
- package/dist/index.js +94 -33
- package/dist/index.mjs +92 -33
- package/package.json +25 -6
package/README.md
CHANGED
|
@@ -11,9 +11,9 @@ npm install @fragmentpay/react
|
|
|
11
11
|
|
|
12
12
|
## Required environment variables
|
|
13
13
|
|
|
14
|
-
| Name | Where | Example
|
|
15
|
-
| ----------------------------- | ----------------- |
|
|
16
|
-
| `NEXT_PUBLIC_FRAG_PUBLIC_KEY` | browser (public) | `pk_live_…` or `pk_test_…`
|
|
14
|
+
| Name | Where | Example |
|
|
15
|
+
| ----------------------------- | ----------------- | ------------------------------------------- |
|
|
16
|
+
| `NEXT_PUBLIC_FRAG_PUBLIC_KEY` | browser (public) | `pk_live_…` or `pk_test_…` |
|
|
17
17
|
| `FRAG_SECRET_KEY` | your backend only | `sk_live_…` (used by `@fragmentpay/server`) |
|
|
18
18
|
|
|
19
19
|
The React SDK never touches `sk_…`. Intent creation happens on your server
|
|
@@ -81,6 +81,30 @@ Behind the scenes: click → POST to your `/api/frag` → returns intent →
|
|
|
81
81
|
embed Frag checkout iframe → live-poll status → invoke `onSettled` when
|
|
82
82
|
`status === "settled"`.
|
|
83
83
|
|
|
84
|
+
## Embed vs redirect
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// Embed (default) — hosted checkout iframe with post-message events
|
|
88
|
+
<FragmentPayCheckout intentId={id} onSettled={onDone} onError={onErr} />
|
|
89
|
+
|
|
90
|
+
// Redirect — full-page navigation to hosted checkout
|
|
91
|
+
<FragmentPayCheckout intentId={id} mode="redirect" />
|
|
92
|
+
|
|
93
|
+
// Or just point the user at the hosted URL manually:
|
|
94
|
+
window.location.href = intent.checkout_url;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Polling with `useFragmentIntent`
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
const { data, status } = useFragmentIntent(intentId, { intervalMs: 3000 });
|
|
101
|
+
// data.status: created → quoted → executing → settled | failed | expired
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Polling uses exponential backoff on 5xx / network errors and honors any
|
|
105
|
+
`Retry-After` header the API returns; on the terminal states (`settled`,
|
|
106
|
+
`failed`, `expired`, `cancelled`, `refunded`) polling stops automatically.
|
|
107
|
+
|
|
84
108
|
## Validation
|
|
85
109
|
|
|
86
110
|
Wrong props fail immediately with a clear message:
|
|
@@ -98,6 +122,7 @@ Wrong props fail immediately with a clear message:
|
|
|
98
122
|
- `useFragmentIntent(intentId, { intervalMs })`
|
|
99
123
|
- `useFragmentTokens({ chain })`
|
|
100
124
|
- `useCreateIntent({ endpoint, transform, headers })`
|
|
125
|
+
- `fetchWithRetry(url, init)` — exported for advanced integrators
|
|
101
126
|
|
|
102
127
|
## License
|
|
103
128
|
|
package/dist/index.d.mts
CHANGED
|
@@ -8,7 +8,7 @@ import * as React from 'react';
|
|
|
8
8
|
|
|
9
9
|
interface FragmentPayProviderProps {
|
|
10
10
|
publicKey: string;
|
|
11
|
-
/** Override the API/host origin. Defaults to https://frag.
|
|
11
|
+
/** Override the API/host origin. Defaults to https://frag.cash. */
|
|
12
12
|
baseUrl?: string;
|
|
13
13
|
children: React.ReactNode;
|
|
14
14
|
}
|
|
@@ -25,14 +25,43 @@ interface FetchWithRetryOpts extends RequestInit {
|
|
|
25
25
|
* with jitter for retryable failures. Exposed for advanced integrators.
|
|
26
26
|
*/
|
|
27
27
|
declare function fetchWithRetry(input: string, init?: FetchWithRetryOpts): Promise<Response>;
|
|
28
|
-
type IntentStatus = "created" | "requires_payment" | "quoted" | "executing" | "settled" | "failed" | "expired" | "cancelled" | "refunded";
|
|
28
|
+
type IntentStatus = "created" | "requires_payment" | "quoted" | "awaiting_signature" | "executing" | "partially_filled" | "settled" | "failed" | "expired" | "cancelled" | "refunded";
|
|
29
29
|
interface PublicIntent {
|
|
30
30
|
id: string;
|
|
31
31
|
amount: string;
|
|
32
32
|
currency: string;
|
|
33
33
|
status: IntentStatus;
|
|
34
|
+
settlement_chain?: string | null;
|
|
35
|
+
settlement_token?: string | null;
|
|
36
|
+
expires_at?: string;
|
|
34
37
|
metadata?: Record<string, unknown>;
|
|
35
38
|
}
|
|
39
|
+
type RefundStatus = "pending" | "processing" | "retry_scheduled" | "succeeded" | "failed";
|
|
40
|
+
type RefundPhase = "queued" | "sent" | "completed" | "failed";
|
|
41
|
+
/**
|
|
42
|
+
* Bucket a raw refund status into a stable UI phase. Mirrors the identical
|
|
43
|
+
* helper in `@fragmentpay/server` so front-end and back-end render matching
|
|
44
|
+
* status pills.
|
|
45
|
+
*/
|
|
46
|
+
declare function refundPhase(status: RefundStatus | string | null | undefined): RefundPhase;
|
|
47
|
+
interface PublicRefundStatus {
|
|
48
|
+
id: string;
|
|
49
|
+
payment_intent_id: string;
|
|
50
|
+
amount_usd: number;
|
|
51
|
+
chain: string | null;
|
|
52
|
+
token: string | null;
|
|
53
|
+
destination_tail: string;
|
|
54
|
+
reason: string;
|
|
55
|
+
status: RefundStatus;
|
|
56
|
+
phase: RefundPhase;
|
|
57
|
+
tx_hash: string | null;
|
|
58
|
+
failure_reason: string | null;
|
|
59
|
+
attempts: number;
|
|
60
|
+
created_at: string;
|
|
61
|
+
updated_at: string;
|
|
62
|
+
paid_at: string | null;
|
|
63
|
+
terminal: boolean;
|
|
64
|
+
}
|
|
36
65
|
declare function useFragmentIntent(intentId: string | null, opts?: {
|
|
37
66
|
intervalMs?: number;
|
|
38
67
|
}): {
|
|
@@ -40,6 +69,21 @@ declare function useFragmentIntent(intentId: string | null, opts?: {
|
|
|
40
69
|
status: "idle" | "loading" | "ready" | "error";
|
|
41
70
|
error: string | null;
|
|
42
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* Poll the payer-facing refund status endpoint at `/api/public/refunds/:id/status`.
|
|
74
|
+
* UNAUTHENTICATED — safe to use in customer-facing receipt / order-tracking UIs.
|
|
75
|
+
*
|
|
76
|
+
* Returns `{ data, status, error }` where `data.phase` buckets the raw refund
|
|
77
|
+
* state into `queued | sent | completed | failed` for a stable status pill.
|
|
78
|
+
* Polling stops automatically once `data.terminal === true`.
|
|
79
|
+
*/
|
|
80
|
+
declare function useFragmentRefund(refundId: string | null, opts?: {
|
|
81
|
+
intervalMs?: number;
|
|
82
|
+
}): {
|
|
83
|
+
data: PublicRefundStatus | null;
|
|
84
|
+
status: "idle" | "loading" | "ready" | "error";
|
|
85
|
+
error: string | null;
|
|
86
|
+
};
|
|
43
87
|
interface FragmentPayCheckoutProps {
|
|
44
88
|
intentId: string;
|
|
45
89
|
/** Called once the intent transitions to `settled`. */
|
|
@@ -96,4 +140,4 @@ interface FragmentPayButtonProps extends UseCreateIntentOptions {
|
|
|
96
140
|
}
|
|
97
141
|
declare function FragmentPayButton({ input, label, onSettled, onError, className, style, ...opts }: FragmentPayButtonProps): React.JSX.Element;
|
|
98
142
|
|
|
99
|
-
export { type FetchWithRetryOpts, type FragToken, FragmentPayButton, type FragmentPayButtonProps, FragmentPayCheckout, type FragmentPayCheckoutProps, FragmentPayProvider, type FragmentPayProviderProps, type IntentStatus, type PublicIntent, type UseCreateIntentOptions, fetchWithRetry, useCreateIntent, useFragmentIntent, useFragmentTokens };
|
|
143
|
+
export { type FetchWithRetryOpts, type FragToken, FragmentPayButton, type FragmentPayButtonProps, FragmentPayCheckout, type FragmentPayCheckoutProps, FragmentPayProvider, type FragmentPayProviderProps, type IntentStatus, type PublicIntent, type PublicRefundStatus, type RefundPhase, type RefundStatus, type UseCreateIntentOptions, fetchWithRetry, refundPhase, useCreateIntent, useFragmentIntent, useFragmentRefund, useFragmentTokens };
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import * as React from 'react';
|
|
|
8
8
|
|
|
9
9
|
interface FragmentPayProviderProps {
|
|
10
10
|
publicKey: string;
|
|
11
|
-
/** Override the API/host origin. Defaults to https://frag.
|
|
11
|
+
/** Override the API/host origin. Defaults to https://frag.cash. */
|
|
12
12
|
baseUrl?: string;
|
|
13
13
|
children: React.ReactNode;
|
|
14
14
|
}
|
|
@@ -25,14 +25,43 @@ interface FetchWithRetryOpts extends RequestInit {
|
|
|
25
25
|
* with jitter for retryable failures. Exposed for advanced integrators.
|
|
26
26
|
*/
|
|
27
27
|
declare function fetchWithRetry(input: string, init?: FetchWithRetryOpts): Promise<Response>;
|
|
28
|
-
type IntentStatus = "created" | "requires_payment" | "quoted" | "executing" | "settled" | "failed" | "expired" | "cancelled" | "refunded";
|
|
28
|
+
type IntentStatus = "created" | "requires_payment" | "quoted" | "awaiting_signature" | "executing" | "partially_filled" | "settled" | "failed" | "expired" | "cancelled" | "refunded";
|
|
29
29
|
interface PublicIntent {
|
|
30
30
|
id: string;
|
|
31
31
|
amount: string;
|
|
32
32
|
currency: string;
|
|
33
33
|
status: IntentStatus;
|
|
34
|
+
settlement_chain?: string | null;
|
|
35
|
+
settlement_token?: string | null;
|
|
36
|
+
expires_at?: string;
|
|
34
37
|
metadata?: Record<string, unknown>;
|
|
35
38
|
}
|
|
39
|
+
type RefundStatus = "pending" | "processing" | "retry_scheduled" | "succeeded" | "failed";
|
|
40
|
+
type RefundPhase = "queued" | "sent" | "completed" | "failed";
|
|
41
|
+
/**
|
|
42
|
+
* Bucket a raw refund status into a stable UI phase. Mirrors the identical
|
|
43
|
+
* helper in `@fragmentpay/server` so front-end and back-end render matching
|
|
44
|
+
* status pills.
|
|
45
|
+
*/
|
|
46
|
+
declare function refundPhase(status: RefundStatus | string | null | undefined): RefundPhase;
|
|
47
|
+
interface PublicRefundStatus {
|
|
48
|
+
id: string;
|
|
49
|
+
payment_intent_id: string;
|
|
50
|
+
amount_usd: number;
|
|
51
|
+
chain: string | null;
|
|
52
|
+
token: string | null;
|
|
53
|
+
destination_tail: string;
|
|
54
|
+
reason: string;
|
|
55
|
+
status: RefundStatus;
|
|
56
|
+
phase: RefundPhase;
|
|
57
|
+
tx_hash: string | null;
|
|
58
|
+
failure_reason: string | null;
|
|
59
|
+
attempts: number;
|
|
60
|
+
created_at: string;
|
|
61
|
+
updated_at: string;
|
|
62
|
+
paid_at: string | null;
|
|
63
|
+
terminal: boolean;
|
|
64
|
+
}
|
|
36
65
|
declare function useFragmentIntent(intentId: string | null, opts?: {
|
|
37
66
|
intervalMs?: number;
|
|
38
67
|
}): {
|
|
@@ -40,6 +69,21 @@ declare function useFragmentIntent(intentId: string | null, opts?: {
|
|
|
40
69
|
status: "idle" | "loading" | "ready" | "error";
|
|
41
70
|
error: string | null;
|
|
42
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* Poll the payer-facing refund status endpoint at `/api/public/refunds/:id/status`.
|
|
74
|
+
* UNAUTHENTICATED — safe to use in customer-facing receipt / order-tracking UIs.
|
|
75
|
+
*
|
|
76
|
+
* Returns `{ data, status, error }` where `data.phase` buckets the raw refund
|
|
77
|
+
* state into `queued | sent | completed | failed` for a stable status pill.
|
|
78
|
+
* Polling stops automatically once `data.terminal === true`.
|
|
79
|
+
*/
|
|
80
|
+
declare function useFragmentRefund(refundId: string | null, opts?: {
|
|
81
|
+
intervalMs?: number;
|
|
82
|
+
}): {
|
|
83
|
+
data: PublicRefundStatus | null;
|
|
84
|
+
status: "idle" | "loading" | "ready" | "error";
|
|
85
|
+
error: string | null;
|
|
86
|
+
};
|
|
43
87
|
interface FragmentPayCheckoutProps {
|
|
44
88
|
intentId: string;
|
|
45
89
|
/** Called once the intent transitions to `settled`. */
|
|
@@ -96,4 +140,4 @@ interface FragmentPayButtonProps extends UseCreateIntentOptions {
|
|
|
96
140
|
}
|
|
97
141
|
declare function FragmentPayButton({ input, label, onSettled, onError, className, style, ...opts }: FragmentPayButtonProps): React.JSX.Element;
|
|
98
142
|
|
|
99
|
-
export { type FetchWithRetryOpts, type FragToken, FragmentPayButton, type FragmentPayButtonProps, FragmentPayCheckout, type FragmentPayCheckoutProps, FragmentPayProvider, type FragmentPayProviderProps, type IntentStatus, type PublicIntent, type UseCreateIntentOptions, fetchWithRetry, useCreateIntent, useFragmentIntent, useFragmentTokens };
|
|
143
|
+
export { type FetchWithRetryOpts, type FragToken, FragmentPayButton, type FragmentPayButtonProps, FragmentPayCheckout, type FragmentPayCheckoutProps, FragmentPayProvider, type FragmentPayProviderProps, type IntentStatus, type PublicIntent, type PublicRefundStatus, type RefundPhase, type RefundStatus, type UseCreateIntentOptions, fetchWithRetry, refundPhase, useCreateIntent, useFragmentIntent, useFragmentRefund, useFragmentTokens };
|
package/dist/index.js
CHANGED
|
@@ -34,8 +34,10 @@ __export(index_exports, {
|
|
|
34
34
|
FragmentPayCheckout: () => FragmentPayCheckout,
|
|
35
35
|
FragmentPayProvider: () => FragmentPayProvider,
|
|
36
36
|
fetchWithRetry: () => fetchWithRetry,
|
|
37
|
+
refundPhase: () => refundPhase,
|
|
37
38
|
useCreateIntent: () => useCreateIntent,
|
|
38
39
|
useFragmentIntent: () => useFragmentIntent,
|
|
40
|
+
useFragmentRefund: () => useFragmentRefund,
|
|
39
41
|
useFragmentTokens: () => useFragmentTokens
|
|
40
42
|
});
|
|
41
43
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -44,7 +46,9 @@ var import_jsx_runtime = require("react/jsx-runtime");
|
|
|
44
46
|
var FragContext = React.createContext(null);
|
|
45
47
|
function FragmentPayProvider({ publicKey, baseUrl, children }) {
|
|
46
48
|
if (typeof publicKey !== "string" || !/^pk_(test|live)_[A-Za-z0-9_-]{8,}$/.test(publicKey)) {
|
|
47
|
-
throw new Error(
|
|
49
|
+
throw new Error(
|
|
50
|
+
"[fragmentpay] publicKey must match pk_test_\u2026 or pk_live_\u2026 (min 8 chars after prefix)"
|
|
51
|
+
);
|
|
48
52
|
}
|
|
49
53
|
if (baseUrl !== void 0) {
|
|
50
54
|
try {
|
|
@@ -54,7 +58,7 @@ function FragmentPayProvider({ publicKey, baseUrl, children }) {
|
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
const value = React.useMemo(
|
|
57
|
-
() => ({ publicKey, baseUrl: (baseUrl ?? "https://frag.
|
|
61
|
+
() => ({ publicKey, baseUrl: (baseUrl ?? "https://frag.cash").replace(/\/$/, "") }),
|
|
58
62
|
[publicKey, baseUrl]
|
|
59
63
|
);
|
|
60
64
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FragContext.Provider, { value, children });
|
|
@@ -117,6 +121,18 @@ async function fetchWithRetry(input, init = {}) {
|
|
|
117
121
|
attempt += 1;
|
|
118
122
|
}
|
|
119
123
|
}
|
|
124
|
+
function refundPhase(status) {
|
|
125
|
+
switch (status) {
|
|
126
|
+
case "succeeded":
|
|
127
|
+
return "completed";
|
|
128
|
+
case "processing":
|
|
129
|
+
return "sent";
|
|
130
|
+
case "failed":
|
|
131
|
+
return "failed";
|
|
132
|
+
default:
|
|
133
|
+
return "queued";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
120
136
|
function useFragmentIntent(intentId, opts = {}) {
|
|
121
137
|
const { baseUrl } = useFrag();
|
|
122
138
|
if (intentId !== null && (typeof intentId !== "string" || intentId.length < 3)) {
|
|
@@ -143,7 +159,9 @@ function useFragmentIntent(intentId, opts = {}) {
|
|
|
143
159
|
const data = await res.json();
|
|
144
160
|
if (!alive) return;
|
|
145
161
|
setState({ data, status: "ready", error: null });
|
|
146
|
-
const terminal = ["settled", "failed", "expired", "cancelled", "refunded"].includes(
|
|
162
|
+
const terminal = ["settled", "failed", "expired", "cancelled", "refunded"].includes(
|
|
163
|
+
data.status
|
|
164
|
+
);
|
|
147
165
|
if (!terminal) timer = setTimeout(tick, interval);
|
|
148
166
|
} catch (e) {
|
|
149
167
|
if (!alive) return;
|
|
@@ -161,6 +179,51 @@ function useFragmentIntent(intentId, opts = {}) {
|
|
|
161
179
|
}, [intentId, baseUrl, interval]);
|
|
162
180
|
return state;
|
|
163
181
|
}
|
|
182
|
+
function useFragmentRefund(refundId, opts = {}) {
|
|
183
|
+
const { baseUrl } = useFrag();
|
|
184
|
+
if (refundId !== null && (typeof refundId !== "string" || refundId.length < 3)) {
|
|
185
|
+
throw new Error("[fragmentpay] useFragmentRefund: refundId must be a non-empty string or null");
|
|
186
|
+
}
|
|
187
|
+
const interval = opts.intervalMs ?? 3e3;
|
|
188
|
+
if (typeof interval !== "number" || interval < 500 || interval > 6e4) {
|
|
189
|
+
throw new Error("[fragmentpay] useFragmentRefund: intervalMs must be between 500 and 60000");
|
|
190
|
+
}
|
|
191
|
+
const [state, setState] = React.useState({ data: null, status: "idle", error: null });
|
|
192
|
+
React.useEffect(() => {
|
|
193
|
+
if (!refundId) return;
|
|
194
|
+
let alive = true;
|
|
195
|
+
let timer;
|
|
196
|
+
async function tick() {
|
|
197
|
+
try {
|
|
198
|
+
const res = await fetchWithRetry(
|
|
199
|
+
`${baseUrl}/api/public/refunds/${refundId}/status`,
|
|
200
|
+
{ maxRetries: 4 }
|
|
201
|
+
);
|
|
202
|
+
if (!res.ok) {
|
|
203
|
+
const retryAfter = parseRetryAfter(res.headers.get("retry-after")) ?? interval * 2;
|
|
204
|
+
throw Object.assign(new Error(`HTTP ${res.status}`), { retryAfter });
|
|
205
|
+
}
|
|
206
|
+
const raw = await res.json();
|
|
207
|
+
const data = { ...raw, phase: refundPhase(raw.status) };
|
|
208
|
+
if (!alive) return;
|
|
209
|
+
setState({ data, status: "ready", error: null });
|
|
210
|
+
if (!data.terminal) timer = setTimeout(tick, interval);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
if (!alive) return;
|
|
213
|
+
const wait = e.retryAfter ?? interval * 2;
|
|
214
|
+
setState((s) => ({ ...s, status: "error", error: e.message }));
|
|
215
|
+
timer = setTimeout(tick, wait);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
setState({ data: null, status: "loading", error: null });
|
|
219
|
+
tick();
|
|
220
|
+
return () => {
|
|
221
|
+
alive = false;
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
};
|
|
224
|
+
}, [refundId, baseUrl, interval]);
|
|
225
|
+
return state;
|
|
226
|
+
}
|
|
164
227
|
function FragmentPayCheckout({
|
|
165
228
|
intentId,
|
|
166
229
|
onSettled,
|
|
@@ -235,29 +298,32 @@ function useCreateIntent(opts) {
|
|
|
235
298
|
throw new Error("[fragmentpay] useCreateIntent: `endpoint` is required");
|
|
236
299
|
}
|
|
237
300
|
const [state, setState] = React.useState({ intent: null, loading: false, error: null });
|
|
238
|
-
const mutate = React.useCallback(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
301
|
+
const mutate = React.useCallback(
|
|
302
|
+
async (input) => {
|
|
303
|
+
setState({ intent: null, loading: true, error: null });
|
|
304
|
+
try {
|
|
305
|
+
const body = opts.transform ? opts.transform(input) : input;
|
|
306
|
+
const res = await fetchWithRetry(opts.endpoint, {
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: { "content-type": "application/json", ...opts.headers ?? {} },
|
|
309
|
+
body: JSON.stringify(body),
|
|
310
|
+
maxRetries: 3
|
|
311
|
+
});
|
|
312
|
+
if (!res.ok) {
|
|
313
|
+
const retryAfter = res.headers.get("retry-after");
|
|
314
|
+
const suffix = res.status === 429 && retryAfter ? ` (retry after ${retryAfter}s)` : "";
|
|
315
|
+
throw new Error(`HTTP ${res.status}${suffix}: ${await res.text()}`);
|
|
316
|
+
}
|
|
317
|
+
const intent = await res.json();
|
|
318
|
+
setState({ intent, loading: false, error: null });
|
|
319
|
+
return intent;
|
|
320
|
+
} catch (e) {
|
|
321
|
+
setState({ intent: null, loading: false, error: e.message });
|
|
322
|
+
throw e;
|
|
252
323
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
} catch (e) {
|
|
257
|
-
setState({ intent: null, loading: false, error: e.message });
|
|
258
|
-
throw e;
|
|
259
|
-
}
|
|
260
|
-
}, [opts.endpoint, opts.transform, opts.headers]);
|
|
324
|
+
},
|
|
325
|
+
[opts.endpoint, opts.transform, opts.headers]
|
|
326
|
+
);
|
|
261
327
|
return { ...state, mutate };
|
|
262
328
|
}
|
|
263
329
|
function FragmentPayButton({
|
|
@@ -283,14 +349,7 @@ function FragmentPayButton({
|
|
|
283
349
|
}
|
|
284
350
|
),
|
|
285
351
|
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "alert", style: { color: "crimson", fontSize: 12 }, children: error }),
|
|
286
|
-
intent?.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
287
|
-
FragmentPayCheckout,
|
|
288
|
-
{
|
|
289
|
-
intentId: intent.id,
|
|
290
|
-
onSettled,
|
|
291
|
-
onError
|
|
292
|
-
}
|
|
293
|
-
)
|
|
352
|
+
intent?.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FragmentPayCheckout, { intentId: intent.id, onSettled, onError })
|
|
294
353
|
] });
|
|
295
354
|
}
|
|
296
355
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -299,7 +358,9 @@ function FragmentPayButton({
|
|
|
299
358
|
FragmentPayCheckout,
|
|
300
359
|
FragmentPayProvider,
|
|
301
360
|
fetchWithRetry,
|
|
361
|
+
refundPhase,
|
|
302
362
|
useCreateIntent,
|
|
303
363
|
useFragmentIntent,
|
|
364
|
+
useFragmentRefund,
|
|
304
365
|
useFragmentTokens
|
|
305
366
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,9 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
var FragContext = React.createContext(null);
|
|
5
5
|
function FragmentPayProvider({ publicKey, baseUrl, children }) {
|
|
6
6
|
if (typeof publicKey !== "string" || !/^pk_(test|live)_[A-Za-z0-9_-]{8,}$/.test(publicKey)) {
|
|
7
|
-
throw new Error(
|
|
7
|
+
throw new Error(
|
|
8
|
+
"[fragmentpay] publicKey must match pk_test_\u2026 or pk_live_\u2026 (min 8 chars after prefix)"
|
|
9
|
+
);
|
|
8
10
|
}
|
|
9
11
|
if (baseUrl !== void 0) {
|
|
10
12
|
try {
|
|
@@ -14,7 +16,7 @@ function FragmentPayProvider({ publicKey, baseUrl, children }) {
|
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
const value = React.useMemo(
|
|
17
|
-
() => ({ publicKey, baseUrl: (baseUrl ?? "https://frag.
|
|
19
|
+
() => ({ publicKey, baseUrl: (baseUrl ?? "https://frag.cash").replace(/\/$/, "") }),
|
|
18
20
|
[publicKey, baseUrl]
|
|
19
21
|
);
|
|
20
22
|
return /* @__PURE__ */ jsx(FragContext.Provider, { value, children });
|
|
@@ -77,6 +79,18 @@ async function fetchWithRetry(input, init = {}) {
|
|
|
77
79
|
attempt += 1;
|
|
78
80
|
}
|
|
79
81
|
}
|
|
82
|
+
function refundPhase(status) {
|
|
83
|
+
switch (status) {
|
|
84
|
+
case "succeeded":
|
|
85
|
+
return "completed";
|
|
86
|
+
case "processing":
|
|
87
|
+
return "sent";
|
|
88
|
+
case "failed":
|
|
89
|
+
return "failed";
|
|
90
|
+
default:
|
|
91
|
+
return "queued";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
80
94
|
function useFragmentIntent(intentId, opts = {}) {
|
|
81
95
|
const { baseUrl } = useFrag();
|
|
82
96
|
if (intentId !== null && (typeof intentId !== "string" || intentId.length < 3)) {
|
|
@@ -103,7 +117,9 @@ function useFragmentIntent(intentId, opts = {}) {
|
|
|
103
117
|
const data = await res.json();
|
|
104
118
|
if (!alive) return;
|
|
105
119
|
setState({ data, status: "ready", error: null });
|
|
106
|
-
const terminal = ["settled", "failed", "expired", "cancelled", "refunded"].includes(
|
|
120
|
+
const terminal = ["settled", "failed", "expired", "cancelled", "refunded"].includes(
|
|
121
|
+
data.status
|
|
122
|
+
);
|
|
107
123
|
if (!terminal) timer = setTimeout(tick, interval);
|
|
108
124
|
} catch (e) {
|
|
109
125
|
if (!alive) return;
|
|
@@ -121,6 +137,51 @@ function useFragmentIntent(intentId, opts = {}) {
|
|
|
121
137
|
}, [intentId, baseUrl, interval]);
|
|
122
138
|
return state;
|
|
123
139
|
}
|
|
140
|
+
function useFragmentRefund(refundId, opts = {}) {
|
|
141
|
+
const { baseUrl } = useFrag();
|
|
142
|
+
if (refundId !== null && (typeof refundId !== "string" || refundId.length < 3)) {
|
|
143
|
+
throw new Error("[fragmentpay] useFragmentRefund: refundId must be a non-empty string or null");
|
|
144
|
+
}
|
|
145
|
+
const interval = opts.intervalMs ?? 3e3;
|
|
146
|
+
if (typeof interval !== "number" || interval < 500 || interval > 6e4) {
|
|
147
|
+
throw new Error("[fragmentpay] useFragmentRefund: intervalMs must be between 500 and 60000");
|
|
148
|
+
}
|
|
149
|
+
const [state, setState] = React.useState({ data: null, status: "idle", error: null });
|
|
150
|
+
React.useEffect(() => {
|
|
151
|
+
if (!refundId) return;
|
|
152
|
+
let alive = true;
|
|
153
|
+
let timer;
|
|
154
|
+
async function tick() {
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetchWithRetry(
|
|
157
|
+
`${baseUrl}/api/public/refunds/${refundId}/status`,
|
|
158
|
+
{ maxRetries: 4 }
|
|
159
|
+
);
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
const retryAfter = parseRetryAfter(res.headers.get("retry-after")) ?? interval * 2;
|
|
162
|
+
throw Object.assign(new Error(`HTTP ${res.status}`), { retryAfter });
|
|
163
|
+
}
|
|
164
|
+
const raw = await res.json();
|
|
165
|
+
const data = { ...raw, phase: refundPhase(raw.status) };
|
|
166
|
+
if (!alive) return;
|
|
167
|
+
setState({ data, status: "ready", error: null });
|
|
168
|
+
if (!data.terminal) timer = setTimeout(tick, interval);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
if (!alive) return;
|
|
171
|
+
const wait = e.retryAfter ?? interval * 2;
|
|
172
|
+
setState((s) => ({ ...s, status: "error", error: e.message }));
|
|
173
|
+
timer = setTimeout(tick, wait);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
setState({ data: null, status: "loading", error: null });
|
|
177
|
+
tick();
|
|
178
|
+
return () => {
|
|
179
|
+
alive = false;
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
};
|
|
182
|
+
}, [refundId, baseUrl, interval]);
|
|
183
|
+
return state;
|
|
184
|
+
}
|
|
124
185
|
function FragmentPayCheckout({
|
|
125
186
|
intentId,
|
|
126
187
|
onSettled,
|
|
@@ -195,29 +256,32 @@ function useCreateIntent(opts) {
|
|
|
195
256
|
throw new Error("[fragmentpay] useCreateIntent: `endpoint` is required");
|
|
196
257
|
}
|
|
197
258
|
const [state, setState] = React.useState({ intent: null, loading: false, error: null });
|
|
198
|
-
const mutate = React.useCallback(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
259
|
+
const mutate = React.useCallback(
|
|
260
|
+
async (input) => {
|
|
261
|
+
setState({ intent: null, loading: true, error: null });
|
|
262
|
+
try {
|
|
263
|
+
const body = opts.transform ? opts.transform(input) : input;
|
|
264
|
+
const res = await fetchWithRetry(opts.endpoint, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: { "content-type": "application/json", ...opts.headers ?? {} },
|
|
267
|
+
body: JSON.stringify(body),
|
|
268
|
+
maxRetries: 3
|
|
269
|
+
});
|
|
270
|
+
if (!res.ok) {
|
|
271
|
+
const retryAfter = res.headers.get("retry-after");
|
|
272
|
+
const suffix = res.status === 429 && retryAfter ? ` (retry after ${retryAfter}s)` : "";
|
|
273
|
+
throw new Error(`HTTP ${res.status}${suffix}: ${await res.text()}`);
|
|
274
|
+
}
|
|
275
|
+
const intent = await res.json();
|
|
276
|
+
setState({ intent, loading: false, error: null });
|
|
277
|
+
return intent;
|
|
278
|
+
} catch (e) {
|
|
279
|
+
setState({ intent: null, loading: false, error: e.message });
|
|
280
|
+
throw e;
|
|
212
281
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
} catch (e) {
|
|
217
|
-
setState({ intent: null, loading: false, error: e.message });
|
|
218
|
-
throw e;
|
|
219
|
-
}
|
|
220
|
-
}, [opts.endpoint, opts.transform, opts.headers]);
|
|
282
|
+
},
|
|
283
|
+
[opts.endpoint, opts.transform, opts.headers]
|
|
284
|
+
);
|
|
221
285
|
return { ...state, mutate };
|
|
222
286
|
}
|
|
223
287
|
function FragmentPayButton({
|
|
@@ -243,14 +307,7 @@ function FragmentPayButton({
|
|
|
243
307
|
}
|
|
244
308
|
),
|
|
245
309
|
error && /* @__PURE__ */ jsx("div", { role: "alert", style: { color: "crimson", fontSize: 12 }, children: error }),
|
|
246
|
-
intent?.id && /* @__PURE__ */ jsx(
|
|
247
|
-
FragmentPayCheckout,
|
|
248
|
-
{
|
|
249
|
-
intentId: intent.id,
|
|
250
|
-
onSettled,
|
|
251
|
-
onError
|
|
252
|
-
}
|
|
253
|
-
)
|
|
310
|
+
intent?.id && /* @__PURE__ */ jsx(FragmentPayCheckout, { intentId: intent.id, onSettled, onError })
|
|
254
311
|
] });
|
|
255
312
|
}
|
|
256
313
|
export {
|
|
@@ -258,7 +315,9 @@ export {
|
|
|
258
315
|
FragmentPayCheckout,
|
|
259
316
|
FragmentPayProvider,
|
|
260
317
|
fetchWithRetry,
|
|
318
|
+
refundPhase,
|
|
261
319
|
useCreateIntent,
|
|
262
320
|
useFragmentIntent,
|
|
321
|
+
useFragmentRefund,
|
|
263
322
|
useFragmentTokens
|
|
264
323
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fragmentpay/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "React components for Frag hosted checkout — accept any token, on any chain.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -12,9 +12,23 @@
|
|
|
12
12
|
"require": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
"files": [
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"crypto",
|
|
22
|
+
"payments",
|
|
23
|
+
"react",
|
|
24
|
+
"checkout",
|
|
25
|
+
"fragmentpay",
|
|
26
|
+
"frag"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=18",
|
|
30
|
+
"react-dom": ">=18"
|
|
31
|
+
},
|
|
18
32
|
"license": "MIT",
|
|
19
33
|
"sideEffects": false,
|
|
20
34
|
"repository": {
|
|
@@ -23,8 +37,13 @@
|
|
|
23
37
|
"directory": "sdk/frag-react"
|
|
24
38
|
},
|
|
25
39
|
"homepage": "https://github.com/fragmentpay/fragpay#readme",
|
|
26
|
-
"bugs": {
|
|
27
|
-
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/fragmentpay/fragpay/issues"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"provenance": true
|
|
46
|
+
},
|
|
28
47
|
"scripts": {
|
|
29
48
|
"build": "tsup src/index.tsx --format esm,cjs --dts --clean",
|
|
30
49
|
"prepublishOnly": "npm run build"
|