@flopay/react 0.1.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 +378 -0
- package/dist/index.cjs +1828 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +402 -0
- package/dist/index.d.ts +402 -0
- package/dist/index.mjs +1783 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1828 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AddressElement: () => AddressElement,
|
|
34
|
+
CardCvcElement: () => CardCvcElement,
|
|
35
|
+
CardElement: () => CardElement,
|
|
36
|
+
CardExpiryElement: () => CardExpiryElement,
|
|
37
|
+
CardNumberElement: () => CardNumberElement,
|
|
38
|
+
CheckoutForm: () => CheckoutForm,
|
|
39
|
+
FloPayCheckout: () => FloPayCheckout,
|
|
40
|
+
FloPayProvider: () => FloPayProvider,
|
|
41
|
+
PayPalButton: () => PayPalButton,
|
|
42
|
+
PaymentElement: () => PaymentElement,
|
|
43
|
+
SplitCardForm: () => SplitCardForm,
|
|
44
|
+
useCheckout: () => useCheckout,
|
|
45
|
+
useElements: () => useElements,
|
|
46
|
+
useFloPay: () => useFloPay
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(index_exports);
|
|
49
|
+
|
|
50
|
+
// src/provider.tsx
|
|
51
|
+
var import_react2 = require("react");
|
|
52
|
+
var import_shared = require("@flopay/shared");
|
|
53
|
+
|
|
54
|
+
// src/context.ts
|
|
55
|
+
var import_react = require("react");
|
|
56
|
+
var FloPayContext = (0, import_react.createContext)({
|
|
57
|
+
flopay: null,
|
|
58
|
+
elements: null,
|
|
59
|
+
billingApiUrl: ""
|
|
60
|
+
});
|
|
61
|
+
var CheckoutContext = (0, import_react.createContext)({
|
|
62
|
+
session: null,
|
|
63
|
+
loading: false,
|
|
64
|
+
error: null
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// src/provider.tsx
|
|
68
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
69
|
+
function FloPayProvider({
|
|
70
|
+
flopay: floPayProp,
|
|
71
|
+
options,
|
|
72
|
+
children
|
|
73
|
+
}) {
|
|
74
|
+
const [flopay, setFloPay] = (0, import_react2.useState)(
|
|
75
|
+
floPayProp instanceof Promise ? null : floPayProp
|
|
76
|
+
);
|
|
77
|
+
const [elements, setElements] = (0, import_react2.useState)(null);
|
|
78
|
+
(0, import_react2.useEffect)(() => {
|
|
79
|
+
let cancelled = false;
|
|
80
|
+
if (floPayProp instanceof Promise) {
|
|
81
|
+
floPayProp.then((instance) => {
|
|
82
|
+
if (!cancelled) {
|
|
83
|
+
setFloPay(instance);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
setFloPay(floPayProp);
|
|
88
|
+
}
|
|
89
|
+
return () => {
|
|
90
|
+
cancelled = true;
|
|
91
|
+
};
|
|
92
|
+
}, [floPayProp]);
|
|
93
|
+
(0, import_react2.useEffect)(() => {
|
|
94
|
+
if (!flopay) {
|
|
95
|
+
setElements(null);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const els = flopay.elements({
|
|
99
|
+
appearance: options?.appearance,
|
|
100
|
+
clientSecret: options?.clientSecret,
|
|
101
|
+
amount: options?.amount,
|
|
102
|
+
currency: options?.currency,
|
|
103
|
+
paymentMethodCreation: options?.paymentMethodCreation
|
|
104
|
+
});
|
|
105
|
+
setElements(els);
|
|
106
|
+
return () => {
|
|
107
|
+
els.destroy();
|
|
108
|
+
};
|
|
109
|
+
}, [flopay, options?.appearance, options?.clientSecret, options?.amount, options?.currency]);
|
|
110
|
+
const resolvedBillingApiUrl = (0, import_shared.resolveBillingApiUrl)(options?.billingApiUrl);
|
|
111
|
+
const value = (0, import_react2.useMemo)(
|
|
112
|
+
() => ({ flopay, elements, billingApiUrl: resolvedBillingApiUrl }),
|
|
113
|
+
[flopay, elements, resolvedBillingApiUrl]
|
|
114
|
+
);
|
|
115
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FloPayContext.Provider, { value, children });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/flopay-checkout.tsx
|
|
119
|
+
var import_react6 = __toESM(require("react"), 1);
|
|
120
|
+
var import_js = require("@flopay/js");
|
|
121
|
+
var import_shared4 = require("@flopay/shared");
|
|
122
|
+
|
|
123
|
+
// src/elements.tsx
|
|
124
|
+
var import_react3 = require("react");
|
|
125
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
126
|
+
function createElementComponent(elementType, displayName) {
|
|
127
|
+
function ElementComponent({
|
|
128
|
+
className,
|
|
129
|
+
id,
|
|
130
|
+
style,
|
|
131
|
+
options,
|
|
132
|
+
onChange,
|
|
133
|
+
onReady,
|
|
134
|
+
onFocus,
|
|
135
|
+
onBlur,
|
|
136
|
+
onEscape
|
|
137
|
+
}) {
|
|
138
|
+
const containerRef = (0, import_react3.useRef)(null);
|
|
139
|
+
const elementRef = (0, import_react3.useRef)(null);
|
|
140
|
+
const { elements } = (0, import_react3.useContext)(FloPayContext);
|
|
141
|
+
(0, import_react3.useEffect)(() => {
|
|
142
|
+
if (!elements || !containerRef.current) return;
|
|
143
|
+
let mounted = true;
|
|
144
|
+
(async () => {
|
|
145
|
+
let element = elements.getElement(elementType);
|
|
146
|
+
if (!element) {
|
|
147
|
+
element = await elements.create(elementType, options);
|
|
148
|
+
}
|
|
149
|
+
if (!mounted || !containerRef.current) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
element.mount(containerRef.current);
|
|
153
|
+
elementRef.current = element;
|
|
154
|
+
if (onChange) element.on("change", onChange);
|
|
155
|
+
if (onReady) element.on("ready", onReady);
|
|
156
|
+
if (onFocus) element.on("focus", onFocus);
|
|
157
|
+
if (onBlur) element.on("blur", onBlur);
|
|
158
|
+
if (onEscape) element.on("escape", onEscape);
|
|
159
|
+
})();
|
|
160
|
+
return () => {
|
|
161
|
+
mounted = false;
|
|
162
|
+
if (elementRef.current) {
|
|
163
|
+
try {
|
|
164
|
+
elementRef.current.unmount();
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
elementRef.current = null;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}, [elements]);
|
|
171
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: containerRef, className, id, style });
|
|
172
|
+
}
|
|
173
|
+
ElementComponent.displayName = displayName;
|
|
174
|
+
return ElementComponent;
|
|
175
|
+
}
|
|
176
|
+
var PaymentElement = createElementComponent("payment", "PaymentElement");
|
|
177
|
+
var CardElement = createElementComponent("card", "CardElement");
|
|
178
|
+
var CardNumberElement = createElementComponent("cardNumber", "CardNumberElement");
|
|
179
|
+
var CardExpiryElement = createElementComponent("cardExpiry", "CardExpiryElement");
|
|
180
|
+
var CardCvcElement = createElementComponent("cardCvc", "CardCvcElement");
|
|
181
|
+
var AddressElement = createElementComponent("address", "AddressElement");
|
|
182
|
+
|
|
183
|
+
// src/split-card-form.tsx
|
|
184
|
+
var import_react_stripe_js = require("@stripe/react-stripe-js");
|
|
185
|
+
var import_react5 = require("react");
|
|
186
|
+
|
|
187
|
+
// src/hooks.ts
|
|
188
|
+
var import_react4 = require("react");
|
|
189
|
+
var import_shared2 = require("@flopay/shared");
|
|
190
|
+
function useFloPay() {
|
|
191
|
+
const ctx = (0, import_react4.useContext)(FloPayContext);
|
|
192
|
+
return ctx.flopay;
|
|
193
|
+
}
|
|
194
|
+
function useElements() {
|
|
195
|
+
const ctx = (0, import_react4.useContext)(FloPayContext);
|
|
196
|
+
return ctx.elements;
|
|
197
|
+
}
|
|
198
|
+
function useCheckout() {
|
|
199
|
+
return (0, import_react4.useContext)(CheckoutContext);
|
|
200
|
+
}
|
|
201
|
+
function useBillingApiUrl() {
|
|
202
|
+
const ctx = (0, import_react4.useContext)(FloPayContext);
|
|
203
|
+
return ctx.billingApiUrl || (0, import_shared2.resolveBillingApiUrl)();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/split-card-form.tsx
|
|
207
|
+
var import_shared3 = require("@flopay/shared");
|
|
208
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
209
|
+
var WALLET_RESUME_KEY = "flopay_wallet_resume";
|
|
210
|
+
function ProcessingOverlay({ status }) {
|
|
211
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
|
|
212
|
+
position: "fixed",
|
|
213
|
+
inset: 0,
|
|
214
|
+
background: "rgba(0,0,0,0.35)",
|
|
215
|
+
display: "flex",
|
|
216
|
+
alignItems: "center",
|
|
217
|
+
justifyContent: "center",
|
|
218
|
+
zIndex: 1e3,
|
|
219
|
+
backdropFilter: "blur(2px)"
|
|
220
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
|
|
221
|
+
background: "white",
|
|
222
|
+
borderRadius: 12,
|
|
223
|
+
padding: "2rem 2.5rem",
|
|
224
|
+
textAlign: "center",
|
|
225
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.18)",
|
|
226
|
+
minWidth: 240,
|
|
227
|
+
display: "flex",
|
|
228
|
+
flexDirection: "column",
|
|
229
|
+
alignItems: "center",
|
|
230
|
+
gap: 16
|
|
231
|
+
}, children: [
|
|
232
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { width: 48, height: 48, position: "relative" }, children: [
|
|
233
|
+
status === "processing" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
234
|
+
"svg",
|
|
235
|
+
{
|
|
236
|
+
width: "48",
|
|
237
|
+
height: "48",
|
|
238
|
+
viewBox: "0 0 24 24",
|
|
239
|
+
fill: "none",
|
|
240
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
241
|
+
style: { animation: "flopay-spin 0.8s linear infinite" },
|
|
242
|
+
children: [
|
|
243
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "10", stroke: "#e5e7eb", strokeWidth: "3" }),
|
|
244
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M4 12a8 8 0 018-8v3a5 5 0 00-5 5H4z", fill: "#4A49FF" })
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
),
|
|
248
|
+
status === "success" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { animation: "flopay-pop 0.4s cubic-bezier(0.34,1.56,0.64,1)" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
249
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "11", fill: "#22c55e" }),
|
|
250
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
251
|
+
"path",
|
|
252
|
+
{
|
|
253
|
+
d: "M7 12.5l3 3 7-7",
|
|
254
|
+
stroke: "white",
|
|
255
|
+
strokeWidth: "2.5",
|
|
256
|
+
strokeLinecap: "round",
|
|
257
|
+
strokeLinejoin: "round",
|
|
258
|
+
style: { strokeDasharray: 20, strokeDashoffset: 20, animation: "flopay-draw 0.4s 0.15s ease forwards" }
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
] }) }),
|
|
262
|
+
status === "error" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { animation: "flopay-shake 0.4s ease" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
263
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "11", fill: "#ef4444" }),
|
|
264
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
265
|
+
"path",
|
|
266
|
+
{
|
|
267
|
+
d: "M8 8l8 8M16 8l-8 8",
|
|
268
|
+
stroke: "white",
|
|
269
|
+
strokeWidth: "2.5",
|
|
270
|
+
strokeLinecap: "round",
|
|
271
|
+
style: { strokeDasharray: 12, strokeDashoffset: 12, animation: "flopay-draw 0.3s 0.1s ease forwards" }
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
] }) })
|
|
275
|
+
] }),
|
|
276
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: {
|
|
277
|
+
fontSize: 14,
|
|
278
|
+
fontWeight: 600,
|
|
279
|
+
letterSpacing: "0.05em",
|
|
280
|
+
color: status === "success" ? "#16a34a" : status === "error" ? "#dc2626" : "#374151"
|
|
281
|
+
}, children: [
|
|
282
|
+
status === "processing" && "PROCESSING...",
|
|
283
|
+
status === "success" && "PAYMENT SUCCESSFUL",
|
|
284
|
+
status === "error" && "PAYMENT FAILED"
|
|
285
|
+
] }),
|
|
286
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: `
|
|
287
|
+
@keyframes flopay-spin { to { transform: rotate(360deg); } }
|
|
288
|
+
@keyframes flopay-pop { 0% { transform: scale(0); } 100% { transform: scale(1); } }
|
|
289
|
+
@keyframes flopay-draw { to { stroke-dashoffset: 0; } }
|
|
290
|
+
@keyframes flopay-shake { 0%,100% { transform: translateX(0); } 20%,60% { transform: translateX(-4px); } 40%,80% { transform: translateX(4px); } }
|
|
291
|
+
` })
|
|
292
|
+
] }) });
|
|
293
|
+
}
|
|
294
|
+
var SplitCardForm = (0, import_react5.forwardRef)(
|
|
295
|
+
function SplitCardForm2(props, ref) {
|
|
296
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SplitCardFormInner, { ...props, innerRef: ref });
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
function PayPalButtonInner({
|
|
300
|
+
sessionId,
|
|
301
|
+
email,
|
|
302
|
+
billingApiUrl,
|
|
303
|
+
onTokenizedBody,
|
|
304
|
+
onErrorChange,
|
|
305
|
+
isProcessing = false
|
|
306
|
+
}) {
|
|
307
|
+
const stripe = (0, import_react_stripe_js.useStripe)();
|
|
308
|
+
const elements = (0, import_react_stripe_js.useElements)();
|
|
309
|
+
const [ready, setReady] = (0, import_react5.useState)(false);
|
|
310
|
+
const [submitting, setSubmitting] = (0, import_react5.useState)(false);
|
|
311
|
+
const paypalResumeAttempted = (0, import_react5.useRef)(false);
|
|
312
|
+
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
313
|
+
(0, import_react5.useEffect)(() => {
|
|
314
|
+
if (!stripe || paypalResumeAttempted.current) return;
|
|
315
|
+
const params = new URLSearchParams(window.location.search);
|
|
316
|
+
const paymentIntentId = params.get("payment_intent");
|
|
317
|
+
const clientSecret = params.get("payment_intent_client_secret");
|
|
318
|
+
const redirectStatus = params.get("redirect_status");
|
|
319
|
+
if (!paymentIntentId || !clientSecret) return;
|
|
320
|
+
paypalResumeAttempted.current = true;
|
|
321
|
+
(async () => {
|
|
322
|
+
try {
|
|
323
|
+
setSubmitting(true);
|
|
324
|
+
if (redirectStatus === "failed") {
|
|
325
|
+
onErrorChange?.("PayPal payment was declined. Please try again.");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const { paymentIntent, error } = await stripe.retrievePaymentIntent(clientSecret);
|
|
329
|
+
if (error) {
|
|
330
|
+
onErrorChange?.(error.message ?? "Failed to retrieve PayPal payment status.");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (paymentIntent && (paymentIntent.status === "requires_capture" || paymentIntent.status === "succeeded")) {
|
|
334
|
+
const paymentMethodId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
|
|
335
|
+
onTokenizedBody({
|
|
336
|
+
id: paymentMethodId ?? paymentIntent.id,
|
|
337
|
+
type: "card",
|
|
338
|
+
threeDSecureActionResultTokenId: paymentIntent.id,
|
|
339
|
+
isPaypal: true
|
|
340
|
+
});
|
|
341
|
+
const url = new URL(window.location.href);
|
|
342
|
+
url.searchParams.delete("payment_intent");
|
|
343
|
+
url.searchParams.delete("payment_intent_client_secret");
|
|
344
|
+
url.searchParams.delete("redirect_status");
|
|
345
|
+
window.history.replaceState({}, "", url.toString());
|
|
346
|
+
} else {
|
|
347
|
+
onErrorChange?.("PayPal payment was not completed. Please try again.");
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
onErrorChange?.(err instanceof Error ? err.message : "Failed to complete PayPal payment.");
|
|
351
|
+
} finally {
|
|
352
|
+
setSubmitting(false);
|
|
353
|
+
}
|
|
354
|
+
})();
|
|
355
|
+
}, [stripe, onTokenizedBody, onErrorChange]);
|
|
356
|
+
const handlePayPalConfirm = (0, import_react5.useCallback)(async (_event) => {
|
|
357
|
+
if (!stripe || !elements) return;
|
|
358
|
+
try {
|
|
359
|
+
setSubmitting(true);
|
|
360
|
+
onErrorChange?.(null);
|
|
361
|
+
if (!sessionId || !email) {
|
|
362
|
+
throw new Error("Missing sessionId or email for PayPal payment");
|
|
363
|
+
}
|
|
364
|
+
const createPM = stripe.createPaymentMethod;
|
|
365
|
+
const { error: pmError, paymentMethod } = await createPM({ type: "paypal" });
|
|
366
|
+
if (pmError) {
|
|
367
|
+
console.warn("[FloPay] Could not create PayPal PM upfront:", pmError.message);
|
|
368
|
+
}
|
|
369
|
+
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: { "Content-Type": "application/json" },
|
|
372
|
+
body: JSON.stringify({
|
|
373
|
+
sessionId,
|
|
374
|
+
email,
|
|
375
|
+
paymentMethodType: paymentMethod?.id ?? "paypal",
|
|
376
|
+
isPaypal: "true"
|
|
377
|
+
})
|
|
378
|
+
});
|
|
379
|
+
if (!intentResponse.ok) throw new Error("Failed to create payment intent");
|
|
380
|
+
const intentJson = await intentResponse.json();
|
|
381
|
+
const intentClientSecret = intentJson.data?.id;
|
|
382
|
+
if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
|
|
383
|
+
const confirmParams = {
|
|
384
|
+
return_url: window.location.href
|
|
385
|
+
};
|
|
386
|
+
if (paymentMethod?.id) {
|
|
387
|
+
confirmParams["payment_method"] = paymentMethod.id;
|
|
388
|
+
}
|
|
389
|
+
const { error: confirmError, paymentIntent } = await stripe.confirmPayment({
|
|
390
|
+
clientSecret: intentClientSecret,
|
|
391
|
+
confirmParams,
|
|
392
|
+
redirect: "if_required"
|
|
393
|
+
});
|
|
394
|
+
if (confirmError) {
|
|
395
|
+
onErrorChange?.(confirmError.message ?? "PayPal payment failed.");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const confirmedPmId = typeof paymentIntent?.payment_method === "string" ? paymentIntent.payment_method : paymentIntent?.payment_method?.id;
|
|
399
|
+
onTokenizedBody({
|
|
400
|
+
id: confirmedPmId ?? paymentIntent?.id,
|
|
401
|
+
type: "card",
|
|
402
|
+
threeDSecureActionResultTokenId: paymentIntent?.id,
|
|
403
|
+
isPaypal: true
|
|
404
|
+
});
|
|
405
|
+
} catch (err) {
|
|
406
|
+
onErrorChange?.(err instanceof Error ? err.message : "PayPal payment failed. Please try again.");
|
|
407
|
+
} finally {
|
|
408
|
+
setSubmitting(false);
|
|
409
|
+
}
|
|
410
|
+
}, [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange]);
|
|
411
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
412
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginBottom: ready ? "0.5rem" : 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
413
|
+
import_react_stripe_js.ExpressCheckoutElement,
|
|
414
|
+
{
|
|
415
|
+
onReady: () => setReady(true),
|
|
416
|
+
onLoadError: () => {
|
|
417
|
+
},
|
|
418
|
+
onConfirm: handlePayPalConfirm,
|
|
419
|
+
options: {
|
|
420
|
+
buttonType: { paypal: "paypal" },
|
|
421
|
+
paymentMethods: {
|
|
422
|
+
applePay: "never",
|
|
423
|
+
googlePay: "never",
|
|
424
|
+
paypal: "auto",
|
|
425
|
+
link: "never"
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
) }),
|
|
430
|
+
submitting && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProcessingOverlay, { status: "processing" })
|
|
431
|
+
] });
|
|
432
|
+
}
|
|
433
|
+
function WalletButtonInner({
|
|
434
|
+
sessionId,
|
|
435
|
+
email,
|
|
436
|
+
billingApiUrl,
|
|
437
|
+
showApplePay = true,
|
|
438
|
+
showGooglePay = true,
|
|
439
|
+
onTokenizedBody,
|
|
440
|
+
onErrorChange
|
|
441
|
+
}) {
|
|
442
|
+
const stripe = (0, import_react_stripe_js.useStripe)();
|
|
443
|
+
const elements = (0, import_react_stripe_js.useElements)();
|
|
444
|
+
const [ready, setReady] = (0, import_react5.useState)(false);
|
|
445
|
+
const [submitting, setSubmitting] = (0, import_react5.useState)(false);
|
|
446
|
+
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
447
|
+
const handleWalletConfirm = (0, import_react5.useCallback)(
|
|
448
|
+
async (_event) => {
|
|
449
|
+
if (!stripe || !elements) return;
|
|
450
|
+
try {
|
|
451
|
+
setSubmitting(true);
|
|
452
|
+
onErrorChange?.(null);
|
|
453
|
+
const { error: submitError } = await elements.submit();
|
|
454
|
+
if (submitError) {
|
|
455
|
+
onErrorChange?.(submitError.message ?? "Wallet payment failed.");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const { error: pmError, paymentMethod } = await stripe.createPaymentMethod({ elements });
|
|
459
|
+
if (pmError || !paymentMethod) {
|
|
460
|
+
onErrorChange?.(pmError?.message ?? "Failed to create payment method.");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (!sessionId || !email) {
|
|
464
|
+
throw new Error("Missing sessionId or email for wallet payment");
|
|
465
|
+
}
|
|
466
|
+
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
467
|
+
method: "POST",
|
|
468
|
+
headers: { "Content-Type": "application/json" },
|
|
469
|
+
body: JSON.stringify({
|
|
470
|
+
sessionId,
|
|
471
|
+
email,
|
|
472
|
+
paymentMethodType: paymentMethod.id,
|
|
473
|
+
isPaypal: false
|
|
474
|
+
})
|
|
475
|
+
});
|
|
476
|
+
if (!intentResponse.ok) throw new Error("Failed to create payment intent");
|
|
477
|
+
const intentJson = await intentResponse.json();
|
|
478
|
+
const intentClientSecret = intentJson.data?.id;
|
|
479
|
+
if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
|
|
480
|
+
const { error: confirmError, paymentIntent } = await stripe.confirmCardPayment(
|
|
481
|
+
intentClientSecret,
|
|
482
|
+
{ payment_method: paymentMethod.id }
|
|
483
|
+
);
|
|
484
|
+
if (confirmError) {
|
|
485
|
+
onErrorChange?.(confirmError.message ?? "Wallet payment failed.");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
onTokenizedBody({
|
|
489
|
+
id: paymentMethod.id,
|
|
490
|
+
type: "card",
|
|
491
|
+
threeDSecureActionResultTokenId: paymentIntent?.id
|
|
492
|
+
});
|
|
493
|
+
} catch (err) {
|
|
494
|
+
onErrorChange?.(err instanceof Error ? err.message : "Wallet payment failed. Please try again.");
|
|
495
|
+
} finally {
|
|
496
|
+
setSubmitting(false);
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
[stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange]
|
|
500
|
+
);
|
|
501
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
502
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginBottom: ready ? "0.5rem" : 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
503
|
+
import_react_stripe_js.ExpressCheckoutElement,
|
|
504
|
+
{
|
|
505
|
+
onReady: () => setReady(true),
|
|
506
|
+
onLoadError: () => {
|
|
507
|
+
},
|
|
508
|
+
onConfirm: handleWalletConfirm,
|
|
509
|
+
options: {
|
|
510
|
+
buttonType: { applePay: "plain", googlePay: "plain" },
|
|
511
|
+
paymentMethods: {
|
|
512
|
+
applePay: showApplePay ? "auto" : "never",
|
|
513
|
+
googlePay: showGooglePay ? "auto" : "never",
|
|
514
|
+
paypal: "never",
|
|
515
|
+
link: "never"
|
|
516
|
+
},
|
|
517
|
+
layout: { overflow: "never" }
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
) }),
|
|
521
|
+
submitting && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProcessingOverlay, { status: "processing" })
|
|
522
|
+
] });
|
|
523
|
+
}
|
|
524
|
+
function SplitCardFormInner({
|
|
525
|
+
sessionId,
|
|
526
|
+
billingApiUrl,
|
|
527
|
+
email,
|
|
528
|
+
userId,
|
|
529
|
+
onComplete,
|
|
530
|
+
onError,
|
|
531
|
+
onTokenizedBody,
|
|
532
|
+
firstName,
|
|
533
|
+
lastName,
|
|
534
|
+
chv,
|
|
535
|
+
submitLabel = "CONFIRM PAYMENT",
|
|
536
|
+
className,
|
|
537
|
+
children,
|
|
538
|
+
isProcessing: externalProcessing,
|
|
539
|
+
error: externalError,
|
|
540
|
+
onErrorChange,
|
|
541
|
+
onFirstNameChange,
|
|
542
|
+
onLastNameChange,
|
|
543
|
+
showPayPal = true,
|
|
544
|
+
showApplePay = true,
|
|
545
|
+
showGooglePay = true,
|
|
546
|
+
totalAmount = 0,
|
|
547
|
+
currency = "usd",
|
|
548
|
+
innerRef
|
|
549
|
+
}) {
|
|
550
|
+
const flopay = useFloPay();
|
|
551
|
+
const elements = useElements();
|
|
552
|
+
const contextBillingUrl = useBillingApiUrl();
|
|
553
|
+
const [processing, setProcessing] = (0, import_react5.useState)(false);
|
|
554
|
+
const [error, setError] = (0, import_react5.useState)(null);
|
|
555
|
+
const [is3DSActive, setIs3DSActive] = (0, import_react5.useState)(false);
|
|
556
|
+
const [fullName, setFullName] = (0, import_react5.useState)("");
|
|
557
|
+
const [formReady, setFormReady] = (0, import_react5.useState)(false);
|
|
558
|
+
const [overlayStatus, setOverlayStatus] = (0, import_react5.useState)(null);
|
|
559
|
+
const processingRef = (0, import_react5.useRef)(false);
|
|
560
|
+
const resolvedBillingApiUrl = billingApiUrl || contextBillingUrl;
|
|
561
|
+
const displayError = externalError ?? error;
|
|
562
|
+
const isSubmitting = externalProcessing ?? processing;
|
|
563
|
+
const isSelfContained = !onTokenizedBody;
|
|
564
|
+
const baseUrl = resolvedBillingApiUrl.replace(/\/+$/, "");
|
|
565
|
+
const stripeInstance = (0, import_react5.useMemo)(() => {
|
|
566
|
+
if (!flopay) return null;
|
|
567
|
+
return flopay.getRawProvider();
|
|
568
|
+
}, [flopay]);
|
|
569
|
+
const amountInCents = totalAmount || 100;
|
|
570
|
+
const walletOptions = (0, import_react5.useMemo)(() => ({
|
|
571
|
+
mode: "payment",
|
|
572
|
+
amount: amountInCents,
|
|
573
|
+
currency: currency.toLowerCase(),
|
|
574
|
+
paymentMethodCreation: "manual",
|
|
575
|
+
captureMethod: "manual"
|
|
576
|
+
}), [amountInCents, currency]);
|
|
577
|
+
const paypalOptions = (0, import_react5.useMemo)(() => ({
|
|
578
|
+
mode: "payment",
|
|
579
|
+
amount: amountInCents,
|
|
580
|
+
currency: currency.toLowerCase(),
|
|
581
|
+
captureMethod: "manual"
|
|
582
|
+
}), [amountInCents, currency]);
|
|
583
|
+
const updateError = (0, import_react5.useCallback)(
|
|
584
|
+
(err) => {
|
|
585
|
+
setError(err);
|
|
586
|
+
onErrorChange?.(err);
|
|
587
|
+
},
|
|
588
|
+
[onErrorChange]
|
|
589
|
+
);
|
|
590
|
+
const showWallets = showApplePay || showGooglePay;
|
|
591
|
+
const handleNameChange = (0, import_react5.useCallback)((value) => {
|
|
592
|
+
setFullName(value);
|
|
593
|
+
const parts = value.trim().split(/\s+/);
|
|
594
|
+
onFirstNameChange?.(parts[0] ?? "");
|
|
595
|
+
onLastNameChange?.(parts.length > 1 ? parts.slice(1).join(" ") : "");
|
|
596
|
+
}, [onFirstNameChange, onLastNameChange]);
|
|
597
|
+
const processPaymentInternal = (0, import_react5.useCallback)(
|
|
598
|
+
async (tokenizedBody) => {
|
|
599
|
+
if (processingRef.current) return;
|
|
600
|
+
processingRef.current = true;
|
|
601
|
+
setProcessing(true);
|
|
602
|
+
setOverlayStatus("processing");
|
|
603
|
+
updateError(null);
|
|
604
|
+
try {
|
|
605
|
+
const response = await fetch(`${baseUrl}/v1/checkouts/sessions/process`, {
|
|
606
|
+
method: "POST",
|
|
607
|
+
headers: {
|
|
608
|
+
"Content-Type": "application/json",
|
|
609
|
+
"x-user-id": userId ?? ""
|
|
610
|
+
},
|
|
611
|
+
body: JSON.stringify({
|
|
612
|
+
sessionId,
|
|
613
|
+
tokenizedData: tokenizedBody,
|
|
614
|
+
accountData: {
|
|
615
|
+
userId: userId ?? "",
|
|
616
|
+
email: email ?? "",
|
|
617
|
+
firstName: firstName ?? fullName.trim().split(/\s+/)[0] ?? "",
|
|
618
|
+
lastName: lastName ?? fullName.trim().split(/\s+/).slice(1).join(" ") ?? ""
|
|
619
|
+
},
|
|
620
|
+
chv
|
|
621
|
+
})
|
|
622
|
+
});
|
|
623
|
+
if (response.ok) {
|
|
624
|
+
setOverlayStatus("success");
|
|
625
|
+
await new Promise((r) => setTimeout(r, 1200));
|
|
626
|
+
onComplete?.({
|
|
627
|
+
status: "succeeded",
|
|
628
|
+
paymentIntentId: tokenizedBody.threeDSecureActionResultTokenId
|
|
629
|
+
});
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const json = await response.json().catch(() => null);
|
|
633
|
+
if (json?.type === "3ds_required") {
|
|
634
|
+
const secret = json["threeDSecureToken"];
|
|
635
|
+
if (!flopay || !secret) {
|
|
636
|
+
setOverlayStatus("error");
|
|
637
|
+
updateError("3DS authentication required but no token provided.");
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
setIs3DSActive(true);
|
|
641
|
+
try {
|
|
642
|
+
const result = await flopay.confirmPayment({
|
|
643
|
+
clientSecret: secret,
|
|
644
|
+
returnUrl: window.location.href
|
|
645
|
+
});
|
|
646
|
+
if (result.error) {
|
|
647
|
+
setOverlayStatus("error");
|
|
648
|
+
updateError(result.error.message);
|
|
649
|
+
onError?.(result.error);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (result.status === "succeeded" || result.status === "processing") {
|
|
653
|
+
processingRef.current = false;
|
|
654
|
+
await processPaymentInternal({
|
|
655
|
+
id: result.paymentIntentId,
|
|
656
|
+
type: "card",
|
|
657
|
+
threeDSecureActionResultTokenId: result.paymentIntentId
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
} finally {
|
|
661
|
+
setIs3DSActive(false);
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (json?.type === "paypal_redirect_required") {
|
|
666
|
+
const secret = json["threeDSecureToken"];
|
|
667
|
+
const savedPmId = json["paymentMethodId"] || tokenizedBody.id || "";
|
|
668
|
+
if (!flopay || !secret) {
|
|
669
|
+
setOverlayStatus("error");
|
|
670
|
+
updateError("PayPal authorization required but no token provided.");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const stripeInstance2 = flopay.getRawProvider();
|
|
675
|
+
if (!stripeInstance2) {
|
|
676
|
+
updateError("Payment provider not available.");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const confirmParams = {
|
|
680
|
+
return_url: window.location.href
|
|
681
|
+
};
|
|
682
|
+
if (savedPmId) {
|
|
683
|
+
confirmParams["payment_method"] = savedPmId;
|
|
684
|
+
}
|
|
685
|
+
const { error: confirmError } = await stripeInstance2.confirmPayment({
|
|
686
|
+
clientSecret: secret,
|
|
687
|
+
confirmParams,
|
|
688
|
+
redirect: "if_required"
|
|
689
|
+
});
|
|
690
|
+
if (confirmError) {
|
|
691
|
+
setOverlayStatus("error");
|
|
692
|
+
updateError(confirmError.message ?? "PayPal payment failed.");
|
|
693
|
+
}
|
|
694
|
+
} catch (err) {
|
|
695
|
+
setOverlayStatus("error");
|
|
696
|
+
updateError(err instanceof Error ? err.message : "PayPal authorization failed.");
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
setOverlayStatus("error");
|
|
701
|
+
updateError(json?.message ?? "Payment failed. Please try again.");
|
|
702
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
703
|
+
} catch (err) {
|
|
704
|
+
setOverlayStatus("error");
|
|
705
|
+
updateError(err instanceof Error ? err.message : "An unexpected error occurred");
|
|
706
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
707
|
+
} finally {
|
|
708
|
+
setProcessing(false);
|
|
709
|
+
setOverlayStatus(null);
|
|
710
|
+
processingRef.current = false;
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
[baseUrl, sessionId, userId, email, firstName, lastName, fullName, chv, flopay, onComplete, onError, updateError]
|
|
714
|
+
);
|
|
715
|
+
const dispatchTokenizedBody = (0, import_react5.useCallback)(
|
|
716
|
+
(tokenizedBody) => {
|
|
717
|
+
if (onTokenizedBody) {
|
|
718
|
+
onTokenizedBody(tokenizedBody);
|
|
719
|
+
} else {
|
|
720
|
+
processPaymentInternal(tokenizedBody);
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
[onTokenizedBody, processPaymentInternal]
|
|
724
|
+
);
|
|
725
|
+
(0, import_react5.useImperativeHandle)(innerRef, () => ({
|
|
726
|
+
async handleNextAction(secret) {
|
|
727
|
+
if (!flopay) return;
|
|
728
|
+
setIs3DSActive(true);
|
|
729
|
+
try {
|
|
730
|
+
const result = await flopay.confirmPayment({
|
|
731
|
+
clientSecret: secret,
|
|
732
|
+
returnUrl: window.location.href
|
|
733
|
+
});
|
|
734
|
+
if (result.error) {
|
|
735
|
+
updateError(result.error.message);
|
|
736
|
+
onError?.(result.error);
|
|
737
|
+
} else if (result.status === "succeeded" || result.status === "processing") {
|
|
738
|
+
dispatchTokenizedBody({
|
|
739
|
+
id: result.paymentIntentId,
|
|
740
|
+
type: "card",
|
|
741
|
+
threeDSecureActionResultTokenId: result.paymentIntentId
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
} catch (err) {
|
|
745
|
+
updateError(err instanceof Error ? err.message : "Payment authentication failed.");
|
|
746
|
+
} finally {
|
|
747
|
+
setIs3DSActive(false);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}), [flopay, dispatchTokenizedBody, onError, updateError]);
|
|
751
|
+
(0, import_react5.useEffect)(() => {
|
|
752
|
+
if (typeof window === "undefined") return;
|
|
753
|
+
const stored = localStorage.getItem(WALLET_RESUME_KEY);
|
|
754
|
+
if (!stored) return;
|
|
755
|
+
try {
|
|
756
|
+
const payload = JSON.parse(stored);
|
|
757
|
+
if (payload.sessionId === sessionId) {
|
|
758
|
+
localStorage.removeItem(WALLET_RESUME_KEY);
|
|
759
|
+
dispatchTokenizedBody({
|
|
760
|
+
id: payload.tokenId,
|
|
761
|
+
type: payload.tokenType,
|
|
762
|
+
threeDSecureActionResultTokenId: payload.paymentIntentId
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
} catch {
|
|
766
|
+
localStorage.removeItem(WALLET_RESUME_KEY);
|
|
767
|
+
}
|
|
768
|
+
}, [sessionId, dispatchTokenizedBody]);
|
|
769
|
+
const handleSubmit = (0, import_react5.useCallback)(
|
|
770
|
+
async (e) => {
|
|
771
|
+
e.preventDefault();
|
|
772
|
+
if (!flopay || !elements || isSubmitting || processingRef.current) return;
|
|
773
|
+
setProcessing(true);
|
|
774
|
+
setOverlayStatus("processing");
|
|
775
|
+
updateError(null);
|
|
776
|
+
let handedOff = false;
|
|
777
|
+
try {
|
|
778
|
+
const submitResult = await flopay.submitElements();
|
|
779
|
+
if (submitResult.error) {
|
|
780
|
+
updateError(submitResult.error.message);
|
|
781
|
+
onError?.(submitResult.error);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const pmResult = await flopay.createPaymentMethod();
|
|
785
|
+
if (pmResult.error || !pmResult.paymentMethodId) {
|
|
786
|
+
updateError(pmResult.error?.message ?? "Failed to create payment method.");
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (!sessionId || !email) {
|
|
790
|
+
throw new import_shared3.FloPayError("Missing sessionId or email", "validation_error");
|
|
791
|
+
}
|
|
792
|
+
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
793
|
+
method: "POST",
|
|
794
|
+
headers: { "Content-Type": "application/json" },
|
|
795
|
+
body: JSON.stringify({
|
|
796
|
+
sessionId,
|
|
797
|
+
email,
|
|
798
|
+
paymentMethodType: pmResult.paymentMethodId,
|
|
799
|
+
isPaypal: false
|
|
800
|
+
})
|
|
801
|
+
});
|
|
802
|
+
if (!intentResponse.ok) throw new import_shared3.FloPayError("Failed to create payment intent", "api_error");
|
|
803
|
+
const intentJson = await intentResponse.json();
|
|
804
|
+
const intentClientSecret = intentJson.data?.id;
|
|
805
|
+
if (!intentClientSecret) throw new import_shared3.FloPayError("No client_secret in payment intent response", "api_error");
|
|
806
|
+
const confirmResult = await flopay.confirmCardPayment({
|
|
807
|
+
clientSecret: intentClientSecret,
|
|
808
|
+
paymentMethodId: pmResult.paymentMethodId
|
|
809
|
+
});
|
|
810
|
+
if (confirmResult.error) {
|
|
811
|
+
setOverlayStatus("error");
|
|
812
|
+
updateError(confirmResult.error.message);
|
|
813
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
handedOff = isSelfContained;
|
|
817
|
+
dispatchTokenizedBody({
|
|
818
|
+
id: pmResult.paymentMethodId,
|
|
819
|
+
type: "card",
|
|
820
|
+
threeDSecureActionResultTokenId: confirmResult.paymentIntentId
|
|
821
|
+
});
|
|
822
|
+
} catch (err) {
|
|
823
|
+
setOverlayStatus("error");
|
|
824
|
+
updateError(err instanceof Error ? err.message : "An unexpected error occurred");
|
|
825
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
826
|
+
} finally {
|
|
827
|
+
if (!handedOff) {
|
|
828
|
+
setProcessing(false);
|
|
829
|
+
setOverlayStatus(null);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
[flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError]
|
|
834
|
+
);
|
|
835
|
+
const isReady = flopay !== null && elements !== null;
|
|
836
|
+
if (!isReady) {
|
|
837
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { "data-testid": "flopay-loading", "aria-busy": "true", children: "Loading payment form..." });
|
|
838
|
+
}
|
|
839
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { onSubmit: handleSubmit, className, style: { position: "relative" }, children: [
|
|
840
|
+
overlayStatus && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProcessingOverlay, { status: overlayStatus }),
|
|
841
|
+
showWallets && stripeInstance && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_stripe_js.Elements, { stripe: stripeInstance, options: walletOptions, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
842
|
+
WalletButtonInner,
|
|
843
|
+
{
|
|
844
|
+
sessionId,
|
|
845
|
+
email,
|
|
846
|
+
billingApiUrl: resolvedBillingApiUrl,
|
|
847
|
+
showApplePay,
|
|
848
|
+
showGooglePay,
|
|
849
|
+
onTokenizedBody: dispatchTokenizedBody,
|
|
850
|
+
onErrorChange: updateError
|
|
851
|
+
}
|
|
852
|
+
) }),
|
|
853
|
+
showPayPal && stripeInstance && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_stripe_js.Elements, { stripe: stripeInstance, options: paypalOptions, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
854
|
+
PayPalButtonInner,
|
|
855
|
+
{
|
|
856
|
+
sessionId,
|
|
857
|
+
email,
|
|
858
|
+
billingApiUrl: resolvedBillingApiUrl,
|
|
859
|
+
onTokenizedBody: dispatchTokenizedBody,
|
|
860
|
+
onErrorChange: updateError,
|
|
861
|
+
isProcessing: isSubmitting
|
|
862
|
+
}
|
|
863
|
+
) }),
|
|
864
|
+
(showWallets && stripeInstance || showPayPal && stripeInstance) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
|
|
865
|
+
display: "flex",
|
|
866
|
+
alignItems: "center",
|
|
867
|
+
gap: "0.75rem",
|
|
868
|
+
margin: "0.5rem 0 0.75rem",
|
|
869
|
+
color: "#999",
|
|
870
|
+
fontSize: "0.85rem"
|
|
871
|
+
}, children: [
|
|
872
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { flex: 1, height: 1, backgroundColor: "#ddd" } }),
|
|
873
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "or pay with card" }),
|
|
874
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { flex: 1, height: 1, backgroundColor: "#ddd" } })
|
|
875
|
+
] }),
|
|
876
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { backgroundColor: "#EDEDFF", borderRadius: "8px", padding: "1rem" }, children: [
|
|
877
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { textAlign: "center", fontWeight: 600, fontSize: "1.1rem", padding: "0.5rem 0", color: "#262833" }, children: "Secure card checkout" }),
|
|
878
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
|
|
879
|
+
backgroundColor: "white",
|
|
880
|
+
border: "1px solid #A4A4FF",
|
|
881
|
+
borderTopLeftRadius: "8px",
|
|
882
|
+
borderTopRightRadius: "8px",
|
|
883
|
+
padding: "10px"
|
|
884
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardNumberElement, { onReady: () => setFormReady(true) }) }),
|
|
885
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex" }, children: [
|
|
886
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
|
|
887
|
+
flex: 1,
|
|
888
|
+
backgroundColor: "white",
|
|
889
|
+
border: "1px solid #A4A4FF",
|
|
890
|
+
borderTop: "none",
|
|
891
|
+
borderRight: "none",
|
|
892
|
+
borderBottomLeftRadius: "8px",
|
|
893
|
+
padding: "10px"
|
|
894
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardExpiryElement, {}) }),
|
|
895
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
|
|
896
|
+
flex: 1,
|
|
897
|
+
backgroundColor: "white",
|
|
898
|
+
border: "1px solid #A4A4FF",
|
|
899
|
+
borderTop: "none",
|
|
900
|
+
borderBottomRightRadius: "8px",
|
|
901
|
+
padding: "10px"
|
|
902
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardCvcElement, {}) })
|
|
903
|
+
] }),
|
|
904
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
|
|
905
|
+
backgroundColor: "white",
|
|
906
|
+
border: "1px solid #A4A4FF",
|
|
907
|
+
borderRadius: "8px",
|
|
908
|
+
marginTop: "0.5rem",
|
|
909
|
+
padding: "10px"
|
|
910
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
911
|
+
"input",
|
|
912
|
+
{
|
|
913
|
+
placeholder: "Full Name on Card",
|
|
914
|
+
autoComplete: "cc-name",
|
|
915
|
+
value: fullName,
|
|
916
|
+
onChange: (e) => handleNameChange(e.target.value),
|
|
917
|
+
disabled: isSubmitting,
|
|
918
|
+
required: true,
|
|
919
|
+
style: {
|
|
920
|
+
width: "100%",
|
|
921
|
+
border: "none",
|
|
922
|
+
outline: "none",
|
|
923
|
+
fontSize: "16px",
|
|
924
|
+
fontFamily: "Poppins, sans-serif",
|
|
925
|
+
color: "#262833"
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
) }),
|
|
929
|
+
displayError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", "data-testid": "flopay-error", style: { color: "red", margin: "0.75rem 0", fontSize: "0.9rem" }, children: displayError }),
|
|
930
|
+
children ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
931
|
+
"button",
|
|
932
|
+
{
|
|
933
|
+
type: "submit",
|
|
934
|
+
disabled: !formReady || isSubmitting,
|
|
935
|
+
"data-testid": "flopay-submit",
|
|
936
|
+
style: {
|
|
937
|
+
width: "100%",
|
|
938
|
+
padding: "0.875rem",
|
|
939
|
+
marginTop: "1rem",
|
|
940
|
+
backgroundColor: "#4A49FF",
|
|
941
|
+
color: "white",
|
|
942
|
+
border: "none",
|
|
943
|
+
borderRadius: "8px",
|
|
944
|
+
fontSize: "1rem",
|
|
945
|
+
fontWeight: 600,
|
|
946
|
+
cursor: !formReady || isSubmitting ? "not-allowed" : "pointer",
|
|
947
|
+
opacity: !formReady || isSubmitting ? 0.5 : 1
|
|
948
|
+
},
|
|
949
|
+
children: isSubmitting ? "PROCESSING..." : submitLabel
|
|
950
|
+
}
|
|
951
|
+
),
|
|
952
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
|
|
953
|
+
backgroundColor: "#EFF9F0",
|
|
954
|
+
borderRadius: "8px",
|
|
955
|
+
padding: "0.75rem",
|
|
956
|
+
marginTop: "0.75rem",
|
|
957
|
+
textAlign: "center",
|
|
958
|
+
fontSize: "0.85rem",
|
|
959
|
+
fontWeight: 600,
|
|
960
|
+
color: "#7DAD3A"
|
|
961
|
+
}, children: "Secure Card Checkout" })
|
|
962
|
+
] })
|
|
963
|
+
] });
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/flopay-checkout.tsx
|
|
967
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
968
|
+
function FloPayCheckout({
|
|
969
|
+
sessionId,
|
|
970
|
+
billingApiUrl,
|
|
971
|
+
appearance,
|
|
972
|
+
locale,
|
|
973
|
+
fallbackPublishableKey,
|
|
974
|
+
loading: loadingNode,
|
|
975
|
+
error: errorNode,
|
|
976
|
+
onComplete,
|
|
977
|
+
onError,
|
|
978
|
+
showPayPal = true,
|
|
979
|
+
showApplePay = true,
|
|
980
|
+
showGooglePay = true,
|
|
981
|
+
submitLabel,
|
|
982
|
+
className,
|
|
983
|
+
children,
|
|
984
|
+
checkoutMode: checkoutModeProp,
|
|
985
|
+
confirmLabel,
|
|
986
|
+
renderConfirmButton,
|
|
987
|
+
onSessionCompleted
|
|
988
|
+
}) {
|
|
989
|
+
const resolvedBillingUrl = (0, import_shared4.resolveBillingApiUrl)(billingApiUrl);
|
|
990
|
+
const [unified, setUnified] = (0, import_react6.useState)(null);
|
|
991
|
+
const [flopay, setFloPay] = (0, import_react6.useState)(null);
|
|
992
|
+
const flopayRef = (0, import_react6.useRef)(null);
|
|
993
|
+
const [session, setSession] = (0, import_react6.useState)(null);
|
|
994
|
+
const [isLoading, setIsLoading] = (0, import_react6.useState)(true);
|
|
995
|
+
const [loadError, setLoadError] = (0, import_react6.useState)(null);
|
|
996
|
+
const [currentMode, setCurrentMode] = (0, import_react6.useState)("full");
|
|
997
|
+
const [confirmProcessing, setConfirmProcessing] = (0, import_react6.useState)(false);
|
|
998
|
+
const [modeError, setModeError] = (0, import_react6.useState)(null);
|
|
999
|
+
const autoCheckoutAttempted = (0, import_react6.useRef)(false);
|
|
1000
|
+
const processPaymentForMode = (0, import_react6.useCallback)(
|
|
1001
|
+
async (sess) => {
|
|
1002
|
+
const baseUrl = resolvedBillingUrl.replace(/\/+$/, "");
|
|
1003
|
+
const response = await fetch(`${baseUrl}/v1/checkouts/sessions/process`, {
|
|
1004
|
+
method: "POST",
|
|
1005
|
+
headers: {
|
|
1006
|
+
"Content-Type": "application/json",
|
|
1007
|
+
"x-user-id": sess.customer?.id ?? ""
|
|
1008
|
+
},
|
|
1009
|
+
body: JSON.stringify({
|
|
1010
|
+
sessionId,
|
|
1011
|
+
tokenizedData: { id: void 0 },
|
|
1012
|
+
accountData: {
|
|
1013
|
+
userId: sess.customer?.id ?? "",
|
|
1014
|
+
email: sess.customer?.email ?? "",
|
|
1015
|
+
firstName: sess.customer?.firstName ?? "",
|
|
1016
|
+
lastName: sess.customer?.lastName ?? ""
|
|
1017
|
+
}
|
|
1018
|
+
})
|
|
1019
|
+
});
|
|
1020
|
+
if (response.ok) {
|
|
1021
|
+
onComplete?.({ status: "succeeded" });
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
const json = await response.json().catch(() => null);
|
|
1025
|
+
if ((json?.type === "paypal_redirect_required" || json?.type === "3ds_required") && json?.threeDSecureToken) {
|
|
1026
|
+
return {
|
|
1027
|
+
type: json.type,
|
|
1028
|
+
threeDSecureToken: json.threeDSecureToken,
|
|
1029
|
+
paymentMethodId: json.paymentMethodId
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
throw new import_shared4.FloPayError(
|
|
1033
|
+
json?.message ?? "Payment failed. Please try again.",
|
|
1034
|
+
"api_error"
|
|
1035
|
+
);
|
|
1036
|
+
},
|
|
1037
|
+
[resolvedBillingUrl, sessionId, onComplete]
|
|
1038
|
+
);
|
|
1039
|
+
(0, import_react6.useEffect)(() => {
|
|
1040
|
+
let cancelled = false;
|
|
1041
|
+
setIsLoading(true);
|
|
1042
|
+
setLoadError(null);
|
|
1043
|
+
async function init() {
|
|
1044
|
+
try {
|
|
1045
|
+
const api = new import_js.PaymentAPI(resolvedBillingUrl);
|
|
1046
|
+
const result = await api.getUnifiedCheckoutSession(sessionId);
|
|
1047
|
+
if (cancelled) return;
|
|
1048
|
+
setUnified(result);
|
|
1049
|
+
const sess = result.data.session ?? null;
|
|
1050
|
+
setSession(sess);
|
|
1051
|
+
if (!sess) {
|
|
1052
|
+
throw new import_shared4.FloPayError("No session data returned", "api_error");
|
|
1053
|
+
}
|
|
1054
|
+
if (sess.status === "complete") {
|
|
1055
|
+
setIsLoading(false);
|
|
1056
|
+
onSessionCompleted?.(sess.successUrl ?? "");
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const effectiveMode = checkoutModeProp ?? sess.checkoutMode ?? "full";
|
|
1060
|
+
setCurrentMode(effectiveMode);
|
|
1061
|
+
const hasPayPalRedirectParams = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("payment_intent");
|
|
1062
|
+
if (effectiveMode === "auto" && !autoCheckoutAttempted.current && !hasPayPalRedirectParams) {
|
|
1063
|
+
autoCheckoutAttempted.current = true;
|
|
1064
|
+
const stripeInitPromise = initStripe(result, sess);
|
|
1065
|
+
try {
|
|
1066
|
+
const redirectResult = await processPaymentForMode(sess);
|
|
1067
|
+
if (!redirectResult) {
|
|
1068
|
+
if (!cancelled) setIsLoading(false);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (cancelled) return;
|
|
1072
|
+
await stripeInitPromise;
|
|
1073
|
+
if (redirectResult.type === "paypal_redirect_required") {
|
|
1074
|
+
const stripeRef = flopayRef.current;
|
|
1075
|
+
if (stripeRef) {
|
|
1076
|
+
const rawStripe = stripeRef.getRawProvider();
|
|
1077
|
+
if (rawStripe) {
|
|
1078
|
+
const confirmParams = {
|
|
1079
|
+
return_url: window.location.href
|
|
1080
|
+
};
|
|
1081
|
+
if (redirectResult.paymentMethodId) {
|
|
1082
|
+
confirmParams["payment_method"] = redirectResult.paymentMethodId;
|
|
1083
|
+
}
|
|
1084
|
+
await rawStripe.confirmPayment({
|
|
1085
|
+
clientSecret: redirectResult.threeDSecureToken,
|
|
1086
|
+
confirmParams,
|
|
1087
|
+
redirect: "if_required"
|
|
1088
|
+
});
|
|
1089
|
+
if (!cancelled) setIsLoading(false);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (!cancelled) {
|
|
1095
|
+
setCurrentMode("full");
|
|
1096
|
+
setIsLoading(false);
|
|
1097
|
+
}
|
|
1098
|
+
return;
|
|
1099
|
+
} catch {
|
|
1100
|
+
if (cancelled) return;
|
|
1101
|
+
setCurrentMode("full");
|
|
1102
|
+
await stripeInitPromise;
|
|
1103
|
+
if (!cancelled) setIsLoading(false);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
await initStripe(result, sess);
|
|
1108
|
+
if (!cancelled) setIsLoading(false);
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
if (cancelled) return;
|
|
1111
|
+
const floPayErr = err instanceof import_shared4.FloPayError ? err : new import_shared4.FloPayError(
|
|
1112
|
+
err instanceof Error ? err.message : "Failed to initialize checkout",
|
|
1113
|
+
"api_error"
|
|
1114
|
+
);
|
|
1115
|
+
setLoadError(floPayErr);
|
|
1116
|
+
setIsLoading(false);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
async function initStripe(result, _sess) {
|
|
1120
|
+
let publishableKey;
|
|
1121
|
+
if (result.provider === "stripe") {
|
|
1122
|
+
publishableKey = result.data.stripe?.publishableKey;
|
|
1123
|
+
}
|
|
1124
|
+
if (!publishableKey) publishableKey = fallbackPublishableKey;
|
|
1125
|
+
if (!publishableKey) {
|
|
1126
|
+
throw new import_shared4.FloPayError(
|
|
1127
|
+
"No publishable key found in session response. Provide a fallbackPublishableKey prop or ensure the session includes gatewayData.publishableKey.",
|
|
1128
|
+
"validation_error"
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
const instance = await (0, import_js.loadFloPay)(publishableKey, {
|
|
1132
|
+
billingApiUrl: resolvedBillingUrl,
|
|
1133
|
+
locale
|
|
1134
|
+
});
|
|
1135
|
+
flopayRef.current = instance;
|
|
1136
|
+
setFloPay(instance);
|
|
1137
|
+
}
|
|
1138
|
+
init();
|
|
1139
|
+
return () => {
|
|
1140
|
+
cancelled = true;
|
|
1141
|
+
};
|
|
1142
|
+
}, [
|
|
1143
|
+
sessionId,
|
|
1144
|
+
resolvedBillingUrl,
|
|
1145
|
+
fallbackPublishableKey,
|
|
1146
|
+
locale,
|
|
1147
|
+
checkoutModeProp,
|
|
1148
|
+
onSessionCompleted,
|
|
1149
|
+
processPaymentForMode
|
|
1150
|
+
]);
|
|
1151
|
+
const handleConfirmCheckout = (0, import_react6.useCallback)(async () => {
|
|
1152
|
+
if (confirmProcessing || !session) return;
|
|
1153
|
+
setConfirmProcessing(true);
|
|
1154
|
+
setModeError(null);
|
|
1155
|
+
try {
|
|
1156
|
+
await processPaymentForMode(session);
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
const floPayErr = err instanceof import_shared4.FloPayError ? err : new import_shared4.FloPayError(
|
|
1159
|
+
err instanceof Error ? err.message : "Payment failed",
|
|
1160
|
+
"api_error"
|
|
1161
|
+
);
|
|
1162
|
+
setModeError(floPayErr.message);
|
|
1163
|
+
onError?.(floPayErr);
|
|
1164
|
+
setCurrentMode("full");
|
|
1165
|
+
} finally {
|
|
1166
|
+
setConfirmProcessing(false);
|
|
1167
|
+
}
|
|
1168
|
+
}, [confirmProcessing, session, processPaymentForMode, onError]);
|
|
1169
|
+
const providerOptions = (0, import_react6.useMemo)(() => {
|
|
1170
|
+
if (!unified || !session) return void 0;
|
|
1171
|
+
const opts = {
|
|
1172
|
+
appearance,
|
|
1173
|
+
paymentMethodCreation: "manual",
|
|
1174
|
+
billingApiUrl: resolvedBillingUrl
|
|
1175
|
+
};
|
|
1176
|
+
if (unified.provider === "stripe" && unified.data.stripe?.clientSecret) {
|
|
1177
|
+
opts.clientSecret = unified.data.stripe.clientSecret;
|
|
1178
|
+
} else {
|
|
1179
|
+
const displayTotal = (0, import_shared4.buildCheckoutDisplayData)(session).total;
|
|
1180
|
+
opts.amount = Math.round(displayTotal * 100) || session.amount;
|
|
1181
|
+
opts.currency = session.currency?.toLowerCase();
|
|
1182
|
+
}
|
|
1183
|
+
return opts;
|
|
1184
|
+
}, [unified, session, appearance, resolvedBillingUrl]);
|
|
1185
|
+
const checkoutValue = (0, import_react6.useMemo)(
|
|
1186
|
+
() => ({
|
|
1187
|
+
session,
|
|
1188
|
+
loading: isLoading,
|
|
1189
|
+
error: loadError,
|
|
1190
|
+
checkoutMode: currentMode
|
|
1191
|
+
}),
|
|
1192
|
+
[session, isLoading, loadError, currentMode]
|
|
1193
|
+
);
|
|
1194
|
+
if (isLoading) {
|
|
1195
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: loadingNode ?? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1196
|
+
"div",
|
|
1197
|
+
{
|
|
1198
|
+
style: {
|
|
1199
|
+
display: "flex",
|
|
1200
|
+
justifyContent: "center",
|
|
1201
|
+
padding: 32
|
|
1202
|
+
},
|
|
1203
|
+
children: [
|
|
1204
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1205
|
+
"div",
|
|
1206
|
+
{
|
|
1207
|
+
style: {
|
|
1208
|
+
width: 24,
|
|
1209
|
+
height: 24,
|
|
1210
|
+
border: "2px solid #e5e7eb",
|
|
1211
|
+
borderTopColor: "#6b7280",
|
|
1212
|
+
borderRadius: "50%",
|
|
1213
|
+
animation: "spin 0.6s linear infinite"
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
),
|
|
1217
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` })
|
|
1218
|
+
]
|
|
1219
|
+
}
|
|
1220
|
+
) });
|
|
1221
|
+
}
|
|
1222
|
+
if (loadError) {
|
|
1223
|
+
if (errorNode) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: errorNode(loadError) });
|
|
1224
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1225
|
+
"div",
|
|
1226
|
+
{
|
|
1227
|
+
style: {
|
|
1228
|
+
padding: 24,
|
|
1229
|
+
textAlign: "center",
|
|
1230
|
+
color: "#dc2626",
|
|
1231
|
+
fontSize: 14
|
|
1232
|
+
},
|
|
1233
|
+
children: loadError.message
|
|
1234
|
+
}
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
if (!flopay || !providerOptions) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, {});
|
|
1238
|
+
if (currentMode === "confirm") {
|
|
1239
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CheckoutContext.Provider, { value: checkoutValue, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(FloPayProvider, { flopay, options: providerOptions, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className, children: [
|
|
1240
|
+
modeError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1241
|
+
"div",
|
|
1242
|
+
{
|
|
1243
|
+
style: {
|
|
1244
|
+
color: "#dc2626",
|
|
1245
|
+
fontSize: "0.875rem",
|
|
1246
|
+
marginBottom: "0.75rem",
|
|
1247
|
+
textAlign: "center"
|
|
1248
|
+
},
|
|
1249
|
+
children: modeError
|
|
1250
|
+
}
|
|
1251
|
+
),
|
|
1252
|
+
renderConfirmButton ? renderConfirmButton({
|
|
1253
|
+
onConfirm: handleConfirmCheckout,
|
|
1254
|
+
isProcessing: confirmProcessing
|
|
1255
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1256
|
+
"button",
|
|
1257
|
+
{
|
|
1258
|
+
type: "button",
|
|
1259
|
+
onClick: handleConfirmCheckout,
|
|
1260
|
+
disabled: confirmProcessing,
|
|
1261
|
+
style: {
|
|
1262
|
+
width: "100%",
|
|
1263
|
+
padding: "0.875rem",
|
|
1264
|
+
backgroundColor: "#4A49FF",
|
|
1265
|
+
color: "white",
|
|
1266
|
+
border: "none",
|
|
1267
|
+
borderRadius: "8px",
|
|
1268
|
+
fontSize: "1rem",
|
|
1269
|
+
fontWeight: 600,
|
|
1270
|
+
cursor: confirmProcessing ? "not-allowed" : "pointer",
|
|
1271
|
+
opacity: confirmProcessing ? 0.6 : 1
|
|
1272
|
+
},
|
|
1273
|
+
children: confirmProcessing ? "Processing..." : confirmLabel ?? "Confirm Purchase"
|
|
1274
|
+
}
|
|
1275
|
+
)
|
|
1276
|
+
] }) }) });
|
|
1277
|
+
}
|
|
1278
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CheckoutContext.Provider, { value: checkoutValue, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(FloPayProvider, { flopay, options: providerOptions, children: children ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1279
|
+
SessionInjector,
|
|
1280
|
+
{
|
|
1281
|
+
sessionId,
|
|
1282
|
+
billingApiUrl: resolvedBillingUrl,
|
|
1283
|
+
session,
|
|
1284
|
+
children
|
|
1285
|
+
}
|
|
1286
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1287
|
+
SplitCardForm,
|
|
1288
|
+
{
|
|
1289
|
+
sessionId,
|
|
1290
|
+
email: session?.customer?.email,
|
|
1291
|
+
userId: session?.customer?.id,
|
|
1292
|
+
firstName: session?.customer?.firstName,
|
|
1293
|
+
lastName: session?.customer?.lastName,
|
|
1294
|
+
totalAmount: session ? Math.round((0, import_shared4.buildCheckoutDisplayData)(session).total * 100) : 0,
|
|
1295
|
+
currency: session?.currency?.toLowerCase() ?? "usd",
|
|
1296
|
+
onComplete,
|
|
1297
|
+
onError,
|
|
1298
|
+
showPayPal,
|
|
1299
|
+
showApplePay,
|
|
1300
|
+
showGooglePay,
|
|
1301
|
+
submitLabel,
|
|
1302
|
+
className
|
|
1303
|
+
}
|
|
1304
|
+
) }) });
|
|
1305
|
+
}
|
|
1306
|
+
function SessionInjector({
|
|
1307
|
+
sessionId,
|
|
1308
|
+
billingApiUrl,
|
|
1309
|
+
session,
|
|
1310
|
+
children
|
|
1311
|
+
}) {
|
|
1312
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: import_react6.default.Children.map(children, (child) => {
|
|
1313
|
+
if (!import_react6.default.isValidElement(child)) return child;
|
|
1314
|
+
const existing = child.props;
|
|
1315
|
+
const injected = {};
|
|
1316
|
+
if (!existing.sessionId) injected.sessionId = sessionId;
|
|
1317
|
+
if (!existing.billingApiUrl) injected.billingApiUrl = billingApiUrl;
|
|
1318
|
+
if (session?.customer) {
|
|
1319
|
+
if (!existing.email) injected.email = session.customer.email;
|
|
1320
|
+
if (!existing.userId) injected.userId = session.customer.id;
|
|
1321
|
+
if (!existing.firstName)
|
|
1322
|
+
injected.firstName = session.customer.firstName;
|
|
1323
|
+
if (!existing.lastName)
|
|
1324
|
+
injected.lastName = session.customer.lastName;
|
|
1325
|
+
}
|
|
1326
|
+
if (Object.keys(injected).length === 0) return child;
|
|
1327
|
+
return import_react6.default.cloneElement(child, injected);
|
|
1328
|
+
}) });
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/checkout-form.tsx
|
|
1332
|
+
var import_shared5 = require("@flopay/shared");
|
|
1333
|
+
var import_react7 = require("react");
|
|
1334
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1335
|
+
var WALLET_RESUME_KEY2 = "flopay_wallet_resume";
|
|
1336
|
+
var CheckoutForm = (0, import_react7.forwardRef)(
|
|
1337
|
+
function CheckoutForm2(props, ref) {
|
|
1338
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CheckoutFormInner, { ...props, innerRef: ref });
|
|
1339
|
+
}
|
|
1340
|
+
);
|
|
1341
|
+
function CheckoutFormInner({
|
|
1342
|
+
sessionId,
|
|
1343
|
+
billingApiUrl,
|
|
1344
|
+
email,
|
|
1345
|
+
userId,
|
|
1346
|
+
onComplete,
|
|
1347
|
+
onError,
|
|
1348
|
+
onTokenizedBody,
|
|
1349
|
+
layout = "auto",
|
|
1350
|
+
submitLabel = "Pay",
|
|
1351
|
+
showAddress = false,
|
|
1352
|
+
className,
|
|
1353
|
+
children,
|
|
1354
|
+
firstName,
|
|
1355
|
+
lastName,
|
|
1356
|
+
chv,
|
|
1357
|
+
isProcessing: externalProcessing,
|
|
1358
|
+
error: externalError,
|
|
1359
|
+
onErrorChange,
|
|
1360
|
+
innerRef
|
|
1361
|
+
}) {
|
|
1362
|
+
const flopay = useFloPay();
|
|
1363
|
+
const elements = useElements();
|
|
1364
|
+
const contextBillingUrl = useBillingApiUrl();
|
|
1365
|
+
const [processing, setProcessing] = (0, import_react7.useState)(false);
|
|
1366
|
+
const [error, setError] = (0, import_react7.useState)(null);
|
|
1367
|
+
const [is3DSActive, setIs3DSActive] = (0, import_react7.useState)(false);
|
|
1368
|
+
const displayError = externalError ?? error;
|
|
1369
|
+
const isSubmitting = externalProcessing ?? processing;
|
|
1370
|
+
const isSelfContained = !onTokenizedBody;
|
|
1371
|
+
const baseUrl = (billingApiUrl || contextBillingUrl).replace(/\/+$/, "");
|
|
1372
|
+
const updateError = (0, import_react7.useCallback)(
|
|
1373
|
+
(err) => {
|
|
1374
|
+
setError(err);
|
|
1375
|
+
onErrorChange?.(err);
|
|
1376
|
+
},
|
|
1377
|
+
[onErrorChange]
|
|
1378
|
+
);
|
|
1379
|
+
const processPaymentInternal = (0, import_react7.useCallback)(
|
|
1380
|
+
async (tokenizedBody) => {
|
|
1381
|
+
setProcessing(true);
|
|
1382
|
+
updateError(null);
|
|
1383
|
+
try {
|
|
1384
|
+
const response = await fetch(`${baseUrl}/v1/checkouts/sessions/process`, {
|
|
1385
|
+
method: "POST",
|
|
1386
|
+
headers: {
|
|
1387
|
+
"Content-Type": "application/json",
|
|
1388
|
+
"x-user-id": userId ?? ""
|
|
1389
|
+
},
|
|
1390
|
+
body: JSON.stringify({
|
|
1391
|
+
sessionId,
|
|
1392
|
+
tokenizedData: tokenizedBody,
|
|
1393
|
+
accountData: {
|
|
1394
|
+
userId: userId ?? "",
|
|
1395
|
+
email: email ?? "",
|
|
1396
|
+
firstName: firstName ?? "",
|
|
1397
|
+
lastName: lastName ?? ""
|
|
1398
|
+
},
|
|
1399
|
+
chv
|
|
1400
|
+
})
|
|
1401
|
+
});
|
|
1402
|
+
if (response.ok) {
|
|
1403
|
+
onComplete?.({
|
|
1404
|
+
status: "succeeded",
|
|
1405
|
+
paymentIntentId: tokenizedBody.threeDSecureActionResultTokenId
|
|
1406
|
+
});
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const json = await response.json().catch(() => null);
|
|
1410
|
+
if (json?.type === "3ds_required") {
|
|
1411
|
+
const secret = json["threeDSecureToken"];
|
|
1412
|
+
if (!flopay || !secret) {
|
|
1413
|
+
updateError("3DS authentication required but no token provided.");
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
setIs3DSActive(true);
|
|
1417
|
+
try {
|
|
1418
|
+
const result = await flopay.confirmPayment({
|
|
1419
|
+
clientSecret: secret,
|
|
1420
|
+
returnUrl: window.location.href
|
|
1421
|
+
});
|
|
1422
|
+
if (result.error) {
|
|
1423
|
+
updateError(result.error.message);
|
|
1424
|
+
onError?.(result.error);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (result.status === "succeeded" || result.status === "processing") {
|
|
1428
|
+
await processPaymentInternal({
|
|
1429
|
+
id: result.paymentIntentId,
|
|
1430
|
+
type: "card",
|
|
1431
|
+
threeDSecureActionResultTokenId: result.paymentIntentId
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
} finally {
|
|
1435
|
+
setIs3DSActive(false);
|
|
1436
|
+
}
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (json?.type === "paypal_redirect_required") {
|
|
1440
|
+
const secret = json["clientSecret"];
|
|
1441
|
+
const pmId = json["paymentMethodId"];
|
|
1442
|
+
if (flopay && secret) {
|
|
1443
|
+
localStorage.setItem(WALLET_RESUME_KEY2, JSON.stringify({
|
|
1444
|
+
sessionId,
|
|
1445
|
+
paymentIntentId: "",
|
|
1446
|
+
tokenType: "card",
|
|
1447
|
+
tokenId: pmId,
|
|
1448
|
+
status: "pending_redirect"
|
|
1449
|
+
}));
|
|
1450
|
+
await flopay.confirmPayment({
|
|
1451
|
+
clientSecret: secret,
|
|
1452
|
+
returnUrl: window.location.href
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const errorMessage = json?.message ?? "Payment failed. Please try again.";
|
|
1458
|
+
updateError(errorMessage);
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
updateError(err instanceof Error ? err.message : "An unexpected error occurred");
|
|
1461
|
+
} finally {
|
|
1462
|
+
setProcessing(false);
|
|
1463
|
+
}
|
|
1464
|
+
},
|
|
1465
|
+
[baseUrl, sessionId, userId, email, firstName, lastName, chv, flopay, onComplete, onError, updateError]
|
|
1466
|
+
);
|
|
1467
|
+
const dispatchTokenizedBody = (0, import_react7.useCallback)(
|
|
1468
|
+
(tokenizedBody) => {
|
|
1469
|
+
if (onTokenizedBody) {
|
|
1470
|
+
onTokenizedBody(tokenizedBody);
|
|
1471
|
+
} else {
|
|
1472
|
+
processPaymentInternal(tokenizedBody);
|
|
1473
|
+
}
|
|
1474
|
+
},
|
|
1475
|
+
[onTokenizedBody, processPaymentInternal]
|
|
1476
|
+
);
|
|
1477
|
+
(0, import_react7.useImperativeHandle)(innerRef, () => ({
|
|
1478
|
+
async handleNextAction(secret) {
|
|
1479
|
+
if (!flopay) return;
|
|
1480
|
+
setIs3DSActive(true);
|
|
1481
|
+
try {
|
|
1482
|
+
const result = await flopay.confirmPayment({
|
|
1483
|
+
clientSecret: secret,
|
|
1484
|
+
returnUrl: window.location.href
|
|
1485
|
+
});
|
|
1486
|
+
if (result.error) {
|
|
1487
|
+
updateError(result.error.message);
|
|
1488
|
+
onError?.(result.error);
|
|
1489
|
+
} else if (result.status === "succeeded" || result.status === "processing") {
|
|
1490
|
+
dispatchTokenizedBody({
|
|
1491
|
+
id: result.paymentIntentId,
|
|
1492
|
+
type: "card",
|
|
1493
|
+
threeDSecureActionResultTokenId: result.paymentIntentId
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
updateError(err instanceof Error ? err.message : "Payment authentication failed.");
|
|
1498
|
+
} finally {
|
|
1499
|
+
setIs3DSActive(false);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}), [flopay, dispatchTokenizedBody, onError, updateError]);
|
|
1503
|
+
(0, import_react7.useEffect)(() => {
|
|
1504
|
+
if (typeof window === "undefined") return;
|
|
1505
|
+
const stored = localStorage.getItem(WALLET_RESUME_KEY2);
|
|
1506
|
+
if (!stored) return;
|
|
1507
|
+
try {
|
|
1508
|
+
const payload = JSON.parse(stored);
|
|
1509
|
+
if (payload.sessionId === sessionId) {
|
|
1510
|
+
localStorage.removeItem(WALLET_RESUME_KEY2);
|
|
1511
|
+
dispatchTokenizedBody({
|
|
1512
|
+
id: payload.tokenId,
|
|
1513
|
+
type: payload.tokenType,
|
|
1514
|
+
threeDSecureActionResultTokenId: payload.paymentIntentId
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
} catch {
|
|
1518
|
+
localStorage.removeItem(WALLET_RESUME_KEY2);
|
|
1519
|
+
}
|
|
1520
|
+
}, [sessionId, dispatchTokenizedBody]);
|
|
1521
|
+
const handleSubmit = (0, import_react7.useCallback)(
|
|
1522
|
+
async (e) => {
|
|
1523
|
+
e.preventDefault();
|
|
1524
|
+
if (!flopay || !elements || isSubmitting) return;
|
|
1525
|
+
setProcessing(true);
|
|
1526
|
+
updateError(null);
|
|
1527
|
+
try {
|
|
1528
|
+
const submitResult = await flopay.submitElements();
|
|
1529
|
+
if (submitResult.error) {
|
|
1530
|
+
updateError(submitResult.error.message);
|
|
1531
|
+
onError?.(submitResult.error);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const pmResult = await flopay.createPaymentMethod();
|
|
1535
|
+
if (pmResult.error || !pmResult.paymentMethodId) {
|
|
1536
|
+
updateError(pmResult.error?.message ?? "Failed to create payment method.");
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (!sessionId || !email) {
|
|
1540
|
+
throw new import_shared5.FloPayError("Missing sessionId or email", "validation_error");
|
|
1541
|
+
}
|
|
1542
|
+
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
1543
|
+
method: "POST",
|
|
1544
|
+
headers: { "Content-Type": "application/json" },
|
|
1545
|
+
body: JSON.stringify({
|
|
1546
|
+
sessionId,
|
|
1547
|
+
email,
|
|
1548
|
+
paymentMethodType: pmResult.paymentMethodId,
|
|
1549
|
+
isPaypal: false
|
|
1550
|
+
})
|
|
1551
|
+
});
|
|
1552
|
+
if (!intentResponse.ok) throw new import_shared5.FloPayError("Failed to create payment intent", "api_error");
|
|
1553
|
+
const intentJson = await intentResponse.json();
|
|
1554
|
+
const intentClientSecret = intentJson.data?.id;
|
|
1555
|
+
if (!intentClientSecret) throw new import_shared5.FloPayError("No client_secret in payment intent response", "api_error");
|
|
1556
|
+
const confirmResult = await flopay.confirmCardPayment({
|
|
1557
|
+
clientSecret: intentClientSecret,
|
|
1558
|
+
paymentMethodId: pmResult.paymentMethodId
|
|
1559
|
+
});
|
|
1560
|
+
if (confirmResult.error) {
|
|
1561
|
+
updateError(confirmResult.error.message);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
dispatchTokenizedBody({
|
|
1565
|
+
id: pmResult.paymentMethodId,
|
|
1566
|
+
type: "card",
|
|
1567
|
+
threeDSecureActionResultTokenId: confirmResult.paymentIntentId
|
|
1568
|
+
});
|
|
1569
|
+
} catch (err) {
|
|
1570
|
+
updateError(err instanceof Error ? err.message : "An unexpected error occurred");
|
|
1571
|
+
} finally {
|
|
1572
|
+
if (isSelfContained) {
|
|
1573
|
+
} else {
|
|
1574
|
+
setProcessing(false);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1578
|
+
[flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError]
|
|
1579
|
+
);
|
|
1580
|
+
const isReady = flopay !== null && elements !== null;
|
|
1581
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("form", { onSubmit: handleSubmit, className, style: { position: "relative" }, children: [
|
|
1582
|
+
(is3DSActive || isSubmitting) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { "data-testid": "flopay-overlay", style: {
|
|
1583
|
+
position: "absolute",
|
|
1584
|
+
inset: 0,
|
|
1585
|
+
background: "rgba(255,255,255,0.7)",
|
|
1586
|
+
display: "flex",
|
|
1587
|
+
alignItems: "center",
|
|
1588
|
+
justifyContent: "center",
|
|
1589
|
+
zIndex: 10
|
|
1590
|
+
}, children: is3DSActive ? "Verifying payment..." : "Processing..." }),
|
|
1591
|
+
!isReady && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { "data-testid": "flopay-loading", "aria-busy": "true", children: "Loading payment form..." }),
|
|
1592
|
+
isReady && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
1593
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PaymentElement, { options: { layout } }),
|
|
1594
|
+
showAddress && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(AddressElement, { options: { mode: showAddress === true ? "billing" : showAddress } }),
|
|
1595
|
+
displayError && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { role: "alert", "data-testid": "flopay-error", style: { color: "red", margin: "0.75rem 0" }, children: displayError }),
|
|
1596
|
+
children ?? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1597
|
+
"button",
|
|
1598
|
+
{
|
|
1599
|
+
type: "submit",
|
|
1600
|
+
disabled: isSubmitting || !isReady,
|
|
1601
|
+
"data-testid": "flopay-submit",
|
|
1602
|
+
children: isSubmitting ? "Processing..." : submitLabel
|
|
1603
|
+
}
|
|
1604
|
+
)
|
|
1605
|
+
] })
|
|
1606
|
+
] });
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// src/paypal-button.tsx
|
|
1610
|
+
var import_shared6 = require("@flopay/shared");
|
|
1611
|
+
var import_react8 = require("react");
|
|
1612
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1613
|
+
function PayPalButton({
|
|
1614
|
+
sessionId,
|
|
1615
|
+
billingApiUrl,
|
|
1616
|
+
email,
|
|
1617
|
+
userId,
|
|
1618
|
+
firstName,
|
|
1619
|
+
lastName,
|
|
1620
|
+
chv,
|
|
1621
|
+
onTokenizedBody,
|
|
1622
|
+
onComplete,
|
|
1623
|
+
onErrorChange,
|
|
1624
|
+
isProcessing = false
|
|
1625
|
+
}) {
|
|
1626
|
+
const flopay = useFloPay();
|
|
1627
|
+
const elements = useElements();
|
|
1628
|
+
const contextBillingUrl = useBillingApiUrl();
|
|
1629
|
+
const [ready, setReady] = (0, import_react8.useState)(false);
|
|
1630
|
+
const [submitting, setSubmitting] = (0, import_react8.useState)(false);
|
|
1631
|
+
const paypalResumeAttempted = (0, import_react8.useRef)(false);
|
|
1632
|
+
const baseUrl = (billingApiUrl || contextBillingUrl).replace(/\/+$/, "");
|
|
1633
|
+
const processPaymentInternal = (0, import_react8.useCallback)(
|
|
1634
|
+
async (tokenizedBody) => {
|
|
1635
|
+
try {
|
|
1636
|
+
const response = await fetch(`${baseUrl}/v1/checkouts/sessions/process`, {
|
|
1637
|
+
method: "POST",
|
|
1638
|
+
headers: {
|
|
1639
|
+
"Content-Type": "application/json",
|
|
1640
|
+
"x-user-id": userId ?? ""
|
|
1641
|
+
},
|
|
1642
|
+
body: JSON.stringify({
|
|
1643
|
+
sessionId,
|
|
1644
|
+
tokenizedData: tokenizedBody,
|
|
1645
|
+
accountData: {
|
|
1646
|
+
userId: userId ?? "",
|
|
1647
|
+
email: email ?? "",
|
|
1648
|
+
firstName: firstName ?? "",
|
|
1649
|
+
lastName: lastName ?? ""
|
|
1650
|
+
},
|
|
1651
|
+
chv
|
|
1652
|
+
})
|
|
1653
|
+
});
|
|
1654
|
+
if (response.ok) {
|
|
1655
|
+
onComplete?.();
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
const json = await response.json().catch(() => null);
|
|
1659
|
+
onErrorChange?.(json?.message ?? "Payment failed. Please try again.");
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
onErrorChange?.(err instanceof Error ? err.message : "An unexpected error occurred");
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
[baseUrl, sessionId, userId, email, firstName, lastName, chv, onComplete, onErrorChange]
|
|
1665
|
+
);
|
|
1666
|
+
const dispatchTokenizedBody = (0, import_react8.useCallback)(
|
|
1667
|
+
(body) => {
|
|
1668
|
+
if (onTokenizedBody) {
|
|
1669
|
+
onTokenizedBody(body);
|
|
1670
|
+
} else {
|
|
1671
|
+
processPaymentInternal(body);
|
|
1672
|
+
}
|
|
1673
|
+
},
|
|
1674
|
+
[onTokenizedBody, processPaymentInternal]
|
|
1675
|
+
);
|
|
1676
|
+
(0, import_react8.useEffect)(() => {
|
|
1677
|
+
if (!flopay || paypalResumeAttempted.current) return;
|
|
1678
|
+
const params = new URLSearchParams(window.location.search);
|
|
1679
|
+
const paymentIntentId = params.get("payment_intent");
|
|
1680
|
+
const clientSecret = params.get("payment_intent_client_secret");
|
|
1681
|
+
const redirectStatus = params.get("redirect_status");
|
|
1682
|
+
if (!paymentIntentId || !clientSecret) return;
|
|
1683
|
+
paypalResumeAttempted.current = true;
|
|
1684
|
+
(async () => {
|
|
1685
|
+
try {
|
|
1686
|
+
setSubmitting(true);
|
|
1687
|
+
if (redirectStatus === "failed") {
|
|
1688
|
+
onErrorChange?.("PayPal payment was declined. Please try again.");
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
const provider = flopay.getRawProvider();
|
|
1692
|
+
if (!provider?.retrievePaymentIntent) {
|
|
1693
|
+
onErrorChange?.("Cannot retrieve PayPal payment status.");
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
const { paymentIntent, error: retrieveError } = await provider.retrievePaymentIntent(clientSecret);
|
|
1697
|
+
if (retrieveError) {
|
|
1698
|
+
onErrorChange?.(retrieveError.message ?? "Failed to retrieve PayPal payment status.");
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
if (paymentIntent && (paymentIntent.status === "requires_capture" || paymentIntent.status === "succeeded")) {
|
|
1702
|
+
const pmId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
|
|
1703
|
+
dispatchTokenizedBody({
|
|
1704
|
+
id: pmId ?? paymentIntent.id,
|
|
1705
|
+
type: "card",
|
|
1706
|
+
threeDSecureActionResultTokenId: paymentIntent.id,
|
|
1707
|
+
isPaypal: true
|
|
1708
|
+
});
|
|
1709
|
+
const url = new URL(window.location.href);
|
|
1710
|
+
url.searchParams.delete("payment_intent");
|
|
1711
|
+
url.searchParams.delete("payment_intent_client_secret");
|
|
1712
|
+
url.searchParams.delete("redirect_status");
|
|
1713
|
+
window.history.replaceState({}, "", url.toString());
|
|
1714
|
+
} else {
|
|
1715
|
+
onErrorChange?.("PayPal payment was not completed. Please try again.");
|
|
1716
|
+
}
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
onErrorChange?.(err instanceof Error ? err.message : "Failed to complete PayPal payment.");
|
|
1719
|
+
} finally {
|
|
1720
|
+
setSubmitting(false);
|
|
1721
|
+
}
|
|
1722
|
+
})();
|
|
1723
|
+
}, [flopay, dispatchTokenizedBody, onErrorChange]);
|
|
1724
|
+
const handlePayPalConfirm = (0, import_react8.useCallback)(async () => {
|
|
1725
|
+
if (!flopay || !elements) return;
|
|
1726
|
+
try {
|
|
1727
|
+
setSubmitting(true);
|
|
1728
|
+
onErrorChange?.(null);
|
|
1729
|
+
if (!sessionId || !email) {
|
|
1730
|
+
throw new import_shared6.FloPayError("Missing sessionId or email for PayPal payment", "validation_error");
|
|
1731
|
+
}
|
|
1732
|
+
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
1733
|
+
method: "POST",
|
|
1734
|
+
headers: { "Content-Type": "application/json" },
|
|
1735
|
+
body: JSON.stringify({
|
|
1736
|
+
sessionId,
|
|
1737
|
+
email,
|
|
1738
|
+
paymentMethodType: "paypal",
|
|
1739
|
+
isPaypal: "true"
|
|
1740
|
+
})
|
|
1741
|
+
});
|
|
1742
|
+
if (!intentResponse.ok) throw new import_shared6.FloPayError("Failed to create payment intent", "api_error");
|
|
1743
|
+
const intentJson = await intentResponse.json();
|
|
1744
|
+
const intentClientSecret = intentJson.data?.id;
|
|
1745
|
+
if (!intentClientSecret) throw new import_shared6.FloPayError("No client_secret in response", "api_error");
|
|
1746
|
+
const result = await flopay.confirmPayment({
|
|
1747
|
+
clientSecret: intentClientSecret,
|
|
1748
|
+
returnUrl: window.location.href
|
|
1749
|
+
});
|
|
1750
|
+
if (result.status === "succeeded" || result.status === "processing") {
|
|
1751
|
+
dispatchTokenizedBody({
|
|
1752
|
+
id: result.paymentIntentId,
|
|
1753
|
+
type: "card",
|
|
1754
|
+
threeDSecureActionResultTokenId: result.paymentIntentId,
|
|
1755
|
+
isPaypal: true
|
|
1756
|
+
});
|
|
1757
|
+
} else if (result.error) {
|
|
1758
|
+
onErrorChange?.(result.error.message);
|
|
1759
|
+
}
|
|
1760
|
+
} catch (err) {
|
|
1761
|
+
onErrorChange?.(err instanceof Error ? err.message : "PayPal payment failed. Please try again.");
|
|
1762
|
+
} finally {
|
|
1763
|
+
setSubmitting(false);
|
|
1764
|
+
}
|
|
1765
|
+
}, [flopay, elements, sessionId, email, baseUrl, dispatchTokenizedBody, onErrorChange]);
|
|
1766
|
+
if (!flopay || !elements) {
|
|
1767
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { height: 45, background: "#f0f0f0", borderRadius: 6, animation: "pulse 1.5s infinite" } });
|
|
1768
|
+
}
|
|
1769
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
1770
|
+
!ready && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { height: 45, background: "#f0f0f0", borderRadius: 6 } }),
|
|
1771
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: ready ? {} : { display: "none" }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1772
|
+
"button",
|
|
1773
|
+
{
|
|
1774
|
+
type: "button",
|
|
1775
|
+
onClick: handlePayPalConfirm,
|
|
1776
|
+
disabled: submitting || isProcessing,
|
|
1777
|
+
style: {
|
|
1778
|
+
width: "100%",
|
|
1779
|
+
height: 45,
|
|
1780
|
+
backgroundColor: "#ffc439",
|
|
1781
|
+
color: "#003087",
|
|
1782
|
+
border: "none",
|
|
1783
|
+
borderRadius: 6,
|
|
1784
|
+
fontSize: "1rem",
|
|
1785
|
+
fontWeight: 700,
|
|
1786
|
+
cursor: submitting || isProcessing ? "not-allowed" : "pointer",
|
|
1787
|
+
opacity: submitting || isProcessing ? 0.6 : 1
|
|
1788
|
+
},
|
|
1789
|
+
ref: () => setReady(true),
|
|
1790
|
+
children: submitting ? "Processing..." : "PayPal"
|
|
1791
|
+
}
|
|
1792
|
+
) }),
|
|
1793
|
+
(submitting || isProcessing) && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
|
|
1794
|
+
position: "fixed",
|
|
1795
|
+
inset: 0,
|
|
1796
|
+
background: "rgba(0,0,0,0.4)",
|
|
1797
|
+
display: "flex",
|
|
1798
|
+
alignItems: "center",
|
|
1799
|
+
justifyContent: "center",
|
|
1800
|
+
zIndex: 1e3
|
|
1801
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
|
|
1802
|
+
background: "white",
|
|
1803
|
+
borderRadius: 8,
|
|
1804
|
+
padding: "1.5rem",
|
|
1805
|
+
textAlign: "center",
|
|
1806
|
+
boxShadow: "0 4px 24px rgba(0,0,0,0.15)",
|
|
1807
|
+
width: 280
|
|
1808
|
+
}, children: "Processing PayPal payment..." }) })
|
|
1809
|
+
] });
|
|
1810
|
+
}
|
|
1811
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1812
|
+
0 && (module.exports = {
|
|
1813
|
+
AddressElement,
|
|
1814
|
+
CardCvcElement,
|
|
1815
|
+
CardElement,
|
|
1816
|
+
CardExpiryElement,
|
|
1817
|
+
CardNumberElement,
|
|
1818
|
+
CheckoutForm,
|
|
1819
|
+
FloPayCheckout,
|
|
1820
|
+
FloPayProvider,
|
|
1821
|
+
PayPalButton,
|
|
1822
|
+
PaymentElement,
|
|
1823
|
+
SplitCardForm,
|
|
1824
|
+
useCheckout,
|
|
1825
|
+
useElements,
|
|
1826
|
+
useFloPay
|
|
1827
|
+
});
|
|
1828
|
+
//# sourceMappingURL=index.cjs.map
|