@easypayment/medusa-paypal-ui 1.0.51 → 1.0.52

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.
@@ -5,23 +5,13 @@ export async function markPaymentComplete(
5
5
  baseUrl: string,
6
6
  cartId: string,
7
7
  publishableApiKey?: string
8
- ): Promise<Record<string, any>> {
9
- try {
10
- const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/store/paypal-complete`, {
11
- method: "POST",
12
- headers: {
13
- "Content-Type": "application/json",
14
- ...(publishableApiKey ? { "x-publishable-api-key": publishableApiKey } : {}),
15
- },
16
- body: JSON.stringify({ cart_id: cartId }),
17
- credentials: "include",
18
- })
19
- const data = await resp.json().catch(() => ({}))
20
- return data || {}
21
- } catch (e) {
22
- console.warn("[PayPal] paypal-complete call failed:", e)
23
- return {}
24
- }
8
+ ): Promise<Record<string, unknown>> {
9
+ const http = createHttpClient({ baseUrl, publishableApiKey })
10
+ return http.request<Record<string, unknown>>(`/store/paypal-complete`, {
11
+ method: "POST",
12
+ headers: { "Content-Type": "application/json" },
13
+ body: JSON.stringify({ cart_id: cartId }),
14
+ })
25
15
  }
26
16
 
27
17
  export function createPayPalStoreApi(opts: HttpOptions) {
@@ -1,18 +1,24 @@
1
1
  "use client"
2
2
 
3
- import React, { useMemo, useState } from "react"
3
+ import React, { useMemo, useRef, useState } from "react"
4
4
  import {
5
5
  PayPalCardFieldsProvider,
6
6
  PayPalNumberField,
7
7
  PayPalExpiryField,
8
8
  PayPalCVVField,
9
9
  usePayPalCardFields,
10
+ usePayPalScriptReducer,
10
11
  } from "@paypal/react-paypal-js"
11
12
 
12
13
  import { createPayPalStoreApi, markPaymentComplete } from "../client/paypal"
13
14
  import type { PayPalConfig } from "../client/types"
15
+ import { isNextRouterError } from "../utils/next-errors"
16
+ import { showProcessingOverlay, hideProcessingOverlay } from "../utils/processing-overlay"
14
17
 
15
- const SPIN_STYLE = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`
18
+ const SPIN_STYLE = `
19
+ @keyframes _pp_spin { to { transform: rotate(360deg) } }
20
+ @keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
21
+ `
16
22
 
17
23
  const cardStyle = {
18
24
  input: {
@@ -74,9 +80,14 @@ function SubmitButton({
74
80
  <button
75
81
  type="button"
76
82
  disabled={isDisabled}
83
+ aria-busy={disabled}
77
84
  onClick={() => {
78
85
  onSubmit()
79
- cardFieldsForm?.submit()
86
+ try {
87
+ cardFieldsForm?.submit()
88
+ } catch {
89
+ // submit() error is handled by PayPalCardFieldsProvider onError
90
+ }
80
91
  }}
81
92
  style={{
82
93
  width: "100%",
@@ -137,9 +148,37 @@ export function PayPalAdvancedCard(props: {
137
148
 
138
149
  const [error, setError] = useState<string | null>(null)
139
150
  const [submitting, setSubmitting] = useState(false)
151
+ const submittingRef = useRef(false)
152
+ const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer()
140
153
 
141
154
  if (!config.currency_supported) return null
142
155
 
156
+ if (isRejected) {
157
+ return (
158
+ <div
159
+ role="alert"
160
+ style={{
161
+ display: "flex",
162
+ alignItems: "flex-start",
163
+ gap: 8,
164
+ padding: "10px 14px",
165
+ background: "#fef2f2",
166
+ border: "1px solid #fecaca",
167
+ borderRadius: 8,
168
+ fontSize: 13,
169
+ color: "#b91c1c",
170
+ lineHeight: 1.5,
171
+ }}
172
+ >
173
+ <span style={{ flexShrink: 0, fontSize: 15 }}>⚠️</span>
174
+ <span>
175
+ PayPal failed to load. Please refresh the page or try a different
176
+ payment method.
177
+ </span>
178
+ </div>
179
+ )
180
+ }
181
+
143
182
  if (!config.client_token) {
144
183
  return (
145
184
  <div
@@ -159,50 +198,138 @@ export function PayPalAdvancedCard(props: {
159
198
 
160
199
  const isSandbox = config.environment === "sandbox"
161
200
 
201
+ if (isPending) {
202
+ return (
203
+ <div
204
+ role="status"
205
+ aria-label="Loading PayPal"
206
+ style={{
207
+ display: "flex",
208
+ alignItems: "center",
209
+ justifyContent: "center",
210
+ gap: 10,
211
+ padding: "18px 16px",
212
+ background: "#f9fafb",
213
+ border: "1px solid #e5e7eb",
214
+ borderRadius: 8,
215
+ minHeight: 55,
216
+ }}
217
+ >
218
+ <style>{SPIN_STYLE}</style>
219
+ <div
220
+ style={{
221
+ width: 22,
222
+ height: 22,
223
+ borderRadius: "50%",
224
+ border: "2.5px solid #e5e7eb",
225
+ borderTopColor: "#0070ba",
226
+ animation: "_pp_spin .7s linear infinite",
227
+ flexShrink: 0,
228
+ }}
229
+ />
230
+ <div style={{ fontSize: 13, fontWeight: 500, color: "#6b7280" }}>
231
+ Loading card fields…
232
+ </div>
233
+ </div>
234
+ )
235
+ }
236
+
237
+ if (!isResolved) return null
238
+
162
239
  return (
163
240
  <div style={{ position: "relative" }}>
164
241
  <style>{SPIN_STYLE}</style>
165
242
 
166
243
  {submitting && (
167
244
  <div
245
+ role="status"
246
+ aria-label="Processing payment"
168
247
  style={{
169
- position: "absolute",
248
+ position: "fixed",
170
249
  inset: 0,
171
- zIndex: 20,
172
- background: "rgba(255,255,255,0.92)",
173
- borderRadius: 12,
250
+ zIndex: 9999,
251
+ background: "rgba(255,255,255,0.96)",
174
252
  display: "flex",
175
253
  flexDirection: "column",
176
254
  alignItems: "center",
177
255
  justifyContent: "center",
178
- gap: 14,
179
- minHeight: 180,
256
+ gap: 20,
180
257
  }}
181
258
  >
182
- <div
183
- style={{
184
- width: 36,
185
- height: 36,
186
- borderRadius: "50%",
187
- border: "3px solid #dbeafe",
188
- borderTopColor: "#2563eb",
189
- animation: "_pp_spin .75s linear infinite",
190
- }}
191
- />
259
+ <div style={{ position: "relative", width: 56, height: 56 }}>
260
+ <div
261
+ style={{
262
+ position: "absolute",
263
+ inset: 0,
264
+ borderRadius: "50%",
265
+ border: "3px solid #e5e7eb",
266
+ }}
267
+ />
268
+ <div
269
+ style={{
270
+ position: "absolute",
271
+ inset: 0,
272
+ borderRadius: "50%",
273
+ border: "3px solid transparent",
274
+ borderTopColor: "#2563eb",
275
+ animation: "_pp_spin .8s linear infinite",
276
+ }}
277
+ />
278
+ <svg
279
+ viewBox="0 0 24 24"
280
+ fill="none"
281
+ stroke="#2563eb"
282
+ strokeWidth="1.8"
283
+ strokeLinecap="round"
284
+ strokeLinejoin="round"
285
+ style={{
286
+ position: "absolute",
287
+ top: "50%",
288
+ left: "50%",
289
+ transform: "translate(-50%, -50%)",
290
+ width: 24,
291
+ height: 24,
292
+ }}
293
+ >
294
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
295
+ </svg>
296
+ </div>
192
297
  <div style={{ textAlign: "center" }}>
193
- <div style={{ fontSize: 15, fontWeight: 600, color: "#111827" }}>
194
- Processing your payment
298
+ <div style={{ fontSize: 18, fontWeight: 600, color: "#111827", letterSpacing: "-0.01em" }}>
299
+ Processing your payment
195
300
  </div>
196
- <div style={{ fontSize: 13, color: "#6b7280", marginTop: 4 }}>
197
- Please do not close or refresh this page
301
+ <div style={{ fontSize: 14, color: "#6b7280", marginTop: 6, lineHeight: 1.5 }}>
302
+ This may take a few moments. Please do not close<br />
303
+ or refresh this page.
198
304
  </div>
199
305
  </div>
306
+ <div
307
+ style={{
308
+ width: 200,
309
+ height: 3,
310
+ background: "#e5e7eb",
311
+ borderRadius: 3,
312
+ overflow: "hidden",
313
+ marginTop: 4,
314
+ }}
315
+ >
316
+ <div
317
+ style={{
318
+ width: "40%",
319
+ height: "100%",
320
+ background: "linear-gradient(90deg, #2563eb, #1d4ed8)",
321
+ borderRadius: 3,
322
+ animation: "_pp_progress 1.5s ease-in-out infinite",
323
+ }}
324
+ />
325
+ </div>
200
326
  </div>
201
327
  )}
202
328
 
203
329
  <PayPalCardFieldsProvider
204
330
  style={cardStyle}
205
331
  createOrder={async () => {
332
+ if (submittingRef.current) throw new Error("Payment already processing")
206
333
  setError(null)
207
334
  const r = await api.createOrder(cartId, true)
208
335
  return r.id
@@ -210,27 +337,34 @@ export function PayPalAdvancedCard(props: {
210
337
  onApprove={async (data: { orderID?: string }) => {
211
338
  try {
212
339
  setError(null)
340
+ showProcessingOverlay()
213
341
  const orderId = String(data?.orderID || "")
214
342
  const result = await api.captureOrder(cartId, orderId)
215
343
 
216
344
  const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey)
217
345
 
218
- onPaid?.({ ...result, ...completeResult })
346
+ await onPaid?.({ ...result, ...completeResult })
219
347
  } catch (e: unknown) {
348
+ if (isNextRouterError(e)) throw e
349
+ hideProcessingOverlay()
350
+ submittingRef.current = false
351
+ setSubmitting(false)
220
352
  const msg = e instanceof Error ? e.message : "Card payment failed"
221
353
  setError(msg)
222
354
  onError?.(msg)
223
- } finally {
224
- setSubmitting(false)
225
355
  }
226
356
  }}
227
357
  onCancel={() => {
358
+ hideProcessingOverlay()
359
+ submittingRef.current = false
228
360
  setSubmitting(false)
229
361
  }}
230
362
  onError={(e: Error | { message?: string }) => {
231
363
  const msg = e instanceof Error ? e.message : e?.message || "CardFields error"
232
364
  setError(msg)
233
365
  onError?.(msg)
366
+ hideProcessingOverlay()
367
+ submittingRef.current = false
234
368
  setSubmitting(false)
235
369
  }}
236
370
  >
@@ -318,13 +452,17 @@ export function PayPalAdvancedCard(props: {
318
452
  disabled={submitting}
319
453
  label={submitting ? "Processing…" : "Pay by Card"}
320
454
  onSubmit={() => {
455
+ if (submittingRef.current) return
456
+ submittingRef.current = true
321
457
  setError(null)
322
458
  setSubmitting(true)
459
+ showProcessingOverlay()
323
460
  }}
324
461
  />
325
462
 
326
463
  {error && (
327
464
  <div
465
+ role="alert"
328
466
  style={{
329
467
  display: "flex",
330
468
  alignItems: "flex-start",
@@ -7,7 +7,7 @@ export function PayPalCurrencyNotice({ config }: { config: PayPalConfig }) {
7
7
  if (config.currency_supported) return null
8
8
 
9
9
  return (
10
- <div style={{ padding: 12, border: "1px solid #ddd", borderRadius: 10 }}>
10
+ <div role="alert" style={{ padding: 12, border: "1px solid #ddd", borderRadius: 10 }}>
11
11
  <div style={{ fontWeight: 600, marginBottom: 6 }}>PayPal currency issue</div>
12
12
  <ul style={{ margin: 0, paddingLeft: 18 }}>
13
13
  {(config.currency_errors || []).map((e) => (
@@ -25,6 +25,8 @@ const SPIN_STYLE = `@keyframes _pp_section_spin { to { transform: rotate(360deg)
25
25
  function SessionInitCard() {
26
26
  return (
27
27
  <div
28
+ role="status"
29
+ aria-label="Setting up payment"
28
30
  style={{
29
31
  display: "flex",
30
32
  alignItems: "center",
@@ -58,6 +60,8 @@ function SessionInitCard() {
58
60
  function ConfigLoadingCard() {
59
61
  return (
60
62
  <div
63
+ role="status"
64
+ aria-label="Connecting to PayPal"
61
65
  style={{
62
66
  display: "flex",
63
67
  alignItems: "center",
@@ -95,6 +99,7 @@ function ConfigLoadingCard() {
95
99
  function ErrorCard({ message }: { message: string }) {
96
100
  return (
97
101
  <div
102
+ role="alert"
98
103
  style={{
99
104
  padding: "12px 16px",
100
105
  background: "#fef2f2",
@@ -140,9 +145,9 @@ export function PayPalPaymentSection({
140
145
  })
141
146
 
142
147
  const handlePaid = useCallback(
143
- (captureResult: unknown) => {
148
+ async (captureResult: unknown) => {
144
149
  onPaid?.(captureResult)
145
- onSuccess?.(cartId)
150
+ await onSuccess?.(cartId)
146
151
  },
147
152
  [cartId, onPaid, onSuccess]
148
153
  )
@@ -25,7 +25,7 @@ export function PayPalProvider(props: {
25
25
  "disable-funding": disableFunding,
26
26
  "data-partner-attribution-id": BN_CODE,
27
27
  }
28
- }, [config, intent, disableFunding])
28
+ }, [config.client_id, config.currency, config.client_token, intent, disableFunding])
29
29
 
30
30
  return (
31
31
  <PayPalScriptProvider
@@ -1,10 +1,15 @@
1
1
  "use client"
2
- import React, { useMemo, useState } from "react"
2
+ import React, { useCallback, useMemo, useState } from "react"
3
3
  import { PayPalButtons, usePayPalScriptReducer } from "@paypal/react-paypal-js"
4
4
  import { createPayPalStoreApi, markPaymentComplete } from "../client/paypal"
5
5
  import type { PayPalConfig } from "../client/types"
6
+ import { isNextRouterError } from "../utils/next-errors"
7
+ import { showProcessingOverlay, hideProcessingOverlay } from "../utils/processing-overlay"
6
8
 
7
- const SPIN_STYLE = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`
9
+ const SPIN_STYLE = `
10
+ @keyframes _pp_spin { to { transform: rotate(360deg) } }
11
+ @keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
12
+ `
8
13
 
9
14
  const BUTTON_WIDTH_MAP: Record<string, string> = {
10
15
  small: "300px",
@@ -29,7 +34,14 @@ export function PayPalSmartButtons(props: {
29
34
 
30
35
  const [error, setError] = useState<string | null>(null)
31
36
  const [processing, setProcessing] = useState(false)
32
- const [{ isPending, isResolved }] = usePayPalScriptReducer()
37
+ const [buttonsReady, setButtonsReady] = useState(false)
38
+ const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer()
39
+
40
+ const handleInit = useCallback(() => {
41
+ setButtonsReady(true)
42
+ }, [])
43
+
44
+ const showSpinner = isPending || (isResolved && !buttonsReady)
33
45
 
34
46
  if (!config.currency_supported) return null
35
47
 
@@ -41,43 +53,94 @@ export function PayPalSmartButtons(props: {
41
53
 
42
54
  {processing && (
43
55
  <div
56
+ role="status"
57
+ aria-label="Processing payment"
44
58
  style={{
45
- position: "absolute",
59
+ position: "fixed",
46
60
  inset: 0,
47
- zIndex: 10,
48
- background: "rgba(255,255,255,0.90)",
49
- borderRadius: 8,
61
+ zIndex: 9999,
62
+ background: "rgba(255,255,255,0.96)",
50
63
  display: "flex",
51
64
  flexDirection: "column",
52
65
  alignItems: "center",
53
66
  justifyContent: "center",
54
- gap: 10,
55
- minHeight: 60,
67
+ gap: 20,
56
68
  }}
57
69
  >
58
- <div
59
- style={{
60
- width: 28,
61
- height: 28,
62
- borderRadius: "50%",
63
- border: "2.5px solid #e5e7eb",
64
- borderTopColor: "#0070ba",
65
- animation: "_pp_spin .7s linear infinite",
66
- }}
67
- />
70
+ <div style={{ position: "relative", width: 56, height: 56 }}>
71
+ <div
72
+ style={{
73
+ position: "absolute",
74
+ inset: 0,
75
+ borderRadius: "50%",
76
+ border: "3px solid #e5e7eb",
77
+ }}
78
+ />
79
+ <div
80
+ style={{
81
+ position: "absolute",
82
+ inset: 0,
83
+ borderRadius: "50%",
84
+ border: "3px solid transparent",
85
+ borderTopColor: "#0070ba",
86
+ animation: "_pp_spin .8s linear infinite",
87
+ }}
88
+ />
89
+ <svg
90
+ viewBox="0 0 24 24"
91
+ fill="none"
92
+ stroke="#0070ba"
93
+ strokeWidth="1.8"
94
+ strokeLinecap="round"
95
+ strokeLinejoin="round"
96
+ style={{
97
+ position: "absolute",
98
+ top: "50%",
99
+ left: "50%",
100
+ transform: "translate(-50%, -50%)",
101
+ width: 24,
102
+ height: 24,
103
+ }}
104
+ >
105
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
106
+ </svg>
107
+ </div>
68
108
  <div style={{ textAlign: "center" }}>
69
- <div style={{ fontSize: 13, color: "#374151", fontWeight: 500 }}>
70
- Processing your payment
109
+ <div style={{ fontSize: 18, fontWeight: 600, color: "#111827", letterSpacing: "-0.01em" }}>
110
+ Processing your payment
71
111
  </div>
72
- <div style={{ fontSize: 12, color: "#6b7280", marginTop: 4 }}>
73
- Please do not close or refresh this page
112
+ <div style={{ fontSize: 14, color: "#6b7280", marginTop: 6, lineHeight: 1.5 }}>
113
+ This may take a few moments. Please do not close<br />
114
+ or refresh this page.
74
115
  </div>
75
116
  </div>
117
+ <div
118
+ style={{
119
+ width: 200,
120
+ height: 3,
121
+ background: "#e5e7eb",
122
+ borderRadius: 3,
123
+ overflow: "hidden",
124
+ marginTop: 4,
125
+ }}
126
+ >
127
+ <div
128
+ style={{
129
+ width: "40%",
130
+ height: "100%",
131
+ background: "linear-gradient(90deg, #0070ba, #003087)",
132
+ borderRadius: 3,
133
+ animation: "_pp_progress 1.5s ease-in-out infinite",
134
+ }}
135
+ />
136
+ </div>
76
137
  </div>
77
138
  )}
78
139
 
79
- {isPending && (
140
+ {showSpinner && (
80
141
  <div
142
+ role="status"
143
+ aria-label="Loading PayPal"
81
144
  style={{
82
145
  display: "flex",
83
146
  alignItems: "center",
@@ -107,7 +170,31 @@ export function PayPalSmartButtons(props: {
107
170
  </div>
108
171
  )}
109
172
 
110
- {config.environment === "sandbox" && isResolved && (
173
+ {isRejected && (
174
+ <div
175
+ role="alert"
176
+ style={{
177
+ display: "flex",
178
+ alignItems: "flex-start",
179
+ gap: 8,
180
+ padding: "10px 14px",
181
+ background: "#fef2f2",
182
+ border: "1px solid #fecaca",
183
+ borderRadius: 8,
184
+ fontSize: 13,
185
+ color: "#b91c1c",
186
+ lineHeight: 1.5,
187
+ }}
188
+ >
189
+ <span style={{ flexShrink: 0, fontSize: 15 }}>⚠️</span>
190
+ <span>
191
+ PayPal failed to load. Please refresh the page or try a different
192
+ payment method.
193
+ </span>
194
+ </div>
195
+ )}
196
+
197
+ {config.environment === "sandbox" && buttonsReady && (
111
198
  <div
112
199
  style={{
113
200
  display: "flex",
@@ -144,8 +231,10 @@ export function PayPalSmartButtons(props: {
144
231
  )}
145
232
 
146
233
  {isResolved && (
234
+ <div style={buttonsReady ? undefined : { position: "absolute", left: -9999, opacity: 0, pointerEvents: "none" }}>
147
235
  <PayPalButtons
148
236
  forceReRender={[config.currency, config.intent, cartId]}
237
+ onInit={handleInit}
149
238
  style={{
150
239
  layout: "vertical",
151
240
  color: config.button_color,
@@ -154,42 +243,56 @@ export function PayPalSmartButtons(props: {
154
243
  height: config.button_height,
155
244
  }}
156
245
  createOrder={async () => {
246
+ if (processing) throw new Error("Payment already processing")
157
247
  setError(null)
158
- const r = await api.createOrder(cartId)
159
- return r.id
248
+ setProcessing(true)
249
+ try {
250
+ const r = await api.createOrder(cartId)
251
+ return r.id
252
+ } catch (e: unknown) {
253
+ setProcessing(false)
254
+ throw e
255
+ }
160
256
  }}
161
257
  onApprove={async (data: { orderID?: string }) => {
162
258
  try {
163
259
  setProcessing(true)
260
+ showProcessingOverlay()
164
261
  setError(null)
165
262
  const orderId = String(data?.orderID || "")
263
+ if (!orderId) throw new Error("PayPal order ID is missing from approval response")
166
264
  const result = await api.captureOrder(cartId, orderId)
167
265
 
168
266
  const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey)
169
267
 
170
- onPaid?.({ ...result, ...completeResult })
268
+ await onPaid?.({ ...result, ...completeResult })
171
269
  } catch (e: unknown) {
270
+ if (isNextRouterError(e)) throw e
271
+ hideProcessingOverlay()
272
+ setProcessing(false)
172
273
  const msg = e instanceof Error ? e.message : "Payment capture failed"
173
274
  setError(msg)
174
275
  onError?.(msg)
175
- } finally {
176
- setProcessing(false)
177
276
  }
178
277
  }}
179
278
  onCancel={() => {
279
+ hideProcessingOverlay()
180
280
  setProcessing(false)
181
281
  }}
182
282
  onError={(err: Error | { message?: string }) => {
283
+ hideProcessingOverlay()
183
284
  setProcessing(false)
184
285
  const msg = err instanceof Error ? err.message : err?.message || "PayPal error"
185
286
  setError(msg)
186
287
  onError?.(msg)
187
288
  }}
188
289
  />
290
+ </div>
189
291
  )}
190
292
 
191
293
  {error ? (
192
294
  <div
295
+ role="alert"
193
296
  style={{
194
297
  marginTop: 10,
195
298
  display: "flex",
@@ -11,9 +11,18 @@ type Args = {
11
11
  enabled?: boolean
12
12
  }
13
13
 
14
+ const MAX_CACHE_ENTRIES = 50
14
15
  const _cache = new Map<string, { config: PayPalConfig; at: number }>()
15
16
  const CACHE_TTL = 5 * 60 * 1000
16
17
 
18
+ function cacheSet(key: string, value: { config: PayPalConfig; at: number }) {
19
+ if (_cache.size >= MAX_CACHE_ENTRIES) {
20
+ const oldest = _cache.keys().next().value
21
+ if (oldest !== undefined) _cache.delete(oldest)
22
+ }
23
+ _cache.set(key, value)
24
+ }
25
+
17
26
  function cacheKey(baseUrl: string, cartId?: string) {
18
27
  return `${baseUrl}::${cartId ?? ""}`
19
28
  }
@@ -66,12 +75,12 @@ export function usePayPalConfig({
66
75
  try {
67
76
  const cfg = await api.getConfig(cartId, controller.signal)
68
77
  if (!mounted || id !== fetchIdRef.current) return
69
- _cache.set(k, { config: cfg, at: Date.now() })
78
+ cacheSet(k, { config: cfg, at: Date.now() })
70
79
  setConfig(cfg)
71
- } catch (e: any) {
72
- if (e?.name === "AbortError") return
80
+ } catch (e: unknown) {
81
+ if (e instanceof Error && e.name === "AbortError") return
73
82
  if (!mounted || id !== fetchIdRef.current) return
74
- setError(e?.message || "Failed to load PayPal config")
83
+ setError(e instanceof Error ? e.message : "Failed to load PayPal config")
75
84
  } finally {
76
85
  if (mounted && id === fetchIdRef.current) setLoading(false)
77
86
  }