@flopay/vue 0.0.0-stage → 1.13.1

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.
@@ -0,0 +1,1809 @@
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 __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/context.ts
34
+ var FLOPAY_CONTEXT;
35
+ var init_context = __esm({
36
+ "src/context.ts"() {
37
+ "use strict";
38
+ FLOPAY_CONTEXT = /* @__PURE__ */ Symbol("flopay.vue.provider");
39
+ }
40
+ });
41
+
42
+ // src/provider.ts
43
+ function telemetryBridge(flopay) {
44
+ return flopay[FLOPAY_TELEMETRY_BRIDGE];
45
+ }
46
+ function isPromiseLike(value) {
47
+ return typeof value?.then === "function";
48
+ }
49
+ function createInstrumentEmitter(notify) {
50
+ const emittedOnce = /* @__PURE__ */ new Set();
51
+ return (source) => {
52
+ const event = (0, import_shared3.projectFloInstrument)(source);
53
+ if (!event) return;
54
+ if (import_shared3.FLO_INSTRUMENT_ONCE_NAMES.has(event.name)) {
55
+ if (emittedOnce.has(event.name)) return;
56
+ emittedOnce.add(event.name);
57
+ }
58
+ (0, import_shared3.invokeMerchantCallback)(() => notify(event));
59
+ };
60
+ }
61
+ var import_shared3, import_vue4, FLOPAY_TELEMETRY_BRIDGE, FloPayProvider;
62
+ var init_provider = __esm({
63
+ "src/provider.ts"() {
64
+ "use strict";
65
+ import_shared3 = require("@flopay/shared");
66
+ import_vue4 = require("vue");
67
+ init_context();
68
+ FLOPAY_TELEMETRY_BRIDGE = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.bridge.v1");
69
+ FloPayProvider = (0, import_vue4.defineComponent)({
70
+ name: "FloPayProvider",
71
+ props: {
72
+ flopay: {
73
+ type: [Object, Promise],
74
+ default: null
75
+ },
76
+ paypalFlopay: {
77
+ type: [Object, Promise],
78
+ default: null
79
+ },
80
+ options: { type: Object, default: void 0 }
81
+ },
82
+ emits: { instrument: (_event) => true },
83
+ setup(props, { emit, slots }) {
84
+ const initial = (source) => isPromiseLike(source) ? null : source ?? null;
85
+ const flopay = (0, import_vue4.shallowRef)(initial(props.flopay));
86
+ const paypalFlopay = (0, import_vue4.shallowRef)(initial(props.paypalFlopay));
87
+ const component = (0, import_vue4.getCurrentInstance)();
88
+ const emitInstrument = createInstrumentEmitter((event) => {
89
+ const listener = component?.vnode.props?.onInstrument;
90
+ if (!listener) return emit("instrument", event);
91
+ for (const handler of [listener].flat()) handler(event);
92
+ });
93
+ (0, import_vue4.provide)(FLOPAY_CONTEXT, {
94
+ flopay,
95
+ paypalFlopay,
96
+ billingApiUrl: () => (0, import_shared3.resolveBillingApiUrl)(props.options?.billingApiUrl),
97
+ emitInstrument
98
+ });
99
+ const follow = (read, target, provider) => (0, import_vue4.watch)(
100
+ read,
101
+ (source, _previous, onCleanup) => {
102
+ let active = true;
103
+ onCleanup(() => {
104
+ active = false;
105
+ });
106
+ if (!isPromiseLike(source)) {
107
+ target.value = source ?? null;
108
+ return;
109
+ }
110
+ if (provider === "paypal") target.value = null;
111
+ source.then(
112
+ (instance) => {
113
+ if (active) target.value = instance;
114
+ },
115
+ () => {
116
+ if (!active) return;
117
+ emitInstrument({
118
+ class: "technical_error",
119
+ errorCode: "SDK_INITIALIZATION_FAILED",
120
+ ...provider ? { provider } : {}
121
+ });
122
+ }
123
+ );
124
+ },
125
+ { immediate: true }
126
+ );
127
+ (0, import_vue4.onMounted)(() => {
128
+ emitInstrument({ class: "lifecycle", name: "checkout.mount" });
129
+ follow(() => props.flopay, flopay);
130
+ follow(() => props.paypalFlopay, paypalFlopay, "paypal");
131
+ (0, import_vue4.watch)(
132
+ flopay,
133
+ (instance, _previous, onCleanup) => {
134
+ if (!instance) return;
135
+ emitInstrument({ class: "lifecycle", name: "sdk.initialize.ready" });
136
+ const unsubscribe = telemetryBridge(instance)?.subscribe?.(emitInstrument);
137
+ onCleanup(() => unsubscribe?.());
138
+ void (0, import_vue4.nextTick)(() => emitInstrument({ class: "lifecycle", name: "checkout.rendered" }));
139
+ },
140
+ { immediate: true }
141
+ );
142
+ (0, import_vue4.watch)(paypalFlopay, (instance, _previous, onCleanup) => {
143
+ if (!instance || instance === flopay.value) return;
144
+ const unsubscribe = telemetryBridge(instance)?.subscribe?.(emitInstrument);
145
+ onCleanup(() => unsubscribe?.());
146
+ });
147
+ });
148
+ return () => slots.default?.();
149
+ }
150
+ });
151
+ }
152
+ });
153
+
154
+ // src/stripe-paypal-continuation.ts
155
+ function sessionStore() {
156
+ if (typeof window === "undefined") return null;
157
+ try {
158
+ return window.sessionStorage;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+ function createStripePayPalAttempt() {
164
+ const random = globalThis.crypto?.randomUUID?.();
165
+ if (random) return random;
166
+ return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
167
+ }
168
+ function saveStripePayPalContinuation(continuation) {
169
+ const store = sessionStore();
170
+ if (!store) return false;
171
+ try {
172
+ store.setItem(
173
+ STRIPE_PAYPAL_CONTINUATION_KEY,
174
+ JSON.stringify({
175
+ attempt: continuation.attempt,
176
+ sessionId: continuation.sessionId,
177
+ ...continuation.nonce ? { nonce: continuation.nonce } : {}
178
+ })
179
+ );
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+ function clearStripePayPalContinuation() {
186
+ try {
187
+ sessionStore()?.removeItem(STRIPE_PAYPAL_CONTINUATION_KEY);
188
+ } catch {
189
+ }
190
+ }
191
+ function stripePayPalReturnUrl(attempt) {
192
+ const url = new URL((0, import_shared4.stripeRedirectReturnUrl)());
193
+ url.searchParams.delete(STRIPE_PAYPAL_ATTEMPT_PARAM);
194
+ if (attempt) url.searchParams.set(STRIPE_PAYPAL_ATTEMPT_PARAM, attempt);
195
+ return url.toString();
196
+ }
197
+ function readStripePayPalReturnContinuation() {
198
+ if (typeof window === "undefined") return null;
199
+ const [paymentIntentId] = (0, import_shared4.readStripeRedirectParams)();
200
+ if (!paymentIntentId) return null;
201
+ const attempt = new URLSearchParams(window.location.search).get(STRIPE_PAYPAL_ATTEMPT_PARAM);
202
+ if (!attempt) return null;
203
+ try {
204
+ const raw = sessionStore()?.getItem(STRIPE_PAYPAL_CONTINUATION_KEY);
205
+ if (!raw) return null;
206
+ const parsed = JSON.parse(raw);
207
+ if (!parsed || parsed.attempt !== attempt || typeof parsed.sessionId !== "string") return null;
208
+ return {
209
+ attempt,
210
+ sessionId: parsed.sessionId,
211
+ ...typeof parsed.nonce === "string" ? { nonce: parsed.nonce } : {}
212
+ };
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+ function consumeStripePayPalReturn() {
218
+ clearStripePayPalContinuation();
219
+ window.history.replaceState({}, "", stripePayPalReturnUrl());
220
+ }
221
+ var import_shared4, STRIPE_PAYPAL_CONTINUATION_KEY, STRIPE_PAYPAL_ATTEMPT_PARAM;
222
+ var init_stripe_paypal_continuation = __esm({
223
+ "src/stripe-paypal-continuation.ts"() {
224
+ "use strict";
225
+ import_shared4 = require("@flopay/shared");
226
+ STRIPE_PAYPAL_CONTINUATION_KEY = "flopay_vue_paypal_resume";
227
+ STRIPE_PAYPAL_ATTEMPT_PARAM = "flopay_paypal_attempt";
228
+ }
229
+ });
230
+
231
+ // src/stripe-elements.ts
232
+ var stripe_elements_exports = {};
233
+ __export(stripe_elements_exports, {
234
+ StripePaymentMethods: () => StripePaymentMethods
235
+ });
236
+ function checkoutButtonMethodFromProviderMethod(method) {
237
+ if (method === "paypal") return "paypal";
238
+ if (method === "apple_pay") return "apple_pay";
239
+ return "google_pay";
240
+ }
241
+ function walletClockNow() {
242
+ return globalThis.performance?.now() ?? Date.now();
243
+ }
244
+ function stripeMethodCategory(method) {
245
+ return WALLET_METHODS.has(method) ? "wallet" : "apm";
246
+ }
247
+ function declineReason(error) {
248
+ return error.decline_code ?? error.code ?? "provider_declined";
249
+ }
250
+ var import_card_setup2, import_shared5, import_vue5, WALLET_METHODS, EXPRESS_CHECKOUT_CLICK_BUDGET_MS, PAYPAL_REDIRECT_TILE_HEIGHT, stripePayPalResumeAttempts, StripePaymentMethods;
251
+ var init_stripe_elements = __esm({
252
+ "src/stripe-elements.ts"() {
253
+ "use strict";
254
+ import_card_setup2 = require("@flopay/js/card-setup");
255
+ import_shared5 = require("@flopay/shared");
256
+ import_vue5 = require("vue");
257
+ init_context();
258
+ init_provider();
259
+ init_stripe_paypal_continuation();
260
+ WALLET_METHODS = /* @__PURE__ */ new Set(["apple_pay", "google_pay", "link", "paypal", "amazon_pay"]);
261
+ EXPRESS_CHECKOUT_CLICK_BUDGET_MS = 900;
262
+ PAYPAL_REDIRECT_TILE_HEIGHT = 44;
263
+ stripePayPalResumeAttempts = /* @__PURE__ */ new Map();
264
+ StripePaymentMethods = (0, import_vue5.defineComponent)({
265
+ name: "FloPayStripePaymentMethods",
266
+ props: {
267
+ session: { type: Object, default: null },
268
+ sessionId: { type: String, required: true },
269
+ nonce: { type: String, default: void 0 },
270
+ billingApiUrl: { type: String, required: true },
271
+ publishableKey: { type: String, required: true },
272
+ methods: { type: Array, required: true },
273
+ /** `payment` for checkout, `setup` for payment-method setup. */
274
+ intentKind: { type: String, default: "payment" },
275
+ currency: { type: String, default: void 0 },
276
+ amount: { type: Number, default: 0 },
277
+ /**
278
+ * React's `totalAmount`: the cart total due today, in minor units. When it
279
+ * is given on a checkout (`intentKind: 'payment'`), the wallet row follows
280
+ * React's wallet group: its Elements mode, amount and capture method come
281
+ * from the shared `resolveWalletModeOptions` (setup mode for a $0 total),
282
+ * and its intent is `totalAmount > 0 ? 'payment' : 'setup'`.
283
+ */
284
+ totalAmount: { type: Number, default: void 0 },
285
+ returnUrl: { type: String, default: void 0 },
286
+ /** The theme's appearance, normalized for Stripe Elements (React's setup branch does the same). */
287
+ appearance: { type: Object, default: void 0 },
288
+ /**
289
+ * React's `stripePayPalPresentation`, which the form resolves from its
290
+ * `walletPresentation` prop through the shared
291
+ * `resolveStripePayPalPresentation`. With `redirect`, Stripe-routed PayPal
292
+ * leaves the wallet row and renders as React's full-page "Pay with PayPal"
293
+ * redirect tile, which resumes the payment when the buyer returns.
294
+ */
295
+ paypalPresentation: { type: String, default: "popup" },
296
+ /**
297
+ * The form's click gate (React's `runBeforeButtonClick`), run by the
298
+ * redirect tile and by the wallet row's Express Checkout click.
299
+ */
300
+ runBeforeButtonClick: {
301
+ type: Function,
302
+ default: void 0
303
+ },
304
+ /**
305
+ * React's `onTokenizedBody={dispatchTokenizedBody}` for the redirect tile:
306
+ * the form posts `/process` with the approved payment. Without it, the
307
+ * tile reports `complete` itself, as this component's wallet row does.
308
+ */
309
+ onTokenizedBody: {
310
+ type: Function,
311
+ default: void 0
312
+ },
313
+ /**
314
+ * React's `isProcessing={isSubmitting}`: while the form is processing, the
315
+ * PayPal redirect tile and the APM button are disabled and ignore clicks.
316
+ */
317
+ isProcessing: { type: Boolean, default: false },
318
+ /**
319
+ * The form's storage-free stand-in for the click-time account patch React
320
+ * restores from its `localStorage` continuation (issue #410, triage AC 4:
321
+ * Vue never stores it). On an authorized redirect return it is asked once
322
+ * for the account, with the returned session id; what it answers goes to
323
+ * `onTokenizedBody` as the attempt's `accountPatch`. A throw or no answer
324
+ * continues without one, because the payment is already authorized.
325
+ */
326
+ recoverAccountPatch: {
327
+ type: Function,
328
+ default: void 0
329
+ }
330
+ },
331
+ emits: {
332
+ /** The buyer confirmed a wallet or APM and its element validated: the attempt starts. */
333
+ submitting: () => true,
334
+ complete: (_result) => true,
335
+ decline: (_decline) => true,
336
+ error: (_error) => true
337
+ },
338
+ setup(props, { emit }) {
339
+ const walletContainer = (0, import_vue5.ref)(null);
340
+ const apmContainer = (0, import_vue5.ref)(null);
341
+ const processing = (0, import_vue5.ref)(false);
342
+ const busy = () => processing.value || props.isProcessing;
343
+ const cleanups = [];
344
+ let apmElements = null;
345
+ let stripe = null;
346
+ const elementsOptions = (methods, surface) => {
347
+ const setupFutureUsage = (0, import_shared5.resolveSetupFutureUsage)(props.session, methods, surface);
348
+ const currency = (props.currency ?? props.session?.currency ?? "usd").toLowerCase();
349
+ const appearance = props.appearance ? { appearance: (0, import_shared5.toStripeAppearance)(props.appearance) } : {};
350
+ return props.intentKind === "setup" ? { mode: "setup", currency, paymentMethodTypes: methods, ...appearance } : {
351
+ mode: props.session?.mode === "subscription" ? "subscription" : "payment",
352
+ amount: props.amount,
353
+ currency,
354
+ paymentMethodTypes: methods,
355
+ ...setupFutureUsage ? { setupFutureUsage } : {},
356
+ ...appearance
357
+ };
358
+ };
359
+ const walletFollowsTotal = () => props.intentKind === "payment" && typeof props.totalAmount === "number";
360
+ const walletIntentKind = () => {
361
+ if (!walletFollowsTotal()) return props.intentKind;
362
+ return (props.totalAmount ?? 0) > 0 ? "payment" : "setup";
363
+ };
364
+ const walletElementsOptions = (methods) => {
365
+ if (!walletFollowsTotal()) return elementsOptions(methods, "wallet");
366
+ const setupFutureUsage = (0, import_shared5.resolveSetupFutureUsage)(props.session, methods, "wallet");
367
+ const currency = (props.currency ?? props.session?.currency ?? "usd").toLowerCase();
368
+ return {
369
+ ...(0, import_shared5.resolveWalletModeOptions)(props.totalAmount, props.session?.captureMethod),
370
+ currency,
371
+ paymentMethodTypes: methods,
372
+ ...setupFutureUsage ? { setupFutureUsage } : {},
373
+ ...props.appearance ? { appearance: (0, import_shared5.toStripeAppearance)(props.appearance) } : {}
374
+ };
375
+ };
376
+ let walletAttempt = null;
377
+ const floPayContext = (0, import_vue5.inject)(FLOPAY_CONTEXT, null);
378
+ const walletTelemetry = () => {
379
+ const instance = floPayContext?.flopay.value;
380
+ return instance ? telemetryBridge(instance) : void 0;
381
+ };
382
+ let walletSheetClick = null;
383
+ let walletSheetAttempts = 0;
384
+ const endWalletSheetClick = () => {
385
+ walletSheetClick?.stopObserving();
386
+ walletSheetClick = null;
387
+ };
388
+ const observeWalletSheetClick = (method, startedAt) => {
389
+ endWalletSheetClick();
390
+ walletSheetAttempts += 1;
391
+ const click = {
392
+ attempt: walletSheetAttempts,
393
+ method,
394
+ startedAt,
395
+ handoffObserved: false,
396
+ stopObserving: () => {
397
+ }
398
+ };
399
+ const onBlur = () => {
400
+ click.handoffObserved = true;
401
+ };
402
+ const onVisibilityChange = () => {
403
+ if (document.visibilityState === "hidden") click.handoffObserved = true;
404
+ };
405
+ window.addEventListener("blur", onBlur);
406
+ document.addEventListener("visibilitychange", onVisibilityChange);
407
+ click.stopObserving = () => {
408
+ window.removeEventListener("blur", onBlur);
409
+ document.removeEventListener("visibilitychange", onVisibilityChange);
410
+ };
411
+ walletSheetClick = click;
412
+ };
413
+ cleanups.push(endWalletSheetClick);
414
+ const reportWalletSheetRefusal = (click) => {
415
+ const environment = (0, import_shared5.detectPayPalEnvironment)();
416
+ walletTelemetry()?.error?.({
417
+ errorCode: "PROVIDER_RUNTIME_FAILED",
418
+ failureCategory: "provider_runtime",
419
+ stage: "overlay_open",
420
+ provider: "stripe",
421
+ paymentMethodCategory: "wallet",
422
+ requestCategory: "provider_sdk",
423
+ terminalReason: "provider_error",
424
+ attempt: click.attempt,
425
+ walletType: "google_pay",
426
+ ...environment,
427
+ presentationMode: "popup"
428
+ });
429
+ const failure = new import_shared5.FloPayError((0, import_shared5.buildWalletSheetWindowMessage)("Google Pay"), "api_error", {
430
+ code: "external_payment_method_failed",
431
+ param: click.method,
432
+ details: {
433
+ providerCode: import_shared5.WALLET_SHEET_WINDOW_TOO_SMALL_CODE,
434
+ popupBlocked: false,
435
+ ...environment,
436
+ presentationMode: "popup"
437
+ }
438
+ });
439
+ (0, import_shared5.invokeMerchantCallback)(() => emit("error", failure));
440
+ };
441
+ const gateWalletClick = async (event) => {
442
+ const clickStartedAt = walletClockNow();
443
+ walletAttempt = null;
444
+ endWalletSheetClick();
445
+ const gate = props.runBeforeButtonClick;
446
+ if (!gate) {
447
+ observeWalletSheetClick(event.expressPaymentType, clickStartedAt);
448
+ event.resolve();
449
+ return;
450
+ }
451
+ let clickActive = true;
452
+ const clickTimer = setTimeout(() => {
453
+ clickActive = false;
454
+ event.reject();
455
+ }, EXPRESS_CHECKOUT_CLICK_BUDGET_MS);
456
+ let beforeClick;
457
+ try {
458
+ beforeClick = await gate(checkoutButtonMethodFromProviderMethod(event.expressPaymentType));
459
+ } catch {
460
+ beforeClick = { proceed: false };
461
+ } finally {
462
+ clearTimeout(clickTimer);
463
+ }
464
+ if (!clickActive) return;
465
+ if (!beforeClick.proceed) {
466
+ event.reject();
467
+ return;
468
+ }
469
+ walletAttempt = {
470
+ method: event.expressPaymentType,
471
+ ...beforeClick.sessionId ? { sessionId: beforeClick.sessionId } : {},
472
+ ...beforeClick.nonce ? { nonce: beforeClick.nonce } : {}
473
+ };
474
+ observeWalletSheetClick(event.expressPaymentType, clickStartedAt);
475
+ event.resolve();
476
+ };
477
+ const confirm = async (elements, method, intentKind = props.intentKind, attempt = null) => {
478
+ if (!stripe || processing.value) return;
479
+ processing.value = true;
480
+ const api = new import_card_setup2.PaymentAPI(props.billingApiUrl, { telemetry: false });
481
+ const category = stripeMethodCategory(method);
482
+ const sessionId = attempt?.sessionId ?? props.sessionId;
483
+ const nonce = attempt?.nonce ?? props.nonce;
484
+ try {
485
+ const submitted = await elements.submit();
486
+ if (submitted.error) return;
487
+ (0, import_shared5.invokeMerchantCallback)(() => emit("submitting"));
488
+ const intent = await api.createSessionIntent(sessionId, nonce ?? "", {
489
+ provider: "stripe",
490
+ paymentMethodCategory: category,
491
+ paymentMethodType: method,
492
+ paymentMethodId: null,
493
+ intentKind
494
+ });
495
+ const clientSecret = intent.clientSecret ?? "";
496
+ const returnUrl = props.returnUrl ?? window.location.href;
497
+ const { error } = intentKind === "setup" ? await stripe.confirmSetup({
498
+ elements,
499
+ clientSecret,
500
+ confirmParams: { return_url: returnUrl },
501
+ redirect: "if_required"
502
+ }) : await stripe.confirmPayment({
503
+ elements,
504
+ clientSecret,
505
+ confirmParams: { return_url: returnUrl },
506
+ redirect: "if_required"
507
+ });
508
+ if (error) {
509
+ if (intentKind === "payment") {
510
+ void api.reportSessionIntentDecline(sessionId, nonce ?? "", {
511
+ provider: "stripe",
512
+ paymentMethodCategory: category,
513
+ paymentMethodType: method,
514
+ providerDeclineReason: declineReason(error),
515
+ providerIntentId: intent.providerObjectId
516
+ }).catch(() => {
517
+ });
518
+ }
519
+ const decline = {
520
+ method,
521
+ message: error.message ?? "Your payment was declined.",
522
+ code: error.code,
523
+ declineCode: error.decline_code ?? error.code,
524
+ sessionId
525
+ };
526
+ (0, import_shared5.invokeMerchantCallback)(() => emit("decline", decline));
527
+ return;
528
+ }
529
+ const result = {
530
+ status: "succeeded",
531
+ paymentIntentId: intent.providerObjectId,
532
+ checkoutMethod: method
533
+ };
534
+ (0, import_shared5.invokeMerchantCallback)(() => emit("complete", result));
535
+ } catch (error) {
536
+ const failure = error instanceof import_shared5.FloPayError ? error : new import_shared5.FloPayError(
537
+ error instanceof Error ? error.message : "Payment failed.",
538
+ "api_error"
539
+ );
540
+ (0, import_shared5.invokeMerchantCallback)(() => emit("error", failure));
541
+ } finally {
542
+ processing.value = false;
543
+ }
544
+ };
545
+ const paypalRedirect = () => props.paypalPresentation === "redirect" && props.intentKind === "payment" && props.methods.includes("paypal");
546
+ let active = true;
547
+ const emitPayPalDecline = (message, sessionId, error) => {
548
+ const decline = {
549
+ method: "paypal",
550
+ message,
551
+ ...error ? { code: error.code, declineCode: error.decline_code ?? error.code } : {},
552
+ sessionId
553
+ };
554
+ (0, import_shared5.invokeMerchantCallback)(() => emit("decline", decline));
555
+ };
556
+ const emitPayPalFailure = (message, code) => {
557
+ const failure = new import_shared5.FloPayError(message, "api_error", code ? { code } : void 0);
558
+ (0, import_shared5.invokeMerchantCallback)(() => emit("error", failure));
559
+ };
560
+ const handOffPayPal = (body, overrides) => {
561
+ if (props.onTokenizedBody) {
562
+ props.onTokenizedBody(body, overrides);
563
+ return;
564
+ }
565
+ const result = {
566
+ status: "succeeded",
567
+ paymentIntentId: body.threeDSecureActionResultTokenId,
568
+ checkoutMethod: "paypal"
569
+ };
570
+ (0, import_shared5.invokeMerchantCallback)(() => emit("complete", result));
571
+ };
572
+ const redirectToPayPal = async () => {
573
+ if (!stripe || busy()) return;
574
+ processing.value = true;
575
+ const api = new import_card_setup2.PaymentAPI(props.billingApiUrl, { telemetry: false });
576
+ let sessionId = props.sessionId;
577
+ try {
578
+ const beforeClick = props.runBeforeButtonClick ? await props.runBeforeButtonClick("paypal") : { proceed: true };
579
+ if (!beforeClick.proceed) return;
580
+ sessionId = beforeClick.sessionId ?? props.sessionId;
581
+ const nonce = beforeClick.nonce ?? props.nonce;
582
+ const createPaymentMethod = stripe.createPaymentMethod;
583
+ const { error: pmError, paymentMethod } = await createPaymentMethod.call(stripe, {
584
+ type: "paypal"
585
+ });
586
+ if (pmError) {
587
+ emitPayPalFailure(
588
+ pmError.message ?? "PayPal could not be started.",
589
+ "stripe_paypal_create_payment_method_failed"
590
+ );
591
+ return;
592
+ }
593
+ const intent = await api.createSessionIntent(sessionId, nonce ?? "", {
594
+ provider: "stripe",
595
+ paymentMethodCategory: "wallet",
596
+ paymentMethodType: "paypal",
597
+ paymentMethodId: paymentMethod?.id ?? null,
598
+ intentKind: "payment"
599
+ });
600
+ if (intent.provider !== "stripe") {
601
+ throw new Error("Invalid provider returned for PayPal intent");
602
+ }
603
+ const clientSecret = intent.clientSecret ?? "";
604
+ const attempt = createStripePayPalAttempt();
605
+ const continuationSaved = saveStripePayPalContinuation({ attempt, sessionId, nonce });
606
+ if (!continuationSaved) {
607
+ throw new Error("Unable to save PayPal checkout progress. Please try again.");
608
+ }
609
+ const { error, paymentIntent } = await stripe.confirmPayment({
610
+ clientSecret,
611
+ confirmParams: {
612
+ return_url: stripePayPalReturnUrl(attempt),
613
+ ...paymentMethod?.id ? { payment_method: paymentMethod.id } : {}
614
+ },
615
+ redirect: "if_required"
616
+ });
617
+ clearStripePayPalContinuation();
618
+ if (error) {
619
+ if ((0, import_shared5.isProviderDecline)(error)) {
620
+ void api.reportSessionIntentDecline(sessionId, nonce ?? "", {
621
+ provider: "stripe",
622
+ paymentMethodCategory: "wallet",
623
+ paymentMethodType: "paypal",
624
+ providerDeclineReason: error.code ?? "provider_declined",
625
+ providerIntentId: intent.providerObjectId
626
+ }).catch(() => {
627
+ });
628
+ emitPayPalDecline(error.message ?? "Your payment was declined.", sessionId, error);
629
+ return;
630
+ }
631
+ emitPayPalFailure(
632
+ error.message ?? "PayPal payment failed.",
633
+ "stripe_paypal_confirm_failed"
634
+ );
635
+ return;
636
+ }
637
+ const paymentMethodId = typeof paymentIntent?.payment_method === "string" ? paymentIntent.payment_method : paymentIntent?.payment_method?.id;
638
+ handOffPayPal(
639
+ {
640
+ id: paymentMethodId ?? paymentIntent?.id,
641
+ type: "card",
642
+ threeDSecureActionResultTokenId: paymentIntent?.id,
643
+ isPaypal: true
644
+ },
645
+ {
646
+ accountPatch: beforeClick.accountPatch,
647
+ paymentMethodCategory: "paypal",
648
+ presentationMode: "redirect",
649
+ sessionId,
650
+ nonce
651
+ }
652
+ );
653
+ } catch (error) {
654
+ emitPayPalFailure(
655
+ error instanceof Error ? error.message : "PayPal payment failed.",
656
+ "stripe_paypal_failed"
657
+ );
658
+ } finally {
659
+ processing.value = false;
660
+ }
661
+ };
662
+ const recoverReturnAccount = async (sessionId) => {
663
+ const recover = props.recoverAccountPatch;
664
+ if (!recover) return void 0;
665
+ try {
666
+ return await recover(sessionId) ?? void 0;
667
+ } catch {
668
+ return void 0;
669
+ }
670
+ };
671
+ const continueAuthorizedReturn = async (paymentIntent, continuation) => {
672
+ const paymentMethodId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
673
+ const accountPatch = await recoverReturnAccount(continuation.sessionId);
674
+ if (!active) return;
675
+ handOffPayPal(
676
+ {
677
+ id: paymentMethodId ?? paymentIntent.id,
678
+ type: "card",
679
+ threeDSecureActionResultTokenId: paymentIntent.id,
680
+ isPaypal: true
681
+ },
682
+ {
683
+ ...accountPatch ? { accountPatch } : {},
684
+ sessionId: continuation.sessionId,
685
+ nonce: continuation.nonce,
686
+ presentationMode: "redirect"
687
+ }
688
+ );
689
+ };
690
+ const resumeStripePayPal = async (provider) => {
691
+ const [paymentIntentId, clientSecret, redirectStatus] = (0, import_shared5.readStripeRedirectParams)();
692
+ if (!paymentIntentId || !clientSecret) return;
693
+ const continuation = readStripePayPalReturnContinuation();
694
+ if (!continuation) return;
695
+ const sessionId = continuation.sessionId;
696
+ const retrievalKey = continuation.attempt;
697
+ let retrieval = stripePayPalResumeAttempts.get(retrievalKey);
698
+ if (!retrieval) {
699
+ retrieval = redirectStatus === "failed" ? Promise.resolve(null) : Promise.resolve().then(() => provider.retrievePaymentIntent(clientSecret));
700
+ stripePayPalResumeAttempts.set(retrievalKey, retrieval);
701
+ }
702
+ processing.value = true;
703
+ try {
704
+ const result = await retrieval;
705
+ if (!active) return;
706
+ if (result?.error) {
707
+ stripePayPalResumeAttempts.delete(retrievalKey);
708
+ emitPayPalFailure(
709
+ result.error.message ?? "Failed to retrieve PayPal payment status.",
710
+ "REDIRECT_RESUME_FAILED"
711
+ );
712
+ return;
713
+ }
714
+ consumeStripePayPalReturn();
715
+ stripePayPalResumeAttempts.delete(retrievalKey);
716
+ if (redirectStatus === "failed") {
717
+ emitPayPalDecline("PayPal payment was declined. Please try again.", sessionId);
718
+ return;
719
+ }
720
+ const paymentIntent = result?.paymentIntent;
721
+ if (paymentIntent && paymentIntent.id !== paymentIntentId) {
722
+ emitPayPalFailure(
723
+ "Failed to match the returned PayPal payment.",
724
+ "REDIRECT_RESUME_FAILED"
725
+ );
726
+ return;
727
+ }
728
+ if (paymentIntent && (paymentIntent.status === "requires_capture" || paymentIntent.status === "succeeded")) {
729
+ await continueAuthorizedReturn(paymentIntent, continuation);
730
+ return;
731
+ }
732
+ emitPayPalDecline("PayPal payment was not completed. Please try again.", sessionId);
733
+ } catch (error) {
734
+ if (!active) return;
735
+ stripePayPalResumeAttempts.delete(retrievalKey);
736
+ emitPayPalFailure(
737
+ error instanceof Error ? error.message : "Failed to complete PayPal payment.",
738
+ "REDIRECT_RESUME_FAILED"
739
+ );
740
+ } finally {
741
+ if (active) processing.value = false;
742
+ }
743
+ };
744
+ (0, import_vue5.onMounted)(async () => {
745
+ const wallets = props.methods.filter(
746
+ (method) => WALLET_METHODS.has(method) && !(method === "paypal" && paypalRedirect())
747
+ );
748
+ const apms = props.methods.filter((method) => !WALLET_METHODS.has(method));
749
+ try {
750
+ const { loadStripe } = await import("@stripe/stripe-js");
751
+ stripe = await loadStripe(props.publishableKey);
752
+ if (!stripe) throw new Error("Stripe.js did not initialize.");
753
+ if (paypalRedirect()) void resumeStripePayPal(stripe);
754
+ if (wallets.length > 0 && walletContainer.value) {
755
+ if ((0, import_shared5.shouldPreferWalletPopup)()) cleanups.push((0, import_shared5.denyPaymentPermissionToNewFrames)(document));
756
+ const elements = stripe.elements(walletElementsOptions(wallets));
757
+ const express = elements.create("expressCheckout");
758
+ express.on("click", (event) => gateWalletClick(event));
759
+ express.on("confirm", (event) => {
760
+ const attempt = walletAttempt;
761
+ walletAttempt = null;
762
+ endWalletSheetClick();
763
+ void confirm(elements, event.expressPaymentType, walletIntentKind(), attempt);
764
+ });
765
+ express.on("cancel", () => {
766
+ walletAttempt = null;
767
+ const click = walletSheetClick;
768
+ endWalletSheetClick();
769
+ if (click && (0, import_shared5.isWalletSheetRefusedForWindowSize)({
770
+ method: click.method,
771
+ elapsedMs: walletClockNow() - click.startedAt,
772
+ handoffObserved: click.handoffObserved
773
+ })) {
774
+ reportWalletSheetRefusal(click);
775
+ }
776
+ });
777
+ express.mount(walletContainer.value);
778
+ cleanups.push(() => express.destroy());
779
+ }
780
+ if (apms.length > 0 && apmContainer.value) {
781
+ apmElements = stripe.elements(elementsOptions(apms, "inline_apm"));
782
+ const payment = apmElements.create("payment", { layout: "accordion" });
783
+ payment.mount(apmContainer.value);
784
+ cleanups.push(() => payment.destroy());
785
+ }
786
+ } catch (error) {
787
+ const failure = new import_shared5.FloPayError(
788
+ error instanceof Error ? error.message : "Payment methods failed to load.",
789
+ "api_error",
790
+ { code: "PROVIDER_LOAD_FAILED" }
791
+ );
792
+ (0, import_shared5.invokeMerchantCallback)(() => emit("error", failure));
793
+ }
794
+ });
795
+ (0, import_vue5.onBeforeUnmount)(() => {
796
+ active = false;
797
+ for (const cleanup of cleanups.splice(0)) cleanup();
798
+ });
799
+ return () => (0, import_vue5.h)("div", { "data-testid": "flopay-stripe-methods" }, [
800
+ // React renders the Stripe PayPal group above the wallet row.
801
+ paypalRedirect() ? (0, import_vue5.h)(
802
+ "div",
803
+ {
804
+ role: "group",
805
+ "aria-label": "PayPal payment method",
806
+ "data-testid": "flopay-paypal-focus-target",
807
+ style: { borderRadius: "8px" }
808
+ },
809
+ [
810
+ (0, import_vue5.h)(
811
+ "button",
812
+ {
813
+ type: "button",
814
+ "data-testid": "flopay-stripe-paypal-redirect",
815
+ disabled: busy(),
816
+ onClick: () => void redirectToPayPal(),
817
+ style: {
818
+ width: "100%",
819
+ minHeight: `${PAYPAL_REDIRECT_TILE_HEIGHT}px`,
820
+ border: "1px solid #d1d5db",
821
+ borderRadius: "8px",
822
+ background: "#ffc439",
823
+ color: "#111827",
824
+ cursor: busy() ? "not-allowed" : "pointer",
825
+ fontSize: "1rem",
826
+ fontWeight: 700
827
+ }
828
+ },
829
+ "Pay with PayPal"
830
+ )
831
+ ]
832
+ ) : null,
833
+ (0, import_vue5.h)("div", { ref: walletContainer, "data-testid": "flopay-express-checkout" }),
834
+ (0, import_vue5.h)("div", { ref: apmContainer, "data-testid": "flopay-payment-element" }),
835
+ props.methods.some((method) => !WALLET_METHODS.has(method)) ? (0, import_vue5.h)(
836
+ "button",
837
+ {
838
+ type: "button",
839
+ disabled: busy(),
840
+ onClick: () => {
841
+ if (props.isProcessing) return;
842
+ const method = props.methods.find((candidate) => !WALLET_METHODS.has(candidate));
843
+ if (apmElements && method) void confirm(apmElements, method);
844
+ }
845
+ },
846
+ processing.value ? "Processing\u2026" : "Continue"
847
+ ) : null
848
+ ]);
849
+ }
850
+ });
851
+ }
852
+ });
853
+
854
+ // src/payment-method-setup-paypal.ts
855
+ var payment_method_setup_paypal_exports = {};
856
+ __export(payment_method_setup_paypal_exports, {
857
+ PayPalSetupMethod: () => PayPalSetupMethod
858
+ });
859
+ var import_card_setup3, import_shared6, import_paypal_js, import_vue6, PayPalSetupMethod;
860
+ var init_payment_method_setup_paypal = __esm({
861
+ "src/payment-method-setup-paypal.ts"() {
862
+ "use strict";
863
+ import_card_setup3 = require("@flopay/js/card-setup");
864
+ import_shared6 = require("@flopay/shared");
865
+ import_paypal_js = require("@paypal/paypal-js");
866
+ import_vue6 = require("vue");
867
+ PayPalSetupMethod = (0, import_vue6.defineComponent)({
868
+ name: "FloPayPayPalSetupMethod",
869
+ props: {
870
+ clientId: { type: String, required: true },
871
+ environment: { type: String, default: void 0 },
872
+ /** The setup session's own currency, when the read carries one. A vault flow needs none. */
873
+ currency: { type: String, default: void 0 },
874
+ sessionId: { type: String, required: true },
875
+ nonce: { type: String, required: true },
876
+ billingApiUrl: { type: String, required: true },
877
+ telemetry: { type: Boolean, default: void 0 },
878
+ customer: { type: Object, required: true }
879
+ },
880
+ emits: {
881
+ /** The buyer started saving PayPal. */
882
+ submitting: () => true,
883
+ /** PayPal accepted the account and the billing API took the token. */
884
+ confirmed: () => true,
885
+ /** A technical failure; `retryable` says whether trying again can help. */
886
+ failed: (_error, _options) => true,
887
+ /** The buyer closed PayPal's window without an outcome (not a cancel). */
888
+ abandoned: () => true
889
+ },
890
+ setup(props, { emit }) {
891
+ const container = (0, import_vue6.ref)(null);
892
+ let active = true;
893
+ let close;
894
+ let api = null;
895
+ (0, import_vue6.onMounted)(() => {
896
+ const { inAppBrowser } = (0, import_shared6.detectPayPalEnvironment)();
897
+ const client = new import_card_setup3.PaymentAPI(props.billingApiUrl, { telemetry: props.telemetry });
898
+ api = client;
899
+ const { sessionId, nonce } = props;
900
+ const customer = {
901
+ userId: props.customer.userId,
902
+ email: props.customer.email
903
+ };
904
+ const environment = props.environment;
905
+ void (async () => {
906
+ try {
907
+ const paypal = await (0, import_paypal_js.loadScript)({
908
+ clientId: props.clientId,
909
+ ...props.currency ? { currency: props.currency.toUpperCase() } : {},
910
+ components: "buttons",
911
+ ...environment === "sandbox" || environment === "production" ? { environment } : {}
912
+ });
913
+ const target = container.value;
914
+ if (!active || !target) return;
915
+ if (!paypal?.Buttons) {
916
+ throw new import_shared6.FloPayError("PayPal could not be loaded.", "api_error", {
917
+ code: "paypal_unavailable"
918
+ });
919
+ }
920
+ const buttons = paypal.Buttons({
921
+ createVaultSetupToken: async () => {
922
+ emit("submitting");
923
+ const intent = await client.createSessionIntent(sessionId, nonce, {
924
+ provider: "paypal",
925
+ paymentMethodCategory: "wallet",
926
+ paymentMethodType: import_shared6.PAYPAL_VAULTED_PAYMENT_METHOD_TYPE,
927
+ paymentMethodId: null,
928
+ intentKind: "setup"
929
+ });
930
+ if (intent.provider !== "paypal" || intent.providerObjectType !== "setup_token") {
931
+ throw new import_shared6.FloPayError(
932
+ "The billing API did not return a PayPal setup token.",
933
+ "api_error",
934
+ { code: "InvalidSessionIntentResponse" }
935
+ );
936
+ }
937
+ return intent.providerObjectId;
938
+ },
939
+ onApprove: async (data) => {
940
+ const token = data.vaultSetupToken;
941
+ if (!token) {
942
+ throw new import_shared6.FloPayError("PayPal approved without a setup token.", "api_error", {
943
+ code: "paypal_setup_token_missing"
944
+ });
945
+ }
946
+ const response = await client.processPayment(customer.userId, {
947
+ sessionId,
948
+ nonce,
949
+ paymentMethodCategory: "paypal",
950
+ tokenizedData: {
951
+ id: token,
952
+ type: "paypal_setup_token",
953
+ isPaypal: true,
954
+ gateway: "paypal",
955
+ paymentMethodType: import_shared6.PAYPAL_VAULTED_PAYMENT_METHOD_TYPE
956
+ },
957
+ accountData: customer
958
+ });
959
+ if (!response.ok) {
960
+ throw new import_shared6.FloPayError("PayPal could not be saved.", "api_error", {
961
+ code: "paypal_setup_failed",
962
+ statusCode: response.status
963
+ });
964
+ }
965
+ if (active) emit("confirmed");
966
+ },
967
+ onCancel: () => {
968
+ if (active) emit("abandoned");
969
+ },
970
+ onError: (error) => {
971
+ if (!active) return;
972
+ const { popupBlocked } = (0, import_shared6.classifyExternalMethodFailure)(error, {
973
+ inAppBrowser,
974
+ handoffObserved: false
975
+ });
976
+ emit(
977
+ "failed",
978
+ popupBlocked ? new Error((0, import_shared6.buildExternalMethodRecoveryMessage)("PayPal", true, inAppBrowser)) : error,
979
+ { code: popupBlocked ? "popup_blocked" : "paypal_setup_failed", retryable: true }
980
+ );
981
+ }
982
+ });
983
+ close = () => buttons.close();
984
+ await buttons.render(target);
985
+ } catch (error) {
986
+ if (!active) return;
987
+ emit("failed", error, { code: "paypal_unavailable", retryable: true });
988
+ }
989
+ })();
990
+ });
991
+ (0, import_vue6.onBeforeUnmount)(() => {
992
+ active = false;
993
+ void close?.().catch(() => {
994
+ });
995
+ api?.destroy();
996
+ api = null;
997
+ });
998
+ return () => (0, import_vue6.h)("div", { ref: container, "data-flopay-paypal-setup": "" });
999
+ }
1000
+ });
1001
+ }
1002
+ });
1003
+
1004
+ // src/payment-method-setup-entry.ts
1005
+ var payment_method_setup_entry_exports = {};
1006
+ __export(payment_method_setup_entry_exports, {
1007
+ FloPayPaymentMethodSetup: () => FloPayPaymentMethodSetup
1008
+ });
1009
+ module.exports = __toCommonJS(payment_method_setup_entry_exports);
1010
+
1011
+ // src/flopay-payment-method-setup-main.ts
1012
+ var import_card_setup4 = require("@flopay/js/card-setup");
1013
+ var import_shared7 = require("@flopay/shared");
1014
+ var import_vue7 = require("vue");
1015
+
1016
+ // src/hosted-setup.ts
1017
+ var import_card_setup = require("@flopay/js/card-setup");
1018
+ var import_shared = require("@flopay/shared");
1019
+ var import_vue2 = require("vue");
1020
+
1021
+ // src/processing-overlay.ts
1022
+ var import_vue = require("vue");
1023
+ var PROCESSING_OVERLAY_SUCCESS_DELAY_MS = 600;
1024
+ var PROCESSING_OVERLAY_ERROR_DELAY_MS = 400;
1025
+ var NEUTRAL_THEME = {
1026
+ panel: { background: "white" },
1027
+ spinnerTrackColor: "#e5e7eb",
1028
+ spinnerAccentColor: "#4A49FF",
1029
+ labelColors: { processing: "#374151", success: "#16a34a", error: "#dc2626" },
1030
+ noteColor: "#6b7280"
1031
+ };
1032
+ var KEYFRAMES_ID = "flopay-processing-overlay-keyframes";
1033
+ var KEYFRAMES = [
1034
+ "@keyframes flopay-spin { to { transform: rotate(360deg); } }",
1035
+ "@keyframes flopay-pop { 0% { transform: scale(0); } 100% { transform: scale(1); } }",
1036
+ "@keyframes flopay-draw { to { stroke-dashoffset: 0; } }",
1037
+ "@keyframes flopay-shake { 0%,100% { transform: translateX(0); } 20%,60% { transform: translateX(-4px); } 40%,80% { transform: translateX(4px); } }"
1038
+ ].join("\n");
1039
+ function ensureKeyframes() {
1040
+ if (typeof document === "undefined" || document.getElementById(KEYFRAMES_ID)) return;
1041
+ const style = document.createElement("style");
1042
+ style.id = KEYFRAMES_ID;
1043
+ style.textContent = KEYFRAMES;
1044
+ document.head.appendChild(style);
1045
+ }
1046
+ var SVG = {
1047
+ "aria-hidden": "true",
1048
+ width: "48",
1049
+ height: "48",
1050
+ viewBox: "0 0 24 24",
1051
+ fill: "none"
1052
+ };
1053
+ function statusIcon(status, theme) {
1054
+ if (status === "processing") {
1055
+ return (0, import_vue.h)("svg", { ...SVG, style: { animation: "flopay-spin 0.8s linear infinite" } }, [
1056
+ (0, import_vue.h)("circle", {
1057
+ cx: "12",
1058
+ cy: "12",
1059
+ r: "10",
1060
+ stroke: theme.spinnerTrackColor,
1061
+ "stroke-width": "3"
1062
+ }),
1063
+ (0, import_vue.h)("path", { d: "M4 12a8 8 0 018-8v3a5 5 0 00-5 5H4z", fill: theme.spinnerAccentColor })
1064
+ ]);
1065
+ }
1066
+ if (status === "success") {
1067
+ return (0, import_vue.h)("div", { style: { animation: "flopay-pop 0.4s cubic-bezier(0.34,1.56,0.64,1)" } }, [
1068
+ (0, import_vue.h)("svg", SVG, [
1069
+ (0, import_vue.h)("circle", { cx: "12", cy: "12", r: "11", fill: "#22c55e" }),
1070
+ (0, import_vue.h)("path", {
1071
+ d: "M7 12.5l3 3 7-7",
1072
+ stroke: "white",
1073
+ "stroke-width": "2.5",
1074
+ "stroke-linecap": "round",
1075
+ "stroke-linejoin": "round",
1076
+ style: {
1077
+ strokeDasharray: 20,
1078
+ strokeDashoffset: 20,
1079
+ animation: "flopay-draw 0.4s 0.15s ease forwards"
1080
+ }
1081
+ })
1082
+ ])
1083
+ ]);
1084
+ }
1085
+ return (0, import_vue.h)("div", { style: { animation: "flopay-shake 0.4s ease" } }, [
1086
+ (0, import_vue.h)("svg", SVG, [
1087
+ (0, import_vue.h)("circle", { cx: "12", cy: "12", r: "11", fill: "#ef4444" }),
1088
+ (0, import_vue.h)("path", {
1089
+ d: "M8 8l8 8M16 8l-8 8",
1090
+ stroke: "white",
1091
+ "stroke-width": "2.5",
1092
+ "stroke-linecap": "round",
1093
+ style: {
1094
+ strokeDasharray: 12,
1095
+ strokeDashoffset: 12,
1096
+ animation: "flopay-draw 0.3s 0.1s ease forwards"
1097
+ }
1098
+ })
1099
+ ])
1100
+ ]);
1101
+ }
1102
+ var ProcessingOverlay = (0, import_vue.defineComponent)({
1103
+ name: "FloPayProcessingOverlay",
1104
+ props: {
1105
+ status: { type: String, required: true },
1106
+ errorMessage: { type: String, default: null },
1107
+ processingLabel: { type: String, default: "PROCESSING..." },
1108
+ successLabel: { type: String, default: "PAYMENT SUCCESSFUL" },
1109
+ errorLabel: { type: String, default: "PAYMENT FAILED" },
1110
+ /** Success sub-note; `null` omits it (surfaces that do not redirect). */
1111
+ successNote: {
1112
+ type: String,
1113
+ default: "You will be automatically redirected, do not close or navigate away from this window."
1114
+ },
1115
+ theme: { type: Object, default: void 0 }
1116
+ },
1117
+ setup(props) {
1118
+ (0, import_vue.onMounted)(ensureKeyframes);
1119
+ return () => {
1120
+ const theme = props.theme ?? NEUTRAL_THEME;
1121
+ const noteStyle = {
1122
+ fontSize: "13px",
1123
+ color: theme.noteColor,
1124
+ fontWeight: 400,
1125
+ maxWidth: "260px",
1126
+ lineHeight: 1.4,
1127
+ margin: 0
1128
+ };
1129
+ const label = {
1130
+ processing: props.processingLabel,
1131
+ success: props.successLabel,
1132
+ error: props.errorLabel
1133
+ }[props.status];
1134
+ const note = { processing: null, success: props.successNote, error: props.errorMessage }[props.status];
1135
+ return (0, import_vue.h)(
1136
+ "div",
1137
+ {
1138
+ "data-testid": "flopay-processing-overlay",
1139
+ "data-status": props.status,
1140
+ // The attribute the Vue setup surfaces' overlay already carried.
1141
+ "data-state": props.status,
1142
+ role: "dialog",
1143
+ "aria-modal": "true",
1144
+ style: {
1145
+ position: "fixed",
1146
+ inset: 0,
1147
+ background: "rgba(0,0,0,0.35)",
1148
+ display: "flex",
1149
+ alignItems: "center",
1150
+ justifyContent: "center",
1151
+ zIndex: 1e3,
1152
+ backdropFilter: "blur(2px)"
1153
+ }
1154
+ },
1155
+ [
1156
+ (0, import_vue.h)(
1157
+ "div",
1158
+ {
1159
+ "data-testid": "flopay-processing-overlay-panel",
1160
+ style: {
1161
+ borderRadius: "12px",
1162
+ boxShadow: "0 8px 32px rgba(0,0,0,0.18)",
1163
+ ...theme.panel,
1164
+ padding: "2rem 2.5rem",
1165
+ textAlign: "center",
1166
+ minWidth: "240px",
1167
+ display: "flex",
1168
+ flexDirection: "column",
1169
+ alignItems: "center",
1170
+ gap: "16px"
1171
+ }
1172
+ },
1173
+ [
1174
+ (0, import_vue.h)("div", { style: { width: "48px", height: "48px", position: "relative" } }, [
1175
+ statusIcon(props.status, theme)
1176
+ ]),
1177
+ (0, import_vue.h)(
1178
+ "span",
1179
+ {
1180
+ "data-testid": "flopay-processing-overlay-label",
1181
+ style: {
1182
+ fontSize: "14px",
1183
+ fontWeight: 600,
1184
+ letterSpacing: "0.05em",
1185
+ color: theme.labelColors[props.status]
1186
+ }
1187
+ },
1188
+ label
1189
+ ),
1190
+ note ? (0, import_vue.h)("p", { style: noteStyle }, note) : null
1191
+ ]
1192
+ )
1193
+ ]
1194
+ );
1195
+ };
1196
+ }
1197
+ });
1198
+
1199
+ // src/hosted-setup.ts
1200
+ var FloPayCardSetupError = class extends import_shared.FloPayError {
1201
+ constructor(message, type, options) {
1202
+ super(message, type, options);
1203
+ this.name = "FloPayCardSetupError";
1204
+ this.retryable = options.retryable;
1205
+ }
1206
+ };
1207
+ function asCardSetupError(error, overrides = {}) {
1208
+ const source = error instanceof import_shared.FloPayError ? error : void 0;
1209
+ const statusCode = source?.statusCode;
1210
+ const retryable = overrides.retryable ?? (source?.type === "network_error" || source?.type === "rate_limit_error" || typeof statusCode === "number" && statusCode >= 500 || source?.code === "vault_capture_timeout" || !source);
1211
+ return new FloPayCardSetupError(
1212
+ source?.message ?? (error instanceof Error ? error.message : "Failed to load the secure card form."),
1213
+ source?.type ?? "api_error",
1214
+ {
1215
+ code: overrides.code ?? source?.code ?? "card_setup_load_failed",
1216
+ retryable,
1217
+ ...source?.param ? { param: source.param } : {},
1218
+ ...statusCode === void 0 ? {} : { statusCode }
1219
+ }
1220
+ );
1221
+ }
1222
+ function useCardSetupTheme(source) {
1223
+ const resolved = (0, import_vue2.computed)(
1224
+ () => (0, import_shared.resolveCardSetupTheme)({
1225
+ theme: source.theme ?? "modern-light",
1226
+ appearance: source.appearance,
1227
+ buttonsStyles: source.buttonsStyles
1228
+ })
1229
+ );
1230
+ const vaultTheme = (0, import_vue2.computed)(() => resolved.value.vaultTheme);
1231
+ const surfaceStyle = (0, import_vue2.computed)(
1232
+ () => (0, import_shared.mergeCardSetupSurface)(resolved.value.surface, source.containerStyle)
1233
+ );
1234
+ const overlayTheme = (0, import_vue2.computed)(() => resolved.value.overlayTheme);
1235
+ const appearance = (0, import_vue2.computed)(() => resolved.value.appearance);
1236
+ return { vaultTheme, surfaceStyle, overlayTheme, appearance };
1237
+ }
1238
+ function useHostedSetup(props, emitters, options) {
1239
+ const capture = (0, import_vue2.shallowRef)(null);
1240
+ const vault = (0, import_vue2.shallowRef)(null);
1241
+ const session = (0, import_vue2.shallowRef)(null);
1242
+ const loadError = (0, import_vue2.ref)(null);
1243
+ let active = true;
1244
+ let terminal = false;
1245
+ let completed = false;
1246
+ let submissionInFlight = false;
1247
+ let clearTimer = null;
1248
+ const offs = [];
1249
+ const sessionId = () => props.sessionId ?? "";
1250
+ const showOverlay = (state) => {
1251
+ if (clearTimer) clearTimeout(clearTimer);
1252
+ clearTimer = null;
1253
+ emitters.overlay(state);
1254
+ };
1255
+ const holdFailure = (message) => {
1256
+ showOverlay({ status: "error", message });
1257
+ clearTimer = setTimeout(() => {
1258
+ clearTimer = null;
1259
+ if (active) emitters.overlay(null);
1260
+ }, PROCESSING_OVERLAY_ERROR_DELAY_MS);
1261
+ };
1262
+ const bind = (adapter) => {
1263
+ submissionInFlight = false;
1264
+ offs.push(
1265
+ adapter.on("submitting", () => {
1266
+ submissionInFlight = true;
1267
+ showOverlay({ status: "processing", message: null });
1268
+ }),
1269
+ adapter.on("complete", (event) => {
1270
+ terminal = true;
1271
+ submissionInFlight = false;
1272
+ showOverlay({ status: "success", message: null });
1273
+ setTimeout(() => {
1274
+ if (completed) return;
1275
+ completed = true;
1276
+ (0, import_shared.invokeMerchantCallback)(
1277
+ () => emitters.complete({
1278
+ status: "succeeded",
1279
+ sessionId: sessionId(),
1280
+ ...event.paymentMethod ? { paymentMethod: event.paymentMethod } : {}
1281
+ })
1282
+ );
1283
+ if (active) emitters.overlay(null);
1284
+ }, PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
1285
+ }),
1286
+ adapter.on("decline", (event) => {
1287
+ terminal = true;
1288
+ submissionInFlight = false;
1289
+ holdFailure(event.message ?? null);
1290
+ (0, import_shared.invokeMerchantCallback)(
1291
+ () => emitters.decline({
1292
+ status: "declined",
1293
+ sessionId: sessionId(),
1294
+ ...event.declineReason ? { reason: event.declineReason } : {},
1295
+ ...event.message ? { message: event.message } : {}
1296
+ })
1297
+ );
1298
+ }),
1299
+ // Closing the bank's authentication window is not an outcome: the hosted
1300
+ // form keeps polling and may still report one. Drop the overlay but keep
1301
+ // the submission in flight, so a late failure still shows "CARD NOT SAVED".
1302
+ adapter.on("challenge_dismissed", () => {
1303
+ showOverlay(null);
1304
+ })
1305
+ );
1306
+ };
1307
+ (0, import_vue2.onMounted)(async () => {
1308
+ const billingApiUrl = (0, import_shared.resolveBillingApiUrl)(props.billingApiUrl);
1309
+ const api = new import_card_setup.PaymentAPI(billingApiUrl, { telemetry: props.telemetry });
1310
+ try {
1311
+ if (!sessionId().trim()) {
1312
+ throw new import_shared.FloPayError("A setup surface requires a sessionId.", "validation_error", {
1313
+ code: "MissingCheckoutSessionId",
1314
+ param: "sessionId"
1315
+ });
1316
+ }
1317
+ if (!props.nonce?.trim()) {
1318
+ throw new import_shared.FloPayError("A setup surface requires a session nonce.", "validation_error", {
1319
+ code: "MissingCheckoutSessionToken",
1320
+ param: "nonce"
1321
+ });
1322
+ }
1323
+ const result = await api.getUnifiedCheckoutSession(sessionId(), props.nonce);
1324
+ const read = result.data.session;
1325
+ if (!read || !(0, import_shared.isCardSetupCheckoutSession)(read)) {
1326
+ throw new import_shared.FloPayError(
1327
+ "Only a zero-amount, productless setup session is accepted.",
1328
+ "validation_error",
1329
+ { code: "InvalidCardSetupSession", param: "sessionId" }
1330
+ );
1331
+ }
1332
+ if (!active) return;
1333
+ session.value = result;
1334
+ if (!options.requireVault(result)) return;
1335
+ let block = read.vault;
1336
+ if (!(0, import_shared.isMountableVaultBlock)(block))
1337
+ block = await api.getVaultCapture(sessionId(), props.nonce);
1338
+ const mount = (0, import_shared.resolveVaultMountOptions)(block, {
1339
+ billingApiUrl,
1340
+ sessionId: sessionId(),
1341
+ nonce: props.nonce
1342
+ });
1343
+ if (!mount) {
1344
+ throw new import_shared.FloPayError(
1345
+ "FloPay: this setup session has no vault card capability. Upgrade the billing backend integration.",
1346
+ "api_error",
1347
+ { code: "UnsupportedBackendVaultCapability" }
1348
+ );
1349
+ }
1350
+ if (!active) return;
1351
+ const adapter = new import_card_setup.PciVaultCardCapture({
1352
+ sessionId: sessionId(),
1353
+ operation: "card_setup",
1354
+ telemetry: props.telemetry
1355
+ });
1356
+ bind(adapter);
1357
+ vault.value = mount;
1358
+ capture.value = adapter;
1359
+ } catch (error) {
1360
+ if (!active) return;
1361
+ terminal = true;
1362
+ const failure = asCardSetupError(error);
1363
+ loadError.value = failure;
1364
+ (0, import_shared.invokeMerchantCallback)(() => emitters.error(failure));
1365
+ } finally {
1366
+ api.destroy();
1367
+ }
1368
+ });
1369
+ (0, import_vue2.onBeforeUnmount)(() => {
1370
+ active = false;
1371
+ if (clearTimer) clearTimeout(clearTimer);
1372
+ clearTimer = null;
1373
+ for (const off of offs.splice(0)) off();
1374
+ if (!terminal && sessionId()) {
1375
+ (0, import_shared.invokeMerchantCallback)(
1376
+ () => emitters.cancel({ status: "cancelled", sessionId: sessionId() })
1377
+ );
1378
+ }
1379
+ });
1380
+ return {
1381
+ capture,
1382
+ vault,
1383
+ session,
1384
+ loadError,
1385
+ settle: () => {
1386
+ terminal = true;
1387
+ },
1388
+ widgetFailed: (message) => {
1389
+ if (!submissionInFlight) return;
1390
+ submissionInFlight = false;
1391
+ holdFailure(message);
1392
+ }
1393
+ };
1394
+ }
1395
+
1396
+ // src/vault-card-fields.ts
1397
+ var import_shared2 = require("@flopay/shared");
1398
+ var import_vue3 = require("vue");
1399
+ var labelledCaptures = /* @__PURE__ */ new WeakSet();
1400
+ var VaultCardFields = (0, import_vue3.defineComponent)({
1401
+ name: "VaultCardFields",
1402
+ props: {
1403
+ capture: { type: Object, default: void 0 },
1404
+ html: { type: String, default: void 0 },
1405
+ messageToken: { type: String, default: void 0 },
1406
+ expectedOrigin: { type: String, default: void 0 },
1407
+ verificationRequired: { type: Boolean, default: void 0 },
1408
+ theme: { type: Object, default: void 0 },
1409
+ submitLabel: { type: String, default: void 0 },
1410
+ containerStyle: { type: Object, default: void 0 }
1411
+ },
1412
+ emits: {
1413
+ ready: () => true,
1414
+ error: (_message) => true,
1415
+ validation: (_message) => true
1416
+ },
1417
+ setup(props, { emit }) {
1418
+ const container = (0, import_vue3.ref)(null);
1419
+ let teardown = null;
1420
+ let appliedLabel;
1421
+ let appliedTheme = null;
1422
+ const mountWidget = () => {
1423
+ teardown?.();
1424
+ teardown = null;
1425
+ const el = container.value;
1426
+ const { capture, html } = props;
1427
+ if (!el || !capture || !html) return;
1428
+ let active = true;
1429
+ let readyEmitted = false;
1430
+ const emitReadyOnce = () => {
1431
+ if (!active || readyEmitted) return;
1432
+ readyEmitted = true;
1433
+ (0, import_shared2.invokeMerchantCallback)(() => emit("ready"));
1434
+ };
1435
+ const offReady = capture.on("ready", emitReadyOnce);
1436
+ const offError = capture.on("error", (event) => {
1437
+ const message = event.message ?? "There was a problem loading the secure card form.";
1438
+ (0, import_shared2.invokeMerchantCallback)(() => emit("error", message));
1439
+ });
1440
+ const offValidation = capture.on("validation", (event) => {
1441
+ (0, import_shared2.invokeMerchantCallback)(() => emit("validation", event.message ?? null));
1442
+ });
1443
+ const label = props.submitLabel ?? void 0;
1444
+ const labelDirective = label ?? (labelledCaptures.has(capture) ? null : void 0);
1445
+ appliedLabel = label;
1446
+ appliedTheme = props.theme ?? null;
1447
+ if (label !== void 0) labelledCaptures.add(capture);
1448
+ capture.mount(el, {
1449
+ html,
1450
+ ...props.messageToken ? { messageToken: props.messageToken } : {},
1451
+ ...props.expectedOrigin ? { expectedOrigin: props.expectedOrigin } : {},
1452
+ ...props.verificationRequired ? { verificationRequired: true } : {},
1453
+ ...props.theme ? { theme: props.theme } : {},
1454
+ ...labelDirective === void 0 ? {} : { submitLabel: labelDirective }
1455
+ }).then(emitReadyOnce).catch((error) => {
1456
+ if (!active) return;
1457
+ const message = error instanceof Error ? error.message : "Failed to load the secure card form.";
1458
+ (0, import_shared2.invokeMerchantCallback)(() => emit("error", message));
1459
+ });
1460
+ teardown = () => {
1461
+ active = false;
1462
+ offReady();
1463
+ offError();
1464
+ offValidation();
1465
+ capture.unmount();
1466
+ };
1467
+ };
1468
+ (0, import_vue3.onMounted)(() => {
1469
+ mountWidget();
1470
+ (0, import_vue3.watch)(
1471
+ () => [
1472
+ props.capture,
1473
+ props.html,
1474
+ props.messageToken,
1475
+ props.expectedOrigin,
1476
+ props.verificationRequired
1477
+ ],
1478
+ mountWidget
1479
+ );
1480
+ (0, import_vue3.watch)(
1481
+ () => props.theme,
1482
+ (theme) => {
1483
+ if (theme === void 0) return;
1484
+ if (theme === null && appliedTheme === null) return;
1485
+ appliedTheme = theme;
1486
+ props.capture?.applyTheme?.(theme);
1487
+ }
1488
+ );
1489
+ (0, import_vue3.watch)(
1490
+ () => props.submitLabel ?? void 0,
1491
+ (label) => {
1492
+ if (!props.capture || label === appliedLabel) return;
1493
+ appliedLabel = label;
1494
+ if (label !== void 0) labelledCaptures.add(props.capture);
1495
+ props.capture.setSubmitLabel?.(label ?? null);
1496
+ }
1497
+ );
1498
+ });
1499
+ (0, import_vue3.onBeforeUnmount)(() => {
1500
+ teardown?.();
1501
+ teardown = null;
1502
+ });
1503
+ return () => (0, import_vue3.h)("div", {
1504
+ ref: container,
1505
+ "data-testid": "flopay-vault-card-fields",
1506
+ style: props.containerStyle
1507
+ });
1508
+ }
1509
+ });
1510
+
1511
+ // src/flopay-payment-method-setup-main.ts
1512
+ var VERIFY_ATTEMPTS = 15;
1513
+ var VERIFY_INTERVAL_MS = 2e3;
1514
+ function delay(ms) {
1515
+ return new Promise((resolve) => setTimeout(resolve, ms));
1516
+ }
1517
+ var StripeSetupMethods = (0, import_vue7.defineAsyncComponent)(
1518
+ () => Promise.resolve().then(() => (init_stripe_elements(), stripe_elements_exports)).then((module2) => module2.StripePaymentMethods)
1519
+ );
1520
+ var PayPalSetupMethods = (0, import_vue7.defineAsyncComponent)(
1521
+ () => Promise.resolve().then(() => (init_payment_method_setup_paypal(), payment_method_setup_paypal_exports)).then((module2) => module2.PayPalSetupMethod)
1522
+ );
1523
+ var FloPayPaymentMethodSetup = (0, import_vue7.defineComponent)({
1524
+ name: "FloPayPaymentMethodSetup",
1525
+ props: {
1526
+ sessionId: { type: String, default: void 0 },
1527
+ nonce: { type: String, default: void 0 },
1528
+ billingApiUrl: { type: String, default: void 0 },
1529
+ telemetry: { type: Boolean, default: void 0 },
1530
+ theme: {
1531
+ type: [String, Object],
1532
+ default: "modern-light"
1533
+ },
1534
+ containerStyle: {
1535
+ type: Object,
1536
+ default: void 0
1537
+ },
1538
+ appearance: { type: Object, default: void 0 },
1539
+ buttonsStyles: { type: Object, default: void 0 }
1540
+ },
1541
+ emits: {
1542
+ ready: () => true,
1543
+ complete: (_event) => true,
1544
+ cancel: (_event) => true,
1545
+ decline: (_event) => true,
1546
+ validation: (_message) => true,
1547
+ error: (_error) => true
1548
+ },
1549
+ setup(props, { emit, slots }) {
1550
+ const overlay = (0, import_vue7.shallowRef)(null);
1551
+ const setup = useHostedSetup(
1552
+ props,
1553
+ {
1554
+ complete: (event) => emit("complete", event),
1555
+ decline: (event) => emit("decline", event),
1556
+ cancel: (event) => emit("cancel", event),
1557
+ error: (error) => emit("error", error),
1558
+ // The hosted card's outcome overlay, in card wording (React's `'card'`).
1559
+ overlay: (state) => {
1560
+ showOverlay(state ? { ...state, wording: "card" } : null);
1561
+ }
1562
+ },
1563
+ { requireVault: (session) => (0, import_shared7.readSetupAdvertisement)(session).card }
1564
+ );
1565
+ const theme = useCardSetupTheme(props);
1566
+ const verified = (0, import_vue7.ref)(false);
1567
+ let mounted = false;
1568
+ let submissionInFlight = false;
1569
+ let completionEmitted = false;
1570
+ let verificationGeneration = 0;
1571
+ let overlayTimer = null;
1572
+ const sessionId = () => props.sessionId ?? "";
1573
+ const showOverlay = (next) => {
1574
+ if (overlayTimer) clearTimeout(overlayTimer);
1575
+ overlayTimer = null;
1576
+ overlay.value = next;
1577
+ };
1578
+ const clearOverlayLater = () => {
1579
+ overlayTimer = setTimeout(() => {
1580
+ overlayTimer = null;
1581
+ if (mounted) overlay.value = null;
1582
+ }, PROCESSING_OVERLAY_ERROR_DELAY_MS);
1583
+ };
1584
+ (0, import_vue7.onMounted)(() => {
1585
+ mounted = true;
1586
+ });
1587
+ (0, import_vue7.onBeforeUnmount)(() => {
1588
+ mounted = false;
1589
+ verificationGeneration += 1;
1590
+ if (overlayTimer) clearTimeout(overlayTimer);
1591
+ overlayTimer = null;
1592
+ });
1593
+ const reportVerified = () => {
1594
+ setup.settle();
1595
+ submissionInFlight = false;
1596
+ verified.value = true;
1597
+ showOverlay({ status: "success", wording: "payment_method", message: null });
1598
+ setTimeout(() => {
1599
+ if (completionEmitted) return;
1600
+ completionEmitted = true;
1601
+ (0, import_shared7.invokeMerchantCallback)(
1602
+ () => emit("complete", { status: "succeeded", sessionId: sessionId() })
1603
+ );
1604
+ if (mounted) overlay.value = null;
1605
+ }, PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
1606
+ };
1607
+ const reportDecline = (event) => {
1608
+ setup.settle();
1609
+ submissionInFlight = false;
1610
+ showOverlay({ status: "error", wording: "payment_method", message: event.message ?? null });
1611
+ (0, import_shared7.invokeMerchantCallback)(
1612
+ () => emit("decline", {
1613
+ status: "declined",
1614
+ sessionId: sessionId(),
1615
+ ...event.reason ? { reason: event.reason } : {},
1616
+ ...event.message ? { message: event.message } : {}
1617
+ })
1618
+ );
1619
+ clearOverlayLater();
1620
+ };
1621
+ const reportFailure = (error) => {
1622
+ if (submissionInFlight) {
1623
+ submissionInFlight = false;
1624
+ showOverlay({ status: "error", wording: "payment_method", message: error.message });
1625
+ clearOverlayLater();
1626
+ } else {
1627
+ showOverlay(null);
1628
+ }
1629
+ (0, import_shared7.invokeMerchantCallback)(() => emit("error", error));
1630
+ };
1631
+ const verifySetup = async (options = {}) => {
1632
+ verificationGeneration += 1;
1633
+ const generation = verificationGeneration;
1634
+ const stale = () => !mounted || generation !== verificationGeneration;
1635
+ submissionInFlight = true;
1636
+ showOverlay({ status: "processing", wording: "payment_method", message: null });
1637
+ const api = new import_card_setup4.PaymentAPI((0, import_shared7.resolveBillingApiUrl)(props.billingApiUrl), {
1638
+ telemetry: props.telemetry
1639
+ });
1640
+ try {
1641
+ for (let attempt = 0; attempt < VERIFY_ATTEMPTS; attempt += 1) {
1642
+ if (attempt > 0 || options.waitFirst) await delay(VERIFY_INTERVAL_MS);
1643
+ if (stale()) return;
1644
+ const result = await api.getUnifiedCheckoutSession(sessionId(), props.nonce);
1645
+ if (stale()) return;
1646
+ const status = result.data.session?.status;
1647
+ if (status === "complete") {
1648
+ reportVerified();
1649
+ return;
1650
+ }
1651
+ if (status === "expired") {
1652
+ throw new import_shared7.FloPayError(
1653
+ "This setup session expired before the payment method was saved.",
1654
+ "validation_error",
1655
+ { code: "payment_method_setup_expired" }
1656
+ );
1657
+ }
1658
+ }
1659
+ throw new import_shared7.FloPayError(
1660
+ "The payment method could not be confirmed yet. Please try again.",
1661
+ "api_error",
1662
+ { code: "payment_method_setup_unverified" }
1663
+ );
1664
+ } catch (error) {
1665
+ if (stale()) return;
1666
+ const unverified = error instanceof import_shared7.FloPayError && error.code === "payment_method_setup_unverified";
1667
+ reportFailure(asCardSetupError(error, unverified ? { retryable: true } : {}));
1668
+ } finally {
1669
+ api.destroy();
1670
+ }
1671
+ };
1672
+ const redirectReturn = (0, import_card_setup4.readSetupRedirectReturn)();
1673
+ if (redirectReturn) {
1674
+ const stopRedirectWatch = (0, import_vue7.watch)(
1675
+ () => setup.session.value,
1676
+ (normalized) => {
1677
+ if (!normalized) return;
1678
+ stopRedirectWatch();
1679
+ (0, import_card_setup4.clearSetupRedirectParams)();
1680
+ if (normalized.data.session?.status === "complete") {
1681
+ reportVerified();
1682
+ return;
1683
+ }
1684
+ if (redirectReturn.failed) {
1685
+ reportDecline({ reason: "redirect_failed" });
1686
+ return;
1687
+ }
1688
+ void verifySetup({ waitFirst: true });
1689
+ }
1690
+ );
1691
+ }
1692
+ const overlayNode = () => {
1693
+ const state = overlay.value;
1694
+ if (!state) return null;
1695
+ return (0, import_vue7.h)(ProcessingOverlay, {
1696
+ status: state.status,
1697
+ errorMessage: state.message,
1698
+ ...(0, import_shared7.setupOverlayLabels)(state.wording),
1699
+ successNote: null,
1700
+ ...theme.overlayTheme.value ? { theme: theme.overlayTheme.value } : {}
1701
+ });
1702
+ };
1703
+ return () => {
1704
+ if (setup.loadError.value) {
1705
+ return (0, import_vue7.h)("div", { role: "alert" }, setup.loadError.value.message);
1706
+ }
1707
+ const normalized = setup.session.value;
1708
+ if (!normalized) return slots.loading?.() ?? null;
1709
+ if (verified.value) {
1710
+ return (0, import_vue7.h)("div", { "data-testid": "flopay-payment-method-setup" }, [overlayNode()]);
1711
+ }
1712
+ const advertisement = (0, import_shared7.readSetupAdvertisement)(normalized);
1713
+ const session = normalized.data.session ?? null;
1714
+ const billingApiUrl = (0, import_shared7.resolveBillingApiUrl)(props.billingApiUrl);
1715
+ const stripeBranch = (entry) => (0, import_vue7.h)("div", { key: entry.method, "data-flopay-payment-method": entry.method }, [
1716
+ (0, import_vue7.h)(StripeSetupMethods, {
1717
+ session,
1718
+ sessionId: sessionId(),
1719
+ nonce: props.nonce,
1720
+ billingApiUrl,
1721
+ publishableKey: entry.publishableKey,
1722
+ methods: [entry.method],
1723
+ intentKind: "setup",
1724
+ currency: entry.currency,
1725
+ ...theme.appearance.value ? { appearance: theme.appearance.value } : {},
1726
+ // The provider said yes; wait for the backend to agree.
1727
+ onComplete: () => {
1728
+ void verifySetup();
1729
+ },
1730
+ onDecline: (decline) => reportDecline({
1731
+ ...decline.code ? { reason: decline.code } : {},
1732
+ ...decline.message ? { message: decline.message } : {}
1733
+ }),
1734
+ onError: (error) => reportFailure(asCardSetupError(error))
1735
+ })
1736
+ ]);
1737
+ const paypal = advertisement.paypal;
1738
+ const paypalBranch = paypal ? (0, import_vue7.h)("div", { key: "paypal", "data-flopay-payment-method": "paypal" }, [
1739
+ (0, import_vue7.h)(PayPalSetupMethods, {
1740
+ clientId: paypal.clientId,
1741
+ ...paypal.environment ? { environment: paypal.environment } : {},
1742
+ ...advertisement.currency ? { currency: advertisement.currency } : {},
1743
+ sessionId: sessionId(),
1744
+ nonce: props.nonce ?? "",
1745
+ billingApiUrl,
1746
+ telemetry: props.telemetry,
1747
+ customer: {
1748
+ userId: session?.customer?.id ?? "",
1749
+ email: session?.customer?.email ?? ""
1750
+ },
1751
+ onSubmitting: () => {
1752
+ submissionInFlight = true;
1753
+ showOverlay({ status: "processing", wording: "payment_method", message: null });
1754
+ },
1755
+ onConfirmed: () => {
1756
+ void verifySetup();
1757
+ },
1758
+ onFailed: (error, options) => reportFailure(asCardSetupError(error, options)),
1759
+ // Closing PayPal's window is not an outcome: no cancel, decline or error.
1760
+ onAbandoned: () => {
1761
+ submissionInFlight = false;
1762
+ showOverlay(null);
1763
+ }
1764
+ })
1765
+ ]) : null;
1766
+ const capture = setup.capture.value;
1767
+ const vault = setup.vault.value;
1768
+ const card = advertisement.card && capture && vault?.html ? (0, import_vue7.h)("div", { key: "card", "data-flopay-payment-method": "card" }, [
1769
+ (0, import_vue7.h)(VaultCardFields, {
1770
+ capture,
1771
+ html: vault.html,
1772
+ messageToken: vault.messageToken,
1773
+ expectedOrigin: vault.expectedOrigin,
1774
+ verificationRequired: vault.verificationRequired,
1775
+ theme: theme.vaultTheme.value,
1776
+ containerStyle: theme.surfaceStyle.value,
1777
+ onReady: () => (0, import_shared7.invokeMerchantCallback)(() => emit("ready")),
1778
+ onValidation: (message) => (0, import_shared7.invokeMerchantCallback)(() => emit("validation", message)),
1779
+ onError: (message) => {
1780
+ if (!message) return;
1781
+ setup.widgetFailed(message);
1782
+ const error = asCardSetupError(new Error(message), {
1783
+ code: "card_setup_widget_failed",
1784
+ retryable: true
1785
+ });
1786
+ (0, import_shared7.invokeMerchantCallback)(() => emit("error", error));
1787
+ }
1788
+ })
1789
+ ]) : null;
1790
+ const wallets = advertisement.stripe.filter(
1791
+ ({ method }) => (0, import_shared7.stripeMethodCategory)(method) === "wallet"
1792
+ );
1793
+ const tiles = advertisement.stripe.filter(
1794
+ ({ method }) => (0, import_shared7.stripeMethodCategory)(method) !== "wallet"
1795
+ );
1796
+ return (0, import_vue7.h)("div", { "data-testid": "flopay-payment-method-setup" }, [
1797
+ overlayNode(),
1798
+ ...wallets.map(stripeBranch),
1799
+ paypalBranch,
1800
+ ...tiles.map(stripeBranch),
1801
+ card
1802
+ ]);
1803
+ };
1804
+ }
1805
+ });
1806
+ // Annotate the CommonJS export names for ESM import in node:
1807
+ 0 && (module.exports = {
1808
+ FloPayPaymentMethodSetup
1809
+ });