@easypayment/medusa-paypal-ui 1.2.8 → 1.3.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 +82 -0
- package/LICENSE +21 -1
- package/README.md +8 -2
- package/dist/index.cjs +605 -676
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -28
- package/dist/index.d.ts +101 -28
- package/dist/index.mjs +601 -676
- package/dist/index.mjs.map +1 -1
- package/dist/order.cjs +11 -8
- package/dist/order.cjs.map +1 -1
- package/dist/order.d.cts +1 -1
- package/dist/order.d.ts +1 -1
- package/dist/order.mjs +10 -8
- package/dist/order.mjs.map +1 -1
- package/package.json +43 -4
- package/src/adapters/MedusaNextPayPalAdapter.tsx +48 -10
- package/src/client/config-cache.ts +96 -0
- package/src/client/http.ts +24 -5
- package/src/client/paypal.ts +28 -6
- package/src/components/PayPalAdvancedCard.tsx +88 -214
- package/src/components/PayPalPaymentSection.tsx +17 -6
- package/src/components/PayPalProvider.tsx +4 -1
- package/src/components/PayPalSmartButtons.tsx +92 -224
- package/src/components/internal/ui.tsx +221 -0
- package/src/hooks/usePayPalConfig.ts +43 -32
- package/src/hooks/usePayPalPaymentMethods.ts +59 -54
- package/src/index.ts +4 -0
- package/src/order.ts +16 -8
- package/src/utils/processing-overlay.ts +25 -21
|
@@ -1,52 +1,64 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
/* eslint-disable react-hooks/set-state-in-effect --
|
|
4
|
+
This hook mirrors an external module-level cache into React state inside
|
|
5
|
+
its fetch effect (cache seed, enabled gate, fetch results). Each sync set
|
|
6
|
+
runs at most once per key change, not per render, so no render cascade. */
|
|
7
|
+
|
|
3
8
|
import { useEffect, useMemo, useRef, useState } from "react"
|
|
4
9
|
import { createPayPalStoreApi } from "../client/paypal"
|
|
10
|
+
import {
|
|
11
|
+
configCacheKey,
|
|
12
|
+
fetchPayPalConfigShared,
|
|
13
|
+
getCachedPayPalConfig,
|
|
14
|
+
} from "../client/config-cache"
|
|
5
15
|
import type { PayPalConfig } from "../client/types"
|
|
6
16
|
|
|
7
17
|
type Args = {
|
|
8
18
|
baseUrl: string
|
|
9
19
|
publishableApiKey?: string
|
|
10
20
|
cartId?: string
|
|
21
|
+
/**
|
|
22
|
+
* The cart's current currency code (e.g. "usd"). Optional, but recommended:
|
|
23
|
+
* when provided it becomes part of the cache key, so a region/currency
|
|
24
|
+
* switch on the same cart refetches the config instead of serving a stale
|
|
25
|
+
* one for up to 5 minutes.
|
|
26
|
+
*/
|
|
27
|
+
currencyCode?: string
|
|
28
|
+
credentials?: RequestCredentials
|
|
11
29
|
enabled?: boolean
|
|
12
30
|
}
|
|
13
31
|
|
|
14
|
-
const MAX_CACHE_ENTRIES = 50
|
|
15
|
-
const _cache = new Map<string, { config: PayPalConfig; at: number }>()
|
|
16
|
-
const CACHE_TTL = 5 * 60 * 1000
|
|
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
|
-
|
|
26
|
-
function cacheKey(baseUrl: string, cartId?: string) {
|
|
27
|
-
return `${baseUrl}::${cartId ?? ""}`
|
|
28
|
-
}
|
|
29
|
-
|
|
30
32
|
export function usePayPalConfig({
|
|
31
33
|
baseUrl,
|
|
32
34
|
publishableApiKey,
|
|
33
35
|
cartId,
|
|
36
|
+
currencyCode,
|
|
37
|
+
credentials,
|
|
34
38
|
enabled = true,
|
|
35
39
|
}: Args) {
|
|
36
40
|
const api = useMemo(
|
|
37
|
-
() => createPayPalStoreApi({ baseUrl, publishableApiKey }),
|
|
38
|
-
[baseUrl, publishableApiKey]
|
|
41
|
+
() => createPayPalStoreApi({ baseUrl, publishableApiKey, credentials }),
|
|
42
|
+
[baseUrl, publishableApiKey, credentials]
|
|
39
43
|
)
|
|
40
44
|
|
|
41
|
-
const key =
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
const key = configCacheKey({
|
|
46
|
+
baseUrl,
|
|
47
|
+
publishableApiKey,
|
|
48
|
+
cartId,
|
|
49
|
+
currencyCode,
|
|
50
|
+
})
|
|
51
|
+
const seedConfig = getCachedPayPalConfig(key)
|
|
44
52
|
|
|
45
53
|
const [config, setConfig] = useState<PayPalConfig | null>(seedConfig)
|
|
46
54
|
const [loading, setLoading] = useState<boolean>(
|
|
47
55
|
seedConfig === null && enabled
|
|
48
56
|
)
|
|
49
57
|
const [error, setError] = useState<string | null>(null)
|
|
58
|
+
// Bumping this re-runs the effect after an error, bypassing nothing (the
|
|
59
|
+
// failed fetch was never cached) — gives consumers a retry() without
|
|
60
|
+
// tearing down the hook.
|
|
61
|
+
const [retryToken, setRetryToken] = useState(0)
|
|
50
62
|
|
|
51
63
|
const fetchIdRef = useRef(0)
|
|
52
64
|
|
|
@@ -57,10 +69,9 @@ export function usePayPalConfig({
|
|
|
57
69
|
return
|
|
58
70
|
}
|
|
59
71
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
setConfig(cached.config)
|
|
72
|
+
const cached = getCachedPayPalConfig(key)
|
|
73
|
+
if (cached) {
|
|
74
|
+
setConfig(cached)
|
|
64
75
|
setLoading(false)
|
|
65
76
|
setError(null)
|
|
66
77
|
return
|
|
@@ -68,18 +79,17 @@ export function usePayPalConfig({
|
|
|
68
79
|
|
|
69
80
|
const id = ++fetchIdRef.current
|
|
70
81
|
let mounted = true
|
|
71
|
-
const controller = new AbortController()
|
|
72
82
|
|
|
73
83
|
setLoading(true)
|
|
74
84
|
setError(null)
|
|
75
85
|
;(async () => {
|
|
76
86
|
try {
|
|
77
|
-
const cfg = await
|
|
87
|
+
const cfg = await fetchPayPalConfigShared(key, () =>
|
|
88
|
+
api.getConfig(cartId)
|
|
89
|
+
)
|
|
78
90
|
if (!mounted || id !== fetchIdRef.current) return
|
|
79
|
-
cacheSet(k, { config: cfg, at: Date.now() })
|
|
80
91
|
setConfig(cfg)
|
|
81
92
|
} catch (e: unknown) {
|
|
82
|
-
if (e instanceof Error && e.name === "AbortError") return
|
|
83
93
|
if (!mounted || id !== fetchIdRef.current) return
|
|
84
94
|
setError(
|
|
85
95
|
e instanceof Error ? e.message : "Failed to load PayPal config"
|
|
@@ -91,9 +101,10 @@ export function usePayPalConfig({
|
|
|
91
101
|
|
|
92
102
|
return () => {
|
|
93
103
|
mounted = false
|
|
94
|
-
controller.abort()
|
|
95
104
|
}
|
|
96
|
-
}, [api,
|
|
105
|
+
}, [api, key, cartId, enabled, retryToken])
|
|
106
|
+
|
|
107
|
+
const retry = useMemo(() => () => setRetryToken((t) => t + 1), [])
|
|
97
108
|
|
|
98
|
-
return { config, loading, error }
|
|
109
|
+
return { config, loading, error, retry }
|
|
99
110
|
}
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
/* eslint-disable react-hooks/set-state-in-effect --
|
|
4
|
+
This hook mirrors an external module-level cache into React state inside
|
|
5
|
+
its fetch effect (cache seed, enabled gate, fetch results). Each sync set
|
|
6
|
+
runs at most once per key change, not per render, so no render cascade. */
|
|
7
|
+
|
|
3
8
|
import { useEffect, useMemo, useRef, useState } from "react"
|
|
4
9
|
import { createPayPalStoreApi } from "../client/paypal"
|
|
10
|
+
import { HttpError } from "../client/http"
|
|
11
|
+
import {
|
|
12
|
+
configCacheKey,
|
|
13
|
+
fetchPayPalConfigShared,
|
|
14
|
+
getCachedPayPalConfig,
|
|
15
|
+
} from "../client/config-cache"
|
|
16
|
+
import type { PayPalConfig } from "../client/types"
|
|
5
17
|
|
|
6
18
|
type Args = {
|
|
7
19
|
baseUrl: string
|
|
8
20
|
publishableApiKey?: string
|
|
9
21
|
cartId?: string
|
|
22
|
+
/** See `usePayPalConfig` — include to bust the cache on currency switches. */
|
|
23
|
+
currencyCode?: string
|
|
24
|
+
credentials?: RequestCredentials
|
|
10
25
|
enabled?: boolean
|
|
11
26
|
}
|
|
12
27
|
|
|
@@ -25,22 +40,6 @@ type Result = {
|
|
|
25
40
|
error: string | null
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
const MAX_CACHE_ENTRIES = 50
|
|
29
|
-
const _cache = new Map<string, { result: Result; at: number }>()
|
|
30
|
-
const CACHE_TTL = 5 * 60 * 1000
|
|
31
|
-
|
|
32
|
-
function cacheSet(key: string, value: { result: Result; at: number }) {
|
|
33
|
-
if (_cache.size >= MAX_CACHE_ENTRIES) {
|
|
34
|
-
const oldest = _cache.keys().next().value
|
|
35
|
-
if (oldest !== undefined) _cache.delete(oldest)
|
|
36
|
-
}
|
|
37
|
-
_cache.set(key, value)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function cacheKey(baseUrl: string, cartId?: string) {
|
|
41
|
-
return `ppm::${baseUrl}::${cartId ?? ""}`
|
|
42
|
-
}
|
|
43
|
-
|
|
44
43
|
const DEFAULT_RESULT: Result = {
|
|
45
44
|
paypalEnabled: true,
|
|
46
45
|
paypalTitle: "PayPal",
|
|
@@ -50,23 +49,46 @@ const DEFAULT_RESULT: Result = {
|
|
|
50
49
|
error: null,
|
|
51
50
|
}
|
|
52
51
|
|
|
52
|
+
function toResult(cfg: PayPalConfig): Result {
|
|
53
|
+
return {
|
|
54
|
+
paypalEnabled: cfg.paypal_enabled !== false,
|
|
55
|
+
paypalTitle:
|
|
56
|
+
typeof cfg.paypal_title === "string" && cfg.paypal_title
|
|
57
|
+
? cfg.paypal_title
|
|
58
|
+
: "PayPal",
|
|
59
|
+
cardEnabled: cfg.card_enabled !== false,
|
|
60
|
+
cardTitle:
|
|
61
|
+
typeof cfg.card_title === "string" && cfg.card_title
|
|
62
|
+
? cfg.card_title
|
|
63
|
+
: "Credit or Debit Card",
|
|
64
|
+
loading: false,
|
|
65
|
+
error: null,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
export function usePayPalPaymentMethods({
|
|
54
70
|
baseUrl,
|
|
55
71
|
publishableApiKey,
|
|
56
72
|
cartId,
|
|
73
|
+
currencyCode,
|
|
74
|
+
credentials,
|
|
57
75
|
enabled = true,
|
|
58
76
|
}: Args): Result {
|
|
59
77
|
const api = useMemo(
|
|
60
|
-
() => createPayPalStoreApi({ baseUrl, publishableApiKey }),
|
|
61
|
-
[baseUrl, publishableApiKey]
|
|
78
|
+
() => createPayPalStoreApi({ baseUrl, publishableApiKey, credentials }),
|
|
79
|
+
[baseUrl, publishableApiKey, credentials]
|
|
62
80
|
)
|
|
63
81
|
|
|
64
|
-
const key =
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
const key = configCacheKey({
|
|
83
|
+
baseUrl,
|
|
84
|
+
publishableApiKey,
|
|
85
|
+
cartId,
|
|
86
|
+
currencyCode,
|
|
87
|
+
})
|
|
88
|
+
const seed = getCachedPayPalConfig(key)
|
|
67
89
|
|
|
68
90
|
const [result, setResult] = useState<Result>(
|
|
69
|
-
seed
|
|
91
|
+
seed ? toResult(seed) : { ...DEFAULT_RESULT, loading: enabled }
|
|
70
92
|
)
|
|
71
93
|
const fetchIdRef = useRef(0)
|
|
72
94
|
|
|
@@ -76,54 +98,38 @@ export function usePayPalPaymentMethods({
|
|
|
76
98
|
return
|
|
77
99
|
}
|
|
78
100
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
setResult(cached.result)
|
|
101
|
+
const cached = getCachedPayPalConfig(key)
|
|
102
|
+
if (cached) {
|
|
103
|
+
setResult(toResult(cached))
|
|
83
104
|
return
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
const id = ++fetchIdRef.current
|
|
87
108
|
let mounted = true
|
|
88
|
-
const controller = new AbortController()
|
|
89
109
|
|
|
90
110
|
setResult((prev) => ({ ...prev, loading: true }))
|
|
91
111
|
;(async () => {
|
|
92
112
|
try {
|
|
93
|
-
const cfg = await
|
|
113
|
+
const cfg = await fetchPayPalConfigShared(key, () =>
|
|
114
|
+
api.getConfig(cartId)
|
|
115
|
+
)
|
|
94
116
|
if (!mounted || id !== fetchIdRef.current) return
|
|
95
|
-
|
|
96
|
-
const next: Result = {
|
|
97
|
-
paypalEnabled: cfg.paypal_enabled !== false,
|
|
98
|
-
paypalTitle:
|
|
99
|
-
typeof cfg.paypal_title === "string" && cfg.paypal_title
|
|
100
|
-
? cfg.paypal_title
|
|
101
|
-
: "PayPal",
|
|
102
|
-
cardEnabled: cfg.card_enabled !== false,
|
|
103
|
-
cardTitle:
|
|
104
|
-
typeof cfg.card_title === "string" && cfg.card_title
|
|
105
|
-
? cfg.card_title
|
|
106
|
-
: "Credit or Debit Card",
|
|
107
|
-
loading: false,
|
|
108
|
-
error: null,
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
cacheSet(k, { result: next, at: Date.now() })
|
|
112
|
-
setResult(next)
|
|
117
|
+
setResult(toResult(cfg))
|
|
113
118
|
} catch (e: unknown) {
|
|
114
|
-
const msg = e instanceof Error ? e.message : ""
|
|
115
|
-
if (e instanceof Error && e.name === "AbortError") return
|
|
116
119
|
if (!mounted || id !== fetchIdRef.current) return
|
|
120
|
+
const msg = e instanceof Error ? e.message : ""
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
// 403 means the backend explicitly refused (plugin disabled for this
|
|
123
|
+
// sales channel / key). Branch on the status code, not message text —
|
|
124
|
+
// the backend's JSON message rarely contains the literal "403".
|
|
125
|
+
if (e instanceof HttpError && e.status === 403) {
|
|
126
|
+
setResult({
|
|
120
127
|
...DEFAULT_RESULT,
|
|
121
128
|
paypalEnabled: false,
|
|
122
129
|
cardEnabled: false,
|
|
123
130
|
loading: false,
|
|
124
131
|
error: msg || "PayPal is disabled",
|
|
125
|
-
}
|
|
126
|
-
setResult(disabled)
|
|
132
|
+
})
|
|
127
133
|
return
|
|
128
134
|
}
|
|
129
135
|
|
|
@@ -137,9 +143,8 @@ export function usePayPalPaymentMethods({
|
|
|
137
143
|
|
|
138
144
|
return () => {
|
|
139
145
|
mounted = false
|
|
140
|
-
controller.abort()
|
|
141
146
|
}
|
|
142
|
-
}, [api,
|
|
147
|
+
}, [api, key, cartId, enabled])
|
|
143
148
|
|
|
144
149
|
return result
|
|
145
150
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export * from "./client/types"
|
|
2
2
|
export * from "./client/paypal"
|
|
3
|
+
export { createHttpClient, HttpError } from "./client/http"
|
|
4
|
+
export type { HttpOptions, RequestOptions } from "./client/http"
|
|
5
|
+
export { invalidatePayPalConfigCache } from "./client/config-cache"
|
|
6
|
+
export * from "./constants"
|
|
3
7
|
export * from "./hooks/usePayPalConfig"
|
|
4
8
|
export * from "./components/PayPalProvider"
|
|
5
9
|
export * from "./components/PayPalCurrencyNotice"
|
package/src/order.ts
CHANGED
|
@@ -27,14 +27,18 @@
|
|
|
27
27
|
import {
|
|
28
28
|
PAYPAL_WALLET_PROVIDER_ID,
|
|
29
29
|
PAYPAL_CARD_PROVIDER_ID,
|
|
30
|
+
PAYPAL_PROVIDER_IDS,
|
|
30
31
|
isPayPalProviderId,
|
|
31
32
|
} from "./constants"
|
|
33
|
+
import type { PayPalProviderId } from "./constants"
|
|
32
34
|
|
|
33
35
|
export {
|
|
34
36
|
PAYPAL_WALLET_PROVIDER_ID,
|
|
35
37
|
PAYPAL_CARD_PROVIDER_ID,
|
|
38
|
+
PAYPAL_PROVIDER_IDS,
|
|
36
39
|
isPayPalProviderId,
|
|
37
40
|
}
|
|
41
|
+
export type { PayPalProviderId }
|
|
38
42
|
|
|
39
43
|
export type PayPalPaymentInfo = { title: string }
|
|
40
44
|
|
|
@@ -102,13 +106,16 @@ export async function fetchPayPalConfig(opts: {
|
|
|
102
106
|
return {}
|
|
103
107
|
}
|
|
104
108
|
try {
|
|
109
|
+
// Always arm the timeout — a caller-supplied signal composes with it
|
|
110
|
+
// rather than replacing it (matching the client-side HTTP client), so a
|
|
111
|
+
// signal that never fires can't leave the request hanging forever.
|
|
105
112
|
const timeoutMs = 30_000
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
const controller = new AbortController()
|
|
114
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
|
|
115
|
+
const onCallerAbort = () => controller.abort()
|
|
116
|
+
if (signal) {
|
|
117
|
+
if (signal.aborted) controller.abort()
|
|
118
|
+
else signal.addEventListener("abort", onCallerAbort)
|
|
112
119
|
}
|
|
113
120
|
let res: Response
|
|
114
121
|
try {
|
|
@@ -119,10 +126,11 @@ export async function fetchPayPalConfig(opts: {
|
|
|
119
126
|
? { "x-publishable-api-key": publishableApiKey }
|
|
120
127
|
: {}),
|
|
121
128
|
},
|
|
122
|
-
signal:
|
|
129
|
+
signal: controller.signal,
|
|
123
130
|
})
|
|
124
131
|
} finally {
|
|
125
|
-
|
|
132
|
+
clearTimeout(timeoutId)
|
|
133
|
+
if (signal) signal.removeEventListener("abort", onCallerAbort)
|
|
126
134
|
}
|
|
127
135
|
if (!res.ok) {
|
|
128
136
|
return {}
|
|
@@ -2,23 +2,35 @@ const OVERLAY_ID = "__pp_processing_overlay"
|
|
|
2
2
|
|
|
3
3
|
let cleanupNavListener: (() => void) | null = null
|
|
4
4
|
|
|
5
|
+
/** True while the shared DOM overlay is mounted. */
|
|
6
|
+
export function isProcessingOverlayVisible(): boolean {
|
|
7
|
+
if (typeof document === "undefined") return false
|
|
8
|
+
return !!document.getElementById(OVERLAY_ID)
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
export function showProcessingOverlay() {
|
|
6
12
|
if (typeof document === "undefined") return
|
|
7
13
|
if (document.getElementById(OVERLAY_ID)) return
|
|
8
14
|
|
|
9
15
|
const overlay = document.createElement("div")
|
|
10
16
|
overlay.id = OVERLAY_ID
|
|
17
|
+
// role="status" announces the visible text politely; no aria-label so the
|
|
18
|
+
// text itself (which explains what's happening) is what gets read.
|
|
11
19
|
overlay.setAttribute("role", "status")
|
|
12
|
-
overlay.setAttribute("aria-label", "Processing payment")
|
|
13
20
|
|
|
14
21
|
overlay.innerHTML = `
|
|
15
22
|
<style>
|
|
16
|
-
@keyframes
|
|
17
|
-
@keyframes
|
|
23
|
+
@keyframes _pp_ov_spin { to { transform: rotate(360deg) } }
|
|
24
|
+
@keyframes _pp_ov_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
|
|
25
|
+
._pp-ov-spin { animation: _pp_ov_spin .8s linear infinite }
|
|
26
|
+
._pp-ov-progress { animation: _pp_ov_progress 1.5s ease-in-out infinite }
|
|
27
|
+
@media (prefers-reduced-motion: reduce) {
|
|
28
|
+
._pp-ov-spin, ._pp-ov-progress { animation: none }
|
|
29
|
+
}
|
|
18
30
|
</style>
|
|
19
|
-
<div style="position:relative;width:56px;height:56px">
|
|
31
|
+
<div style="position:relative;width:56px;height:56px" aria-hidden="true">
|
|
20
32
|
<div style="position:absolute;inset:0;border-radius:50%;border:3px solid #e5e7eb"></div>
|
|
21
|
-
<div style="position:absolute;inset:0;border-radius:50%;border:3px solid transparent;border-top-color:#0070ba
|
|
33
|
+
<div class="_pp-ov-spin" style="position:absolute;inset:0;border-radius:50%;border:3px solid transparent;border-top-color:#0070ba"></div>
|
|
22
34
|
<svg viewBox="0 0 24 24" fill="none" stroke="#0070ba" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
|
|
23
35
|
style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:24px;height:24px">
|
|
24
36
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
@@ -30,8 +42,8 @@ export function showProcessingOverlay() {
|
|
|
30
42
|
This may take a few moments. Please do not close<br>or refresh this page.
|
|
31
43
|
</div>
|
|
32
44
|
</div>
|
|
33
|
-
<div style="width:200px;height:3px;background:#e5e7eb;border-radius:3px;overflow:hidden;margin-top:4px">
|
|
34
|
-
<div style="width:40%;height:100%;background:linear-gradient(90deg,#0070ba,#003087);border-radius:3px
|
|
45
|
+
<div style="width:200px;height:3px;background:#e5e7eb;border-radius:3px;overflow:hidden;margin-top:4px" aria-hidden="true">
|
|
46
|
+
<div class="_pp-ov-progress" style="width:40%;height:100%;background:linear-gradient(90deg,#0070ba,#003087);border-radius:3px"></div>
|
|
35
47
|
</div>
|
|
36
48
|
`
|
|
37
49
|
|
|
@@ -51,23 +63,16 @@ export function showProcessingOverlay() {
|
|
|
51
63
|
document.body.appendChild(overlay)
|
|
52
64
|
|
|
53
65
|
// Auto-remove on client-side navigation (Next.js App Router uses
|
|
54
|
-
// pushState/replaceState for soft navigations, not full reloads).
|
|
66
|
+
// pushState/replaceState for soft navigations, not full reloads). The URL is
|
|
67
|
+
// polled instead of monkeypatching history.pushState — patching fights with
|
|
68
|
+
// the Next router and any other library that wraps history, and a stale
|
|
69
|
+
// restore on cleanup could undo THEIR patch.
|
|
55
70
|
const startUrl = location.href
|
|
56
71
|
const onNav = () => {
|
|
57
72
|
if (location.href !== startUrl) hideProcessingOverlay()
|
|
58
73
|
}
|
|
59
74
|
window.addEventListener("popstate", onNav)
|
|
60
|
-
|
|
61
|
-
const origPush = history.pushState.bind(history)
|
|
62
|
-
const origReplace = history.replaceState.bind(history)
|
|
63
|
-
history.pushState = function (...args) {
|
|
64
|
-
origPush(...args)
|
|
65
|
-
onNav()
|
|
66
|
-
}
|
|
67
|
-
history.replaceState = function (...args) {
|
|
68
|
-
origReplace(...args)
|
|
69
|
-
onNav()
|
|
70
|
-
}
|
|
75
|
+
const urlWatcher = window.setInterval(onNav, 250)
|
|
71
76
|
|
|
72
77
|
// bfcache restore: the overlay intentionally survives the unload of a
|
|
73
78
|
// full-page navigation (that's its purpose), but when the buyer presses
|
|
@@ -83,8 +88,7 @@ export function showProcessingOverlay() {
|
|
|
83
88
|
cleanupNavListener = () => {
|
|
84
89
|
window.removeEventListener("popstate", onNav)
|
|
85
90
|
window.removeEventListener("pageshow", onPageShow)
|
|
86
|
-
|
|
87
|
-
history.replaceState = origReplace
|
|
91
|
+
window.clearInterval(urlWatcher)
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
|