@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 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.dev. */
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.dev. */
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("[fragmentpay] publicKey must match pk_test_\u2026 or pk_live_\u2026 (min 8 chars after prefix)");
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.dev").replace(/\/$/, "") }),
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(data.status);
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(async (input) => {
239
- setState({ intent: null, loading: true, error: null });
240
- try {
241
- const body = opts.transform ? opts.transform(input) : input;
242
- const res = await fetchWithRetry(opts.endpoint, {
243
- method: "POST",
244
- headers: { "content-type": "application/json", ...opts.headers ?? {} },
245
- body: JSON.stringify(body),
246
- maxRetries: 3
247
- });
248
- if (!res.ok) {
249
- const retryAfter = res.headers.get("retry-after");
250
- const suffix = res.status === 429 && retryAfter ? ` (retry after ${retryAfter}s)` : "";
251
- throw new Error(`HTTP ${res.status}${suffix}: ${await res.text()}`);
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
- const intent = await res.json();
254
- setState({ intent, loading: false, error: null });
255
- return intent;
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("[fragmentpay] publicKey must match pk_test_\u2026 or pk_live_\u2026 (min 8 chars after prefix)");
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.dev").replace(/\/$/, "") }),
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(data.status);
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(async (input) => {
199
- setState({ intent: null, loading: true, error: null });
200
- try {
201
- const body = opts.transform ? opts.transform(input) : input;
202
- const res = await fetchWithRetry(opts.endpoint, {
203
- method: "POST",
204
- headers: { "content-type": "application/json", ...opts.headers ?? {} },
205
- body: JSON.stringify(body),
206
- maxRetries: 3
207
- });
208
- if (!res.ok) {
209
- const retryAfter = res.headers.get("retry-after");
210
- const suffix = res.status === 429 && retryAfter ? ` (retry after ${retryAfter}s)` : "";
211
- throw new Error(`HTTP ${res.status}${suffix}: ${await res.text()}`);
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
- const intent = await res.json();
214
- setState({ intent, loading: false, error: null });
215
- return intent;
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.1",
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": ["dist", "README.md", "LICENSE"],
16
- "keywords": ["crypto", "payments", "react", "checkout", "fragmentpay", "frag"],
17
- "peerDependencies": { "react": ">=18", "react-dom": ">=18" },
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": { "url": "https://github.com/fragmentpay/fragpay/issues" },
27
- "publishConfig": { "access": "public", "provenance": true },
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"