@easypayment/medusa-paypal-ui 1.1.1 → 1.2.2
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 +97 -61
- package/README.md +781 -759
- package/dist/index.cjs +143 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -2
- package/dist/index.d.ts +64 -2
- package/dist/index.mjs +133 -136
- package/dist/index.mjs.map +1 -1
- package/dist/order.cjs +14 -3
- package/dist/order.cjs.map +1 -1
- package/dist/order.d.cts +13 -4
- package/dist/order.d.ts +13 -4
- package/dist/order.mjs +13 -4
- package/dist/order.mjs.map +1 -1
- package/package.json +76 -75
- package/src/adapters/MedusaNextPayPalAdapter.tsx +34 -5
- package/src/client/http.ts +9 -1
- package/src/client/paypal.ts +27 -1
- package/src/components/PayPalAdvancedCard.tsx +24 -0
- package/src/components/PayPalPaymentSection.tsx +95 -202
- package/src/components/PayPalSmartButtons.tsx +24 -0
- package/src/constants.ts +22 -0
- package/src/hooks/usePayPalPaymentMethods.ts +15 -1
- package/src/index.ts +5 -0
- package/src/order.ts +7 -10
- package/src/utils/captured-state.ts +44 -0
|
@@ -1,202 +1,95 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import React
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function ErrorCard({ message }: { message: string }) {
|
|
100
|
-
return (
|
|
101
|
-
<div
|
|
102
|
-
role="alert"
|
|
103
|
-
style={{
|
|
104
|
-
padding: "12px 16px",
|
|
105
|
-
background: "#fef2f2",
|
|
106
|
-
border: "1px solid #fecaca",
|
|
107
|
-
borderRadius: 10,
|
|
108
|
-
fontSize: 13,
|
|
109
|
-
color: "#b91c1c",
|
|
110
|
-
}}
|
|
111
|
-
>
|
|
112
|
-
{message}
|
|
113
|
-
</div>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export type PayPalPaymentSectionProps = {
|
|
118
|
-
cartId: string
|
|
119
|
-
selectedProviderId: string | null | undefined
|
|
120
|
-
baseUrl: string
|
|
121
|
-
publishableApiKey?: string
|
|
122
|
-
sessionLoading?: boolean
|
|
123
|
-
onSuccess?: (cartId: string) => void
|
|
124
|
-
onError?: (message: string) => void
|
|
125
|
-
onPaid?: (result: unknown) => void
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function PayPalPaymentSection({
|
|
129
|
-
cartId,
|
|
130
|
-
selectedProviderId,
|
|
131
|
-
baseUrl,
|
|
132
|
-
publishableApiKey,
|
|
133
|
-
sessionLoading = false,
|
|
134
|
-
onSuccess,
|
|
135
|
-
onError,
|
|
136
|
-
onPaid,
|
|
137
|
-
}: PayPalPaymentSectionProps) {
|
|
138
|
-
const shouldRender = isPayPalProviderId(selectedProviderId)
|
|
139
|
-
|
|
140
|
-
const { config, loading, error } = usePayPalConfig({
|
|
141
|
-
baseUrl,
|
|
142
|
-
publishableApiKey,
|
|
143
|
-
cartId,
|
|
144
|
-
enabled: shouldRender,
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
const handlePaid = useCallback(
|
|
148
|
-
async (captureResult: unknown) => {
|
|
149
|
-
onPaid?.(captureResult)
|
|
150
|
-
await onSuccess?.(cartId)
|
|
151
|
-
},
|
|
152
|
-
[cartId, onPaid, onSuccess]
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
if (!shouldRender) return null
|
|
156
|
-
if (sessionLoading) return <SessionInitCard />
|
|
157
|
-
if (loading) return <ConfigLoadingCard />
|
|
158
|
-
if (error) return <ErrorCard message={error} />
|
|
159
|
-
if (!config) return null
|
|
160
|
-
|
|
161
|
-
if (config.paypal_enabled === false && selectedProviderId === PAYPAL_WALLET_PROVIDER_ID) {
|
|
162
|
-
return null
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const isCardProvider = selectedProviderId === PAYPAL_CARD_PROVIDER_ID
|
|
166
|
-
if (isCardProvider && config.card_enabled === false) return null
|
|
167
|
-
|
|
168
|
-
const disableFunding = Array.isArray(config.disable_buttons)
|
|
169
|
-
? config.disable_buttons.join(",")
|
|
170
|
-
: undefined
|
|
171
|
-
|
|
172
|
-
return (
|
|
173
|
-
<div key={selectedProviderId} style={{ display: "grid", gap: 12 }}>
|
|
174
|
-
<PayPalCurrencyNotice config={config} />
|
|
175
|
-
<PayPalProvider
|
|
176
|
-
config={config}
|
|
177
|
-
intent={config.intent === "authorize" ? "authorize" : "capture"}
|
|
178
|
-
disableFunding={disableFunding}
|
|
179
|
-
>
|
|
180
|
-
{isCardProvider ? (
|
|
181
|
-
<PayPalAdvancedCard
|
|
182
|
-
baseUrl={baseUrl}
|
|
183
|
-
publishableApiKey={publishableApiKey}
|
|
184
|
-
cartId={cartId}
|
|
185
|
-
config={config}
|
|
186
|
-
onPaid={handlePaid}
|
|
187
|
-
onError={onError}
|
|
188
|
-
/>
|
|
189
|
-
) : (
|
|
190
|
-
<PayPalSmartButtons
|
|
191
|
-
baseUrl={baseUrl}
|
|
192
|
-
publishableApiKey={publishableApiKey}
|
|
193
|
-
cartId={cartId}
|
|
194
|
-
config={config}
|
|
195
|
-
onPaid={handlePaid}
|
|
196
|
-
onError={onError}
|
|
197
|
-
/>
|
|
198
|
-
)}
|
|
199
|
-
</PayPalProvider>
|
|
200
|
-
</div>
|
|
201
|
-
)
|
|
202
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
import { MedusaNextPayPalAdapter } from "../adapters/MedusaNextPayPalAdapter"
|
|
5
|
+
import {
|
|
6
|
+
PAYPAL_WALLET_PROVIDER_ID,
|
|
7
|
+
PAYPAL_CARD_PROVIDER_ID,
|
|
8
|
+
isPayPalProviderId,
|
|
9
|
+
} from "../constants"
|
|
10
|
+
|
|
11
|
+
// Re-exported for backward compatibility — these have always been part of this
|
|
12
|
+
// module's public API. The canonical definitions live in ../constants.
|
|
13
|
+
export { PAYPAL_WALLET_PROVIDER_ID, PAYPAL_CARD_PROVIDER_ID, isPayPalProviderId }
|
|
14
|
+
|
|
15
|
+
const SPIN_STYLE = `@keyframes _pp_section_spin { to { transform: rotate(360deg) } }`
|
|
16
|
+
|
|
17
|
+
function SessionInitCard() {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
role="status"
|
|
21
|
+
aria-label="Setting up payment"
|
|
22
|
+
style={{
|
|
23
|
+
display: "flex",
|
|
24
|
+
alignItems: "center",
|
|
25
|
+
gap: 12,
|
|
26
|
+
padding: "14px 16px",
|
|
27
|
+
marginTop: 8,
|
|
28
|
+
background: "#f9fafb",
|
|
29
|
+
border: "1px solid #e5e7eb",
|
|
30
|
+
borderRadius: 10,
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<style>{SPIN_STYLE}</style>
|
|
34
|
+
<div
|
|
35
|
+
style={{
|
|
36
|
+
width: 20,
|
|
37
|
+
height: 20,
|
|
38
|
+
borderRadius: "50%",
|
|
39
|
+
border: "2.5px solid #e5e7eb",
|
|
40
|
+
borderTopColor: "#0070ba",
|
|
41
|
+
animation: "_pp_section_spin .7s linear infinite",
|
|
42
|
+
flexShrink: 0,
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
<div style={{ fontSize: 13, fontWeight: 500, color: "#111827" }}>
|
|
46
|
+
Setting up payment…
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type PayPalPaymentSectionProps = {
|
|
53
|
+
cartId: string
|
|
54
|
+
selectedProviderId: string | null | undefined
|
|
55
|
+
baseUrl: string
|
|
56
|
+
publishableApiKey?: string
|
|
57
|
+
sessionLoading?: boolean
|
|
58
|
+
onSuccess?: (cartId: string) => void
|
|
59
|
+
onError?: (message: string) => void
|
|
60
|
+
onPaid?: (result: unknown) => void
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Drop-in PayPal payment section for a Medusa checkout.
|
|
65
|
+
*
|
|
66
|
+
* A thin wrapper over {@link MedusaNextPayPalAdapter} (they previously carried
|
|
67
|
+
* two ~90%-identical render paths that had to be edited in lockstep): this
|
|
68
|
+
* component pins the default provider ids and adds the `sessionLoading` gate
|
|
69
|
+
* shown while the storefront is still creating the payment session.
|
|
70
|
+
*/
|
|
71
|
+
export function PayPalPaymentSection({
|
|
72
|
+
cartId,
|
|
73
|
+
selectedProviderId,
|
|
74
|
+
baseUrl,
|
|
75
|
+
publishableApiKey,
|
|
76
|
+
sessionLoading = false,
|
|
77
|
+
onSuccess,
|
|
78
|
+
onError,
|
|
79
|
+
onPaid,
|
|
80
|
+
}: PayPalPaymentSectionProps) {
|
|
81
|
+
if (!isPayPalProviderId(selectedProviderId)) return null
|
|
82
|
+
if (sessionLoading) return <SessionInitCard />
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<MedusaNextPayPalAdapter
|
|
86
|
+
cartId={cartId}
|
|
87
|
+
selectedProviderId={selectedProviderId}
|
|
88
|
+
baseUrl={baseUrl}
|
|
89
|
+
publishableApiKey={publishableApiKey}
|
|
90
|
+
onSuccess={onSuccess}
|
|
91
|
+
onError={onError}
|
|
92
|
+
onPaid={onPaid}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -3,6 +3,11 @@ import React, { useCallback, useEffect, useMemo, useRef, 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 {
|
|
7
|
+
clearCartCaptured,
|
|
8
|
+
markCartCaptured,
|
|
9
|
+
wasCartCaptured,
|
|
10
|
+
} from "../utils/captured-state"
|
|
6
11
|
import { isNextRouterError } from "../utils/next-errors"
|
|
7
12
|
import { showProcessingOverlay, hideProcessingOverlay } from "../utils/processing-overlay"
|
|
8
13
|
|
|
@@ -58,6 +63,19 @@ export function PayPalSmartButtons(props: {
|
|
|
58
63
|
return () => window.removeEventListener("pageshow", onPageShow)
|
|
59
64
|
}, [])
|
|
60
65
|
|
|
66
|
+
// A reload/crash between capture and finalization loses the in-memory
|
|
67
|
+
// capturedRef — restore it from the persisted per-cart flag so the buyer
|
|
68
|
+
// gets the finalize-only Retry button instead of payment buttons that would
|
|
69
|
+
// fail confusingly against an already-captured PayPal order.
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (capturedRef.current || !wasCartCaptured(cartId)) return
|
|
72
|
+
capturedRef.current = {}
|
|
73
|
+
setCompletionPending(true)
|
|
74
|
+
const msg =
|
|
75
|
+
"Your payment was already received for this cart. Please use the button below to finish placing your order — do not pay again."
|
|
76
|
+
setError(msg)
|
|
77
|
+
}, [cartId])
|
|
78
|
+
|
|
61
79
|
// The PayPal SDK never calls onInit when the buttons are ineligible (wrong
|
|
62
80
|
// country/currency/funding) — it silently renders nothing. Without a
|
|
63
81
|
// fallback the buyer stares at "Loading PayPal…" forever.
|
|
@@ -77,6 +95,9 @@ export function PayPalSmartButtons(props: {
|
|
|
77
95
|
setError(null)
|
|
78
96
|
try {
|
|
79
97
|
const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey)
|
|
98
|
+
// The order is finalized — the persisted "captured" flag has served its
|
|
99
|
+
// purpose. Clear it before the consumer callback (which may redirect).
|
|
100
|
+
clearCartCaptured(cartId)
|
|
80
101
|
await onPaid?.({ ...captured, ...completeResult })
|
|
81
102
|
setCompletionPending(false)
|
|
82
103
|
} catch (e: unknown) {
|
|
@@ -343,6 +364,9 @@ export function PayPalSmartButtons(props: {
|
|
|
343
364
|
if (!orderId) throw new Error("PayPal order ID is missing from approval response")
|
|
344
365
|
const result = await api.captureOrder(cartId, orderId)
|
|
345
366
|
capturedRef.current = (result || {}) as Record<string, unknown>
|
|
367
|
+
// Persist across reloads: from here on, only finalization may
|
|
368
|
+
// ever be retried for this cart — never a fresh payment.
|
|
369
|
+
markCartCaptured(cartId)
|
|
346
370
|
} catch (e: unknown) {
|
|
347
371
|
if (isNextRouterError(e)) return
|
|
348
372
|
hideProcessingOverlay()
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider ids registered by `@easypayment/medusa-payment-paypal`.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth — `PayPalPaymentSection`, `MedusaNextPayPalAdapter`,
|
|
5
|
+
* and the server-safe `order` entrypoint all re-export from here so the ids
|
|
6
|
+
* can never drift between entrypoints.
|
|
7
|
+
*/
|
|
8
|
+
export const PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal" as const
|
|
9
|
+
export const PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card" as const
|
|
10
|
+
|
|
11
|
+
export const PAYPAL_PROVIDER_IDS = [
|
|
12
|
+
PAYPAL_WALLET_PROVIDER_ID,
|
|
13
|
+
PAYPAL_CARD_PROVIDER_ID,
|
|
14
|
+
] as const
|
|
15
|
+
|
|
16
|
+
/** True when the provider id belongs to this PayPal plugin. */
|
|
17
|
+
export function isPayPalProviderId(providerId?: string | null): boolean {
|
|
18
|
+
if (!providerId) return false
|
|
19
|
+
return PAYPAL_PROVIDER_IDS.includes(
|
|
20
|
+
providerId as (typeof PAYPAL_PROVIDER_IDS)[number]
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -17,6 +17,13 @@ type Result = {
|
|
|
17
17
|
cardEnabled: boolean
|
|
18
18
|
cardTitle: string
|
|
19
19
|
loading: boolean
|
|
20
|
+
/**
|
|
21
|
+
* Non-null when the config fetch failed and the returned flags are the
|
|
22
|
+
* optimistic defaults rather than the merchant's real settings. Storefronts
|
|
23
|
+
* can use this to hide or annotate the PayPal options instead of advertising
|
|
24
|
+
* methods that will fail once selected.
|
|
25
|
+
*/
|
|
26
|
+
error: string | null
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
const MAX_CACHE_ENTRIES = 50
|
|
@@ -41,6 +48,7 @@ const DEFAULT_RESULT: Result = {
|
|
|
41
48
|
cardEnabled: true,
|
|
42
49
|
cardTitle: "Credit or Debit Card",
|
|
43
50
|
loading: false,
|
|
51
|
+
error: null,
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
export function usePayPalPaymentMethods({
|
|
@@ -97,6 +105,7 @@ export function usePayPalPaymentMethods({
|
|
|
97
105
|
? cfg.card_title
|
|
98
106
|
: "Credit or Debit Card",
|
|
99
107
|
loading: false,
|
|
108
|
+
error: null,
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
cacheSet(k, { result: next, at: Date.now() })
|
|
@@ -112,12 +121,17 @@ export function usePayPalPaymentMethods({
|
|
|
112
121
|
paypalEnabled: false,
|
|
113
122
|
cardEnabled: false,
|
|
114
123
|
loading: false,
|
|
124
|
+
error: msg || "PayPal is disabled",
|
|
115
125
|
}
|
|
116
126
|
setResult(disabled)
|
|
117
127
|
return
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
setResult({
|
|
130
|
+
setResult({
|
|
131
|
+
...DEFAULT_RESULT,
|
|
132
|
+
loading: false,
|
|
133
|
+
error: msg || "Failed to load PayPal payment methods",
|
|
134
|
+
})
|
|
121
135
|
}
|
|
122
136
|
})()
|
|
123
137
|
|
package/src/index.ts
CHANGED
|
@@ -9,3 +9,8 @@ export * from "./adapters/MedusaNextPayPalAdapter"
|
|
|
9
9
|
export * from "./components/PayPalPaymentSection"
|
|
10
10
|
export * from "./hooks/usePayPalPaymentMethods"
|
|
11
11
|
export { showProcessingOverlay, hideProcessingOverlay } from "./utils/processing-overlay"
|
|
12
|
+
export {
|
|
13
|
+
markCartCaptured,
|
|
14
|
+
wasCartCaptured,
|
|
15
|
+
clearCartCaptured,
|
|
16
|
+
} from "./utils/captured-state"
|
package/src/order.ts
CHANGED
|
@@ -24,8 +24,13 @@
|
|
|
24
24
|
* </Text>
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
import {
|
|
28
|
+
PAYPAL_WALLET_PROVIDER_ID,
|
|
29
|
+
PAYPAL_CARD_PROVIDER_ID,
|
|
30
|
+
isPayPalProviderId,
|
|
31
|
+
} from "./constants"
|
|
32
|
+
|
|
33
|
+
export { PAYPAL_WALLET_PROVIDER_ID, PAYPAL_CARD_PROVIDER_ID, isPayPalProviderId }
|
|
29
34
|
|
|
30
35
|
export type PayPalPaymentInfo = { title: string }
|
|
31
36
|
|
|
@@ -41,14 +46,6 @@ export type PayPalConfigTitles = {
|
|
|
41
46
|
card_title?: string | null
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
/** True when the provider id belongs to this PayPal plugin. */
|
|
45
|
-
export function isPayPalProviderId(providerId?: string | null): boolean {
|
|
46
|
-
return (
|
|
47
|
-
providerId === PAYPAL_WALLET_PROVIDER_ID ||
|
|
48
|
-
providerId === PAYPAL_CARD_PROVIDER_ID
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
49
|
/**
|
|
53
50
|
* Null-safe payment label resolver for order / confirmation pages.
|
|
54
51
|
*
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-cart "money was captured" flag, persisted in sessionStorage.
|
|
3
|
+
*
|
|
4
|
+
* The in-memory `capturedRef` protects a buyer within one page lifetime, but a
|
|
5
|
+
* reload/crash between the capture and the order finalization loses it — the
|
|
6
|
+
* buyer would then see fresh payment buttons for a cart whose money is already
|
|
7
|
+
* taken. Persisting the flag lets the components restore the finalize-only
|
|
8
|
+
* Retry path after a reload instead of offering a second payment.
|
|
9
|
+
*
|
|
10
|
+
* sessionStorage is deliberately chosen over localStorage: it is scoped to the
|
|
11
|
+
* tab/session, so an abandoned flag can't leak into a future visit after the
|
|
12
|
+
* cart id is reused, and it needs no expiry logic. Every access is guarded —
|
|
13
|
+
* private browsing modes and storage-blocking settings throw on access, and a
|
|
14
|
+
* storage failure must never break the payment flow.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const KEY_PREFIX = "__pp_captured::"
|
|
18
|
+
|
|
19
|
+
export function markCartCaptured(cartId: string): void {
|
|
20
|
+
if (!cartId) return
|
|
21
|
+
try {
|
|
22
|
+
window.sessionStorage.setItem(`${KEY_PREFIX}${cartId}`, String(Date.now()))
|
|
23
|
+
} catch {
|
|
24
|
+
// storage unavailable — in-memory state still covers the common path
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function wasCartCaptured(cartId: string): boolean {
|
|
29
|
+
if (!cartId) return false
|
|
30
|
+
try {
|
|
31
|
+
return window.sessionStorage.getItem(`${KEY_PREFIX}${cartId}`) !== null
|
|
32
|
+
} catch {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clearCartCaptured(cartId: string): void {
|
|
38
|
+
if (!cartId) return
|
|
39
|
+
try {
|
|
40
|
+
window.sessionStorage.removeItem(`${KEY_PREFIX}${cartId}`)
|
|
41
|
+
} catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
}
|