@flopay/vue 0.0.0-stage → 1.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,3196 @@
1
+ import {
2
+ FloPayCardSetup
3
+ } from "./chunk-DUU6SMKP.mjs";
4
+ import {
5
+ FloPayPaymentMethodSetup
6
+ } from "./chunk-2PLBNCM6.mjs";
7
+ import {
8
+ FloPayCardSetupError,
9
+ VaultCardFields
10
+ } from "./chunk-W52CQTJI.mjs";
11
+ import {
12
+ CHECKOUT_FORM_DEFAULTS,
13
+ CHECKOUT_STATE,
14
+ FLOPAY_CONTEXT,
15
+ FloPayProvider,
16
+ StripePaymentMethods,
17
+ hasStripePayPalReturn
18
+ } from "./chunk-VXYNYMHI.mjs";
19
+
20
+ // src/index.ts
21
+ import { dropThirdPartyOnlyError } from "@flopay/shared";
22
+
23
+ // src/automatic-payment-button.ts
24
+ import { PaymentAPI as PaymentAPI4 } from "@flopay/js";
25
+ import {
26
+ darkenHex,
27
+ derivePrimaryTileStyle as derivePrimaryTileStyle2,
28
+ FloPayError as FloPayError5,
29
+ invokeMerchantCallback as invokeMerchantCallback4,
30
+ resolveBillingApiUrl as resolveBillingApiUrl4,
31
+ resolveButtonsLayoutTheme as resolveButtonsLayoutTheme2,
32
+ resolveTheme as resolveTheme3
33
+ } from "@flopay/shared";
34
+ import { computed as computed3, defineComponent as defineComponent5, h as h5, onBeforeUnmount as onBeforeUnmount5, ref as ref5 } from "vue";
35
+
36
+ // src/flopay-checkout.ts
37
+ import { loadFloPay, PaymentAPI as PaymentAPI3 } from "@flopay/js";
38
+ import {
39
+ buildCheckoutDisplayData,
40
+ clearCachedInlineCheckout,
41
+ clearCachedInlineSession,
42
+ FloPayError as FloPayError4,
43
+ hashCreateParams,
44
+ invokeMerchantCallback as invokeMerchantCallback3,
45
+ isDetachedSessionEligible,
46
+ legacyCachedSessionMatchesDraft,
47
+ markSessionRecentlyCompleted as markSessionRecentlyCompleted2,
48
+ persistCachedInlineIdempotencyKey,
49
+ persistCachedInlineSession,
50
+ readCachedInlineIdempotencyKey,
51
+ readCachedInlineSession,
52
+ resolveBillingApiUrl as resolveBillingApiUrl3,
53
+ resolveCheckoutLogLayout,
54
+ resolveDirectPaypalConfig,
55
+ resolveIdempotencyKey,
56
+ resolveTheme as resolveTheme2,
57
+ serializeCreateParams,
58
+ wasSessionRecentlyCompleted
59
+ } from "@flopay/shared";
60
+ import {
61
+ defineComponent as defineComponent4,
62
+ h as h4,
63
+ onBeforeUnmount as onBeforeUnmount4,
64
+ onMounted as onMounted3,
65
+ provide,
66
+ reactive,
67
+ ref as ref4,
68
+ shallowRef as shallowRef3,
69
+ watch as watch3
70
+ } from "vue";
71
+
72
+ // src/saved-payment.ts
73
+ import { completeSavedCardNextAction } from "@flopay/js";
74
+ import { FloPayError, normalizeCheckoutFailureOutcome } from "@flopay/shared";
75
+ var SAVED_PAYMENT_SUCCESS_DELAY_MS = 600;
76
+ function isTerminalSuccess(session) {
77
+ return session?.status === "authorized" || session?.status === "complete";
78
+ }
79
+ function terminalResult(session, sessionId) {
80
+ if (session.status !== "authorized") return { status: "succeeded" };
81
+ return {
82
+ status: "authorized",
83
+ paymentId: session.paymentId,
84
+ sessionId: session.id || sessionId || void 0,
85
+ authorizationExpiresAt: session.authorizationExpiresAt
86
+ };
87
+ }
88
+ function completionSession(completion, prior) {
89
+ if (completion.data.session) return completion.data.session;
90
+ const raw = completion.raw;
91
+ const attempt = raw?.latestTransactionAttempt?.result;
92
+ if (raw?.status === "authorized" || attempt === "AUTHORIZED") {
93
+ return { ...prior, status: "authorized" };
94
+ }
95
+ if (raw?.status === "completed" || attempt === "SUCCESS") {
96
+ return { ...prior, status: "complete" };
97
+ }
98
+ return null;
99
+ }
100
+ function processErrorToFloPayError(error, fallback) {
101
+ return new FloPayError(error.message ?? fallback, "api_error", {
102
+ code: error.code ?? normalizeCheckoutFailureOutcome(error.outcome) ?? normalizeCheckoutFailureOutcome(error.type) ?? error.gatewayErrorCode
103
+ });
104
+ }
105
+ function readErrorCode(error) {
106
+ const code = error?.code;
107
+ return typeof code === "string" ? code : void 0;
108
+ }
109
+ function coerceError(error, fallback) {
110
+ if (error instanceof FloPayError) return error;
111
+ return new FloPayError(error instanceof Error ? error.message : fallback, "api_error", {
112
+ ...readErrorCode(error) ? { code: readErrorCode(error) } : {}
113
+ });
114
+ }
115
+ async function chargeSavedPayment(options) {
116
+ const { api, session, unified, signal } = options;
117
+ const nonce = options.nonce || session.clientSecret || void 0;
118
+ const completeAction = async (nextActionRedirectUrl, actionNonce) => {
119
+ const completed = await completeSavedCardNextAction(
120
+ {
121
+ billingApiUrl: options.billingApiUrl,
122
+ sessionId: session.id,
123
+ nonce: actionNonce,
124
+ nextActionRedirectUrl,
125
+ ...signal ? { signal } : {}
126
+ },
127
+ api
128
+ );
129
+ if (completed.autoProcessingError) {
130
+ throw processErrorToFloPayError(
131
+ completed.autoProcessingError,
132
+ "Card authentication was not completed."
133
+ );
134
+ }
135
+ const settled = completionSession(completed, session);
136
+ if (!isTerminalSuccess(settled)) {
137
+ throw new FloPayError("Card authentication was not completed.", "api_error");
138
+ }
139
+ return { ...terminalResult(settled, session.id), checkoutMethod: "card" };
140
+ };
141
+ const settleCompletion = async (completed) => {
142
+ const failure = completed.autoProcessingError;
143
+ if (failure?.type === "3ds_required" && failure.nextActionRedirectUrl && nonce) {
144
+ return completeAction(failure.nextActionRedirectUrl, nonce);
145
+ }
146
+ if (failure) {
147
+ throw processErrorToFloPayError(failure, "Automatic payment failed. Please try again.");
148
+ }
149
+ const settled = completionSession(completed, session);
150
+ if (!isTerminalSuccess(settled)) {
151
+ throw new FloPayError("Automatic payment failed. Please try again.", "api_error", {
152
+ ...settled?.status === "expired" ? { code: "session_expired" } : {}
153
+ });
154
+ }
155
+ return terminalResult(settled, session.id);
156
+ };
157
+ if (unified?.autoProcessingPending || unified?.autoProcessingError) {
158
+ const pending = unified.autoProcessingPending;
159
+ const completed = pending ? await api.waitForCheckoutSessionCompletion(pending.sessionId || session.id, {
160
+ initialDelayMs: pending.retryAfterMs,
161
+ nonce,
162
+ statusUrl: pending.statusUrl,
163
+ ...signal ? { signal } : {}
164
+ }) : unified;
165
+ return settleCompletion(completed);
166
+ }
167
+ const customer = session.customer;
168
+ const response = await api.processPayment(customer?.id ?? "", {
169
+ sessionId: session.id,
170
+ nonce,
171
+ paymentMethodCategory: "saved",
172
+ accountData: {
173
+ userId: customer?.id ?? "",
174
+ email: customer?.email ?? "",
175
+ firstName: customer?.firstName ?? "",
176
+ lastName: customer?.lastName ?? "",
177
+ country: customer?.country ?? void 0,
178
+ zip: customer?.zip ?? void 0
179
+ },
180
+ returnUrl: typeof window === "undefined" ? void 0 : window.location.href
181
+ });
182
+ const body = await response.json().catch(() => null);
183
+ if (response.status === 202) {
184
+ const completed = await api.waitForCheckoutSessionCompletion(session.id, {
185
+ ...typeof body?.retryAfterMs === "number" ? { initialDelayMs: body.retryAfterMs } : {},
186
+ ...typeof body?.statusUrl === "string" ? { statusUrl: body.statusUrl } : {},
187
+ nonce,
188
+ ...signal ? { signal } : {}
189
+ });
190
+ return settleCompletion(completed);
191
+ }
192
+ if (response.ok) {
193
+ const authorized = body?.status === "authorized" || session.captureMethod === "manual";
194
+ return authorized ? { status: "authorized", paymentId: body?.paymentId ?? session.paymentId } : { status: "succeeded" };
195
+ }
196
+ if (body?.type === "3ds_required" && typeof body.nextActionRedirectUrl === "string" && nonce) {
197
+ return completeAction(body.nextActionRedirectUrl, nonce);
198
+ }
199
+ throw new FloPayError(
200
+ body?.message ?? "Payment failed. Please try again.",
201
+ "api_error",
202
+ {
203
+ code: body?.code ?? body?.gatewayErrorCode,
204
+ declineCode: body?.declineCode ?? body?.gatewayDeclineReason,
205
+ statusCode: response.status
206
+ }
207
+ );
208
+ }
209
+
210
+ // src/split-card-form.ts
211
+ import { PaymentAPI as PaymentAPI2, PciVaultCardCapture } from "@flopay/js/card-setup";
212
+ import {
213
+ buildCheckoutProcessMetadata,
214
+ buildProcessPaymentAvsAccount,
215
+ COUNTRY_OPTIONS,
216
+ canRecoverHostedCardForm,
217
+ computeVaultCardThemeColors,
218
+ derivePrimaryTileStyle,
219
+ detectPayPalEnvironment as detectPayPalEnvironment2,
220
+ FloPayError as FloPayError3,
221
+ filterStripeMethodsByCountryGate,
222
+ getStateFromPostalCode,
223
+ invokeMerchantCallback as invokeMerchantCallback2,
224
+ isAVSFieldVisible,
225
+ isMountableVaultBlock,
226
+ isValidEmail as isValidEmail3,
227
+ isValidPostalCode,
228
+ markSessionRecentlyCompleted,
229
+ normalizeBeforeButtonClickError,
230
+ resolveAVSConfig,
231
+ resolveBillingApiUrl as resolveBillingApiUrl2,
232
+ resolveButtonsLayoutTheme,
233
+ resolveStripePayPalPresentation,
234
+ resolveTheme,
235
+ resolveVaultMountOptions,
236
+ STRIPE_EXPRESS_METHODS,
237
+ WALLET_SHEET_WINDOW_TOO_SMALL_CODE
238
+ } from "@flopay/shared";
239
+ import {
240
+ Comment,
241
+ computed as computed2,
242
+ defineComponent as defineComponent3,
243
+ Fragment,
244
+ h as h3,
245
+ inject as inject2,
246
+ onBeforeUnmount as onBeforeUnmount3,
247
+ onMounted as onMounted2,
248
+ ref as ref3,
249
+ shallowRef as shallowRef2,
250
+ Text,
251
+ watch as watch2
252
+ } from "vue";
253
+
254
+ // src/direct-paypal-button.ts
255
+ import { PaymentAPI } from "@flopay/js/card-setup";
256
+ import {
257
+ applyFriendlyMessageOverride,
258
+ buildExternalMethodRecoveryMessage,
259
+ buildPayPalProcessPaymentRequest,
260
+ buildPayPalSessionIntentRequest,
261
+ buildPayPalTokenizedBody,
262
+ classifyExternalMethodFailure,
263
+ compactBuyerIdentity as compactBuyerIdentity2,
264
+ describePayPalProcessFailure,
265
+ detectPayPalEnvironment,
266
+ FloPayError as FloPayError2,
267
+ friendlyPayPalFailureMessage,
268
+ generateIdempotencyKey,
269
+ invokeMerchantCallback,
270
+ isPayPalAcknowledgementDeadWindow,
271
+ isPayPalTeardownMessage,
272
+ isValidEmail as isValidEmail2,
273
+ mergePayPalPayerIdentity,
274
+ needsPayPalPayerDetails,
275
+ normalizeGatewayEnvironment,
276
+ paypalProviderIdentityRequired,
277
+ resolveBillingApiUrl,
278
+ resolvePayPalApprovalIdentity,
279
+ resolvePayPalDeclineReason,
280
+ resolvePayPalPresentationMode
281
+ } from "@flopay/shared";
282
+ import { loadScript } from "@paypal/paypal-js";
283
+ import {
284
+ computed,
285
+ defineComponent as defineComponent2,
286
+ h as h2,
287
+ inject,
288
+ onBeforeUnmount as onBeforeUnmount2,
289
+ onMounted,
290
+ ref as ref2,
291
+ watch
292
+ } from "vue";
293
+
294
+ // src/buyer-identity-fallback.ts
295
+ import { compactBuyerIdentity, isValidEmail } from "@flopay/shared";
296
+ import { defineComponent, h, onBeforeUnmount, ref, shallowRef } from "vue";
297
+ function createBuyerIdentityFallback() {
298
+ const request = shallowRef(null);
299
+ let resolver = null;
300
+ let requestId = 0;
301
+ const collect = (methodLabel, identity) => {
302
+ resolver?.(null);
303
+ requestId += 1;
304
+ request.value = { id: requestId, methodLabel, identity };
305
+ return new Promise((resolve) => {
306
+ resolver = resolve;
307
+ });
308
+ };
309
+ const finish = (identity) => {
310
+ const resolve = resolver;
311
+ resolver = null;
312
+ request.value = null;
313
+ resolve?.(identity);
314
+ };
315
+ onBeforeUnmount(() => {
316
+ resolver?.(null);
317
+ resolver = null;
318
+ });
319
+ return { request, collect, finish };
320
+ }
321
+ var inputStyle = {
322
+ minHeight: "42px",
323
+ padding: "0 0.75rem",
324
+ border: "1px solid #d1d5db",
325
+ borderRadius: "6px"
326
+ };
327
+ var BuyerIdentityFallbackPanel = defineComponent({
328
+ name: "BuyerIdentityFallbackPanel",
329
+ props: {
330
+ identity: { type: Object, required: true },
331
+ methodLabel: { type: String, required: true },
332
+ /** Renders a Cancel button (React renders it when `onCancel` is passed). */
333
+ cancellable: { type: Boolean, default: false },
334
+ submitting: { type: Boolean, default: false },
335
+ testId: { type: String, default: "flopay-buyer-identity-pane" }
336
+ },
337
+ emits: {
338
+ complete: (_identity) => true,
339
+ cancel: () => true
340
+ },
341
+ setup(props, { emit }) {
342
+ const email = ref(props.identity.email?.trim() ?? "");
343
+ return () => {
344
+ const valid = isValidEmail(email.value.trim());
345
+ return h(
346
+ "div",
347
+ {
348
+ "data-testid": props.testId,
349
+ style: {
350
+ display: "flex",
351
+ flexDirection: "column",
352
+ gap: "0.75rem",
353
+ padding: "0.875rem",
354
+ border: "1px solid #d1d5db",
355
+ borderRadius: "8px",
356
+ background: "#fff",
357
+ color: "#262833"
358
+ }
359
+ },
360
+ [
361
+ h("div", { style: { fontWeight: 700 } }, `Continue with ${props.methodLabel}`),
362
+ h("label", { style: { display: "flex", flexDirection: "column", gap: "0.375rem" } }, [
363
+ "Email address",
364
+ h("input", {
365
+ type: "email",
366
+ autocomplete: "email",
367
+ "data-testid": "flopay-email",
368
+ required: true,
369
+ value: email.value,
370
+ onInput: (event) => {
371
+ email.value = event.target.value;
372
+ },
373
+ style: inputStyle
374
+ })
375
+ ]),
376
+ h("div", { style: { display: "flex", gap: "0.5rem" } }, [
377
+ props.cancellable ? h(
378
+ "button",
379
+ {
380
+ type: "button",
381
+ onClick: () => emit("cancel"),
382
+ style: { flex: 1, minHeight: "42px" }
383
+ },
384
+ "Cancel"
385
+ ) : null,
386
+ h(
387
+ "button",
388
+ {
389
+ type: "button",
390
+ disabled: !valid || props.submitting,
391
+ onClick: () => emit(
392
+ "complete",
393
+ compactBuyerIdentity({ ...props.identity, email: email.value.trim() })
394
+ ),
395
+ style: { flex: 2, minHeight: "42px" }
396
+ },
397
+ `Continue with ${props.methodLabel}`
398
+ )
399
+ ])
400
+ ]
401
+ );
402
+ };
403
+ }
404
+ });
405
+
406
+ // src/direct-paypal-button.ts
407
+ var PAYPAL_INIT_RETRY_DELAY_MS = 1e3;
408
+ var PayPalStartupFailure = class extends Error {
409
+ constructor(message, options = {}) {
410
+ super(message);
411
+ this.ineligible = options.ineligible ?? false;
412
+ if (options.cause !== void 0) this.cause = options.cause;
413
+ }
414
+ };
415
+ var errorMessage = (error, fallback) => error instanceof Error ? error.message : fallback;
416
+ var debugPanelStyle = (failed) => ({
417
+ margin: "0 0 8px 0",
418
+ padding: "6px 8px",
419
+ background: failed ? "#fef2f2" : "#f3f4f6",
420
+ border: `1px solid ${failed ? "#fca5a5" : "#d1d5db"}`,
421
+ borderRadius: "6px",
422
+ color: "#111827",
423
+ font: "11px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace",
424
+ whiteSpace: "pre-wrap",
425
+ wordBreak: "break-word",
426
+ maxHeight: "220px",
427
+ overflowY: "auto"
428
+ });
429
+ var DirectPayPalButton = defineComponent2({
430
+ name: "DirectPayPalButton",
431
+ props: {
432
+ sessionId: { type: String, default: void 0 },
433
+ nonce: { type: String, default: void 0 },
434
+ billingApiUrl: { type: String, default: void 0 },
435
+ email: { type: String, default: void 0 },
436
+ billingName: { type: String, default: void 0 },
437
+ onBuyerIdentityReady: {
438
+ type: Function,
439
+ default: void 0
440
+ },
441
+ clientId: { type: String, default: void 0 },
442
+ environment: {
443
+ type: String,
444
+ default: void 0
445
+ },
446
+ currency: { type: String, default: void 0 },
447
+ isSubscription: { type: Boolean, default: void 0 },
448
+ providerObjectType: {
449
+ type: String,
450
+ default: void 0
451
+ },
452
+ approvalPresentation: {
453
+ type: String,
454
+ default: void 0
455
+ },
456
+ session: { type: Object, default: null },
457
+ existingOrderId: { type: String, default: void 0 },
458
+ isProcessing: { type: Boolean, default: false },
459
+ allowAutomaticRetry: { type: Boolean, default: true },
460
+ runBeforeButtonClick: {
461
+ type: Function,
462
+ default: void 0
463
+ },
464
+ onTokenizedBody: {
465
+ type: Function,
466
+ default: void 0
467
+ },
468
+ onTechnicalFailure: {
469
+ type: Function,
470
+ default: void 0
471
+ },
472
+ telemetry: { type: Boolean, default: void 0 },
473
+ debug: { type: Boolean, default: false }
474
+ },
475
+ emits: {
476
+ complete: (_result) => true,
477
+ decline: (_decline) => true,
478
+ error: (_error) => true,
479
+ errorChange: (_message) => true,
480
+ buttonClick: (_method) => true,
481
+ loadStateChange: (_ready) => true,
482
+ initializationStateChange: (_state) => true
483
+ },
484
+ setup(props, { emit }) {
485
+ const provider = inject(FLOPAY_CONTEXT, null);
486
+ const container = ref2(null);
487
+ const state = ref2("loading");
488
+ const submitting = ref2(false);
489
+ const debugLines = ref2([]);
490
+ let buttons = null;
491
+ let mounted = false;
492
+ let unmounted = false;
493
+ let generation = 0;
494
+ let retryTimer = null;
495
+ let prepared = null;
496
+ let intentIdempotencyKey;
497
+ let intentAttemptActive = false;
498
+ const beginIntentAttempt = () => {
499
+ intentIdempotencyKey = generateIdempotencyKey();
500
+ if (!intentIdempotencyKey) {
501
+ console.warn(
502
+ "[FloPay] DirectPayPal: secure idempotency key unavailable; retries may not be deduplicated."
503
+ );
504
+ }
505
+ intentAttemptActive = true;
506
+ };
507
+ const {
508
+ request: identityRequest,
509
+ collect: collectBuyerIdentity,
510
+ finish: finishBuyerIdentity
511
+ } = createBuyerIdentityFallback();
512
+ let identityRequirement = {
513
+ sessionId: props.sessionId,
514
+ required: paypalProviderIdentityRequired(props.email, props.session)
515
+ };
516
+ const providerIdentityRequired = () => {
517
+ if (identityRequirement.sessionId !== props.sessionId) {
518
+ identityRequirement = {
519
+ sessionId: props.sessionId,
520
+ required: paypalProviderIdentityRequired(props.email, props.session)
521
+ };
522
+ }
523
+ return identityRequirement.required;
524
+ };
525
+ watch(
526
+ () => props.sessionId,
527
+ () => {
528
+ providerIdentityRequired();
529
+ }
530
+ );
531
+ const isCurrent = (gen) => !unmounted && gen === generation;
532
+ const trace = (line) => {
533
+ if (!props.debug) return;
534
+ debugLines.value = [...debugLines.value, `${(/* @__PURE__ */ new Date()).toISOString().slice(11, 23)} ${line}`];
535
+ };
536
+ watch(
537
+ () => props.debug,
538
+ (on) => {
539
+ if (on) trace("debug:enabled");
540
+ },
541
+ { immediate: true }
542
+ );
543
+ const setState = (next) => {
544
+ state.value = next;
545
+ invokeMerchantCallback(() => emit("initializationStateChange", next));
546
+ };
547
+ const ready = computed(() => state.value === "ready");
548
+ const failed = computed(() => state.value === "exhausted");
549
+ watch(
550
+ [ready, failed],
551
+ ([isReady, isFailed]) => {
552
+ if (!mounted) return;
553
+ invokeMerchantCallback(() => emit("loadStateChange", isReady && !isFailed));
554
+ },
555
+ { flush: "sync" }
556
+ );
557
+ const baseUrl = () => resolveBillingApiUrl(props.billingApiUrl ?? provider?.billingApiUrl() ?? void 0);
558
+ const configuredObjectType = () => props.session?.gateways?.paypal?.providerObjectType ?? props.providerObjectType;
559
+ const objectType = () => configuredObjectType() ?? (props.isSubscription ? "subscription" : "order");
560
+ const advertisedPresentation = () => props.session?.gateways?.paypal?.approvalPresentation ?? props.approvalPresentation;
561
+ const api = () => new PaymentAPI(baseUrl(), { telemetry: false });
562
+ const inAppBrowser = () => detectPayPalEnvironment().inAppBrowser;
563
+ const effectiveSessionId = () => prepared?.sessionId ?? props.sessionId ?? "";
564
+ const effectiveNonce = () => prepared?.nonce ?? props.nonce;
565
+ const createIntent = async () => {
566
+ if (!intentAttemptActive) beginIntentAttempt();
567
+ const intent = await api().createSessionIntent(
568
+ effectiveSessionId(),
569
+ effectiveNonce() ?? "",
570
+ buildPayPalSessionIntentRequest(configuredObjectType()),
571
+ { idempotencyKey: intentIdempotencyKey }
572
+ );
573
+ if (intent.provider !== "paypal" || intent.providerObjectType !== objectType()) {
574
+ throw new FloPayError2("PayPal could not start this payment.", "api_error", {
575
+ code: "paypal_intent_mismatch"
576
+ });
577
+ }
578
+ return intent.providerObjectId;
579
+ };
580
+ const emitDecline = (message, sessionId, codes = {}) => {
581
+ const event = {
582
+ method: "paypal",
583
+ message,
584
+ ...codes.code ? { code: codes.code } : {},
585
+ ...codes.declineCode ? { declineCode: codes.declineCode } : {},
586
+ ...sessionId ? { sessionId } : {}
587
+ };
588
+ invokeMerchantCallback(() => emit("decline", event));
589
+ };
590
+ const decline = (error) => {
591
+ const message = errorMessage(error, "PayPal payment failed.");
592
+ const nonce = effectiveNonce();
593
+ const sessionId = effectiveSessionId();
594
+ if (nonce) {
595
+ void api().reportSessionIntentDecline(sessionId, nonce, {
596
+ provider: "paypal",
597
+ paymentMethodCategory: "wallet",
598
+ paymentMethodType: "paypal",
599
+ providerDeclineReason: resolvePayPalDeclineReason(error)
600
+ }).catch(() => {
601
+ });
602
+ }
603
+ emitDecline(message, sessionId);
604
+ };
605
+ const forwardError = (message) => {
606
+ if (isPayPalTeardownMessage(message)) return;
607
+ const friendly = applyFriendlyMessageOverride(message) ?? message;
608
+ invokeMerchantCallback(() => emit("errorChange", friendly));
609
+ };
610
+ const notifyTechnicalFailure = (error, recoveryMessage, options) => {
611
+ const handler = props.onTechnicalFailure;
612
+ if (handler) {
613
+ const callbackError = options.popupBlocked && inAppBrowser() ? new FloPayError2(recoveryMessage, "api_error", {
614
+ code: options.code ?? "paypal_popup_blocked"
615
+ }) : error;
616
+ invokeMerchantCallback(() => handler("paypal", callbackError, options));
617
+ return;
618
+ }
619
+ invokeMerchantCallback(() => emit("errorChange", recoveryMessage));
620
+ };
621
+ const runtimeFailure = (error) => {
622
+ const message = errorMessage(error, "PayPal failed to render.");
623
+ if (isPayPalTeardownMessage(message)) {
624
+ prepared = null;
625
+ return;
626
+ }
627
+ const classification = classifyExternalMethodFailure(error, {
628
+ inAppBrowser: inAppBrowser(),
629
+ handoffObserved: false
630
+ });
631
+ if (classification.providerDeclined) {
632
+ decline(error);
633
+ prepared = null;
634
+ return;
635
+ }
636
+ prepared = null;
637
+ const recoveryMessage = buildExternalMethodRecoveryMessage(
638
+ "PayPal",
639
+ classification.popupBlocked,
640
+ inAppBrowser()
641
+ );
642
+ const failure = new FloPayError2(recoveryMessage, "api_error", {
643
+ code: classification.popupBlocked ? "paypal_popup_blocked" : "paypal_runtime_failed"
644
+ });
645
+ invokeMerchantCallback(() => emit("error", failure));
646
+ notifyTechnicalFailure(error, recoveryMessage, {
647
+ code: "paypal_runtime_failed",
648
+ popupBlocked: classification.popupBlocked,
649
+ provider: "paypal",
650
+ paymentMethodCategory: "paypal",
651
+ terminalReason: classification.terminalReason
652
+ });
653
+ };
654
+ const tokenizedBody = (token) => buildPayPalTokenizedBody(token, configuredObjectType());
655
+ const processApproval = async (gen, body, attempt) => {
656
+ const sessionId = attempt?.sessionId ?? props.sessionId ?? "";
657
+ const request = buildPayPalProcessPaymentRequest({
658
+ body,
659
+ sessionId,
660
+ nonce: attempt?.nonce ?? props.nonce,
661
+ accountPatch: attempt?.accountPatch,
662
+ session: props.session,
663
+ email: props.email
664
+ });
665
+ let response;
666
+ try {
667
+ response = await api().processPayment(request.userId, request.params);
668
+ } catch (error) {
669
+ if (!isCurrent(gen)) return;
670
+ const message = friendlyPayPalFailureMessage(
671
+ error instanceof Error ? error.message : void 0
672
+ );
673
+ forwardError(message);
674
+ emitDecline(message, sessionId);
675
+ return;
676
+ }
677
+ if (response.ok) {
678
+ if (gen !== generation) return;
679
+ trace("process:ok");
680
+ const result = { status: "succeeded", checkoutMethod: "paypal" };
681
+ invokeMerchantCallback(() => emit("complete", result));
682
+ return;
683
+ }
684
+ if (!isCurrent(gen)) return;
685
+ const json = await response.json().catch(() => null);
686
+ const failure = describePayPalProcessFailure(json);
687
+ trace(`process:rejected status=${response.status}`);
688
+ forwardError(failure.message);
689
+ emitDecline(failure.message, sessionId, {
690
+ code: failure.code,
691
+ declineCode: failure.declineCode
692
+ });
693
+ };
694
+ const resolveApprovalIdentity = async (attempt, actions) => {
695
+ let identity = resolvePayPalApprovalIdentity({
696
+ accountPatch: attempt?.accountPatch,
697
+ email: props.email,
698
+ billingName: props.billingName,
699
+ session: props.session
700
+ });
701
+ if (needsPayPalPayerDetails(identity)) {
702
+ try {
703
+ identity = mergePayPalPayerIdentity(identity, await actions?.order?.get?.());
704
+ } catch {
705
+ }
706
+ }
707
+ if ((props.onBuyerIdentityReady || providerIdentityRequired()) && !isValidEmail2(identity.email)) {
708
+ trace("identity:collect");
709
+ const collected = await collectBuyerIdentity("PayPal", identity);
710
+ if (!collected) return null;
711
+ identity = collected;
712
+ }
713
+ return identity;
714
+ };
715
+ const approve = async (gen, data, actions) => {
716
+ if (!isCurrent(gen)) return;
717
+ const attempt = { ...prepared ?? {} };
718
+ submitting.value = true;
719
+ trace("onApprove");
720
+ invokeMerchantCallback(() => emit("errorChange", null));
721
+ try {
722
+ const token = data.vaultSetupToken ?? data.subscriptionID ?? data.orderID ?? "";
723
+ if (!token) {
724
+ throw new FloPayError2("PayPal did not return an approval token.", "api_error", {
725
+ code: "paypal_missing_token"
726
+ });
727
+ }
728
+ const identity = await resolveApprovalIdentity(attempt, actions);
729
+ if (!identity) {
730
+ trace("identity:cancelled");
731
+ return;
732
+ }
733
+ if (isValidEmail2(identity.email)) {
734
+ attempt.accountPatch = compactBuyerIdentity2({ ...attempt.accountPatch, ...identity });
735
+ await props.onBuyerIdentityReady?.(identity);
736
+ }
737
+ if (!isCurrent(gen)) return;
738
+ const body = tokenizedBody(token);
739
+ const handler = props.onTokenizedBody;
740
+ if (handler) {
741
+ await handler(body, {
742
+ sessionId: attempt.sessionId ?? props.sessionId,
743
+ accountPatch: attempt.accountPatch,
744
+ nonce: attempt.nonce
745
+ });
746
+ return;
747
+ }
748
+ await processApproval(gen, body, attempt);
749
+ } catch (error) {
750
+ if (!isCurrent(gen)) return;
751
+ forwardError(errorMessage(error, "PayPal capture failed."));
752
+ } finally {
753
+ if (!unmounted) submitting.value = false;
754
+ }
755
+ };
756
+ const click = async (gen, actions) => {
757
+ if (!isCurrent(gen)) {
758
+ await actions.reject();
759
+ return;
760
+ }
761
+ beginIntentAttempt();
762
+ prepared = null;
763
+ let next = {};
764
+ const runner = props.runBeforeButtonClick;
765
+ if (runner) {
766
+ try {
767
+ const beforeClick = await runner("paypal");
768
+ if (!isCurrent(gen)) {
769
+ await actions.reject();
770
+ return;
771
+ }
772
+ if (!beforeClick.proceed) {
773
+ trace("onClick:runBeforeButtonClick proceed=false");
774
+ await actions.reject();
775
+ return;
776
+ }
777
+ next = {
778
+ sessionId: beforeClick.sessionId,
779
+ accountPatch: beforeClick.accountPatch,
780
+ nonce: beforeClick.nonce
781
+ };
782
+ } catch (error) {
783
+ trace(
784
+ `onClick:runBeforeButtonClick rejected msg=${errorMessage(error, String(error)).slice(0, 120)}`
785
+ );
786
+ await actions.reject();
787
+ return;
788
+ }
789
+ }
790
+ invokeMerchantCallback(() => emit("buttonClick", "paypal"));
791
+ prepared = next;
792
+ trace("onClick:resolve");
793
+ await actions.resolve();
794
+ };
795
+ const renderButtons = async (gen) => {
796
+ const normalized = normalizeGatewayEnvironment(props.environment);
797
+ const environment = normalized === "live" ? "production" : normalized;
798
+ const type = objectType();
799
+ const existingOrderId = props.existingOrderId;
800
+ trace(`loadScript:start env=${environment ?? "default"} type=${type}`);
801
+ let paypal;
802
+ try {
803
+ paypal = await loadScript({
804
+ clientId: props.clientId ?? "",
805
+ ...props.currency ? { currency: props.currency } : {},
806
+ ...type === "subscription" ? { intent: "subscription", vault: true } : {},
807
+ ...type === "order" ? { intent: "capture" } : {},
808
+ ...environment ? { environment } : {},
809
+ ...environment === "sandbox" ? { dataNamespace: "paypal_sandbox" } : {}
810
+ });
811
+ } catch (error) {
812
+ trace(`loadScript:fail msg=${errorMessage(error, String(error)).slice(0, 120)}`);
813
+ throw new PayPalStartupFailure(errorMessage(error, "PayPal failed to load."), {
814
+ cause: error
815
+ });
816
+ }
817
+ if (!isCurrent(gen)) return;
818
+ trace("loadScript:ok");
819
+ if (!paypal?.Buttons) throw new PayPalStartupFailure("PayPal did not initialize.");
820
+ const start2 = () => {
821
+ if (!existingOrderId) return createIntent();
822
+ if (isCurrent(gen)) return Promise.resolve(existingOrderId);
823
+ return Promise.reject(
824
+ new FloPayError2("PayPal render was superseded.", "api_error", {
825
+ code: "paypal_render_superseded"
826
+ })
827
+ );
828
+ };
829
+ let handlers;
830
+ if (type === "subscription") handlers = { createSubscription: start2 };
831
+ else if (type === "setup_token") handlers = { createVaultSetupToken: () => createIntent() };
832
+ else handlers = { createOrder: start2 };
833
+ const presentation = resolvePayPalPresentationMode(
834
+ detectPayPalEnvironment(),
835
+ advertisedPresentation() === "app_switch" ? "app_switch" : void 0,
836
+ type
837
+ );
838
+ trace(`presentation=${presentation}`);
839
+ let rendered = false;
840
+ const instance = paypal.Buttons({
841
+ style: { layout: "horizontal", tagline: false, height: 44 },
842
+ ...presentation === "app_switch" ? { appSwitchWhenAvailable: true } : {},
843
+ ...handlers,
844
+ onClick: (_data, actions) => click(gen, actions),
845
+ onApprove: (data, actions) => approve(gen, data, actions),
846
+ onError: (error) => {
847
+ if (!isCurrent(gen)) return;
848
+ trace(
849
+ `onError rendered=${rendered} msg=${errorMessage(error, "PayPal failed to render.").slice(0, 120)}`
850
+ );
851
+ if (rendered) runtimeFailure(error);
852
+ }
853
+ });
854
+ buttons = instance;
855
+ const eligible = instance.isEligible();
856
+ trace(`isEligible=${eligible}`);
857
+ if (!eligible) {
858
+ throw new PayPalStartupFailure("PayPal is not available for this buyer.", {
859
+ ineligible: true
860
+ });
861
+ }
862
+ try {
863
+ if (container.value) await instance.render(container.value);
864
+ } catch (error) {
865
+ trace(`render:fail msg=${errorMessage(error, String(error)).slice(0, 120)}`);
866
+ throw new PayPalStartupFailure(errorMessage(error, "PayPal failed to render."), {
867
+ cause: error
868
+ });
869
+ }
870
+ if (!isCurrent(gen)) return;
871
+ rendered = true;
872
+ trace("render:ok");
873
+ };
874
+ const exhaust = (failure) => {
875
+ setState("exhausted");
876
+ trace(`exhausted msg=${failure.message.slice(0, 120)}`);
877
+ if (failure.ineligible) {
878
+ console.error("[FloPay] DirectPayPal load/render failure:", failure.message);
879
+ return;
880
+ }
881
+ const classification = classifyExternalMethodFailure(
882
+ {
883
+ code: isPayPalAcknowledgementDeadWindow(failure.message) ? "paypal_init_timeout" : "paypal_init_failure",
884
+ message: failure.message
885
+ },
886
+ { inAppBrowser: inAppBrowser(), handoffObserved: false }
887
+ );
888
+ provider?.emitInstrument({
889
+ class: "technical_error",
890
+ errorCode: "PROVIDER_LOAD_FAILED",
891
+ provider: "paypal"
892
+ });
893
+ const error = classification.popupBlocked ? new FloPayError2(
894
+ buildExternalMethodRecoveryMessage("PayPal", true, inAppBrowser()),
895
+ "api_error",
896
+ { code: "paypal_popup_blocked" }
897
+ ) : new FloPayError2(failure.message, "api_error", { code: "PROVIDER_LOAD_FAILED" });
898
+ invokeMerchantCallback(() => emit("error", error));
899
+ };
900
+ const closeButtons = () => {
901
+ void buttons?.close().catch(() => {
902
+ });
903
+ buttons = null;
904
+ };
905
+ const start = async (gen, retryUsed) => {
906
+ try {
907
+ await renderButtons(gen);
908
+ if (isCurrent(gen)) setState("ready");
909
+ } catch (error) {
910
+ if (!isCurrent(gen)) return;
911
+ const failure = error instanceof PayPalStartupFailure ? error : new PayPalStartupFailure(errorMessage(error, String(error)), { cause: error });
912
+ closeButtons();
913
+ if (!failure.ineligible && !retryUsed && props.allowAutomaticRetry) {
914
+ setState("retrying");
915
+ trace("retry:scheduled");
916
+ retryTimer = setTimeout(() => {
917
+ retryTimer = null;
918
+ if (isCurrent(gen)) void start(gen, true);
919
+ }, PAYPAL_INIT_RETRY_DELAY_MS);
920
+ return;
921
+ }
922
+ exhaust(failure);
923
+ }
924
+ };
925
+ const restart = () => {
926
+ generation += 1;
927
+ if (retryTimer) clearTimeout(retryTimer);
928
+ retryTimer = null;
929
+ prepared = null;
930
+ intentIdempotencyKey = void 0;
931
+ intentAttemptActive = false;
932
+ closeButtons();
933
+ if (!props.clientId) {
934
+ setState("exhausted");
935
+ return;
936
+ }
937
+ setState("loading");
938
+ trace("mount");
939
+ void start(generation, false);
940
+ };
941
+ const configKey = computed(
942
+ () => JSON.stringify([
943
+ baseUrl(),
944
+ props.clientId ?? "",
945
+ props.currency ?? "",
946
+ props.environment ?? "",
947
+ objectType(),
948
+ props.sessionId ?? "",
949
+ props.existingOrderId ?? "",
950
+ advertisedPresentation() === "app_switch" ? "app_switch" : ""
951
+ ])
952
+ );
953
+ watch(configKey, () => {
954
+ if (mounted && !unmounted) restart();
955
+ });
956
+ onMounted(() => {
957
+ mounted = true;
958
+ invokeMerchantCallback(() => emit("loadStateChange", false));
959
+ restart();
960
+ });
961
+ onBeforeUnmount2(() => {
962
+ unmounted = true;
963
+ if (retryTimer) clearTimeout(retryTimer);
964
+ retryTimer = null;
965
+ prepared = null;
966
+ closeButtons();
967
+ });
968
+ return () => {
969
+ const debugPanel = props.debug ? h2(
970
+ "pre",
971
+ {
972
+ "data-testid": "flopay-direct-paypal-debug",
973
+ style: debugPanelStyle(failed.value)
974
+ },
975
+ `FloPay/DirectPayPal-debug (ready=${ready.value} failed=${failed.value})${debugLines.value.length === 0 ? "\n(waiting for first lifecycle event\u2026)" : `
976
+ ${debugLines.value.join("\n")}`}`
977
+ ) : null;
978
+ if (failed.value) return debugPanel ? h2("div", [debugPanel]) : null;
979
+ const request = identityRequest.value;
980
+ return h2("div", [
981
+ debugPanel,
982
+ request ? h2(BuyerIdentityFallbackPanel, {
983
+ key: request.id,
984
+ identity: request.identity,
985
+ methodLabel: request.methodLabel,
986
+ cancellable: true,
987
+ testId: "flopay-direct-paypal-identity-pane",
988
+ onComplete: (identity) => finishBuyerIdentity(identity),
989
+ onCancel: () => finishBuyerIdentity(null)
990
+ }) : null,
991
+ // While the panel is open React keeps PayPal mounted but out of the
992
+ // way (zoid needs a measurable container, so no `display: none`).
993
+ h2("div", {
994
+ ref: container,
995
+ "data-testid": "flopay-direct-paypal",
996
+ "data-state": state.value,
997
+ "aria-busy": submitting.value || props.isProcessing,
998
+ ...request ? {
999
+ "aria-hidden": "true",
1000
+ style: { position: "relative", height: "0", opacity: "0", pointerEvents: "none" }
1001
+ } : {}
1002
+ })
1003
+ ]);
1004
+ };
1005
+ }
1006
+ });
1007
+
1008
+ // src/split-card-form.ts
1009
+ var SUCCESS_DELAY_MS = 600;
1010
+ var isStripeExpressMethod = (method) => STRIPE_EXPRESS_METHODS.includes(method);
1011
+ var GATE_DEBUG_STYLE = {
1012
+ margin: 0,
1013
+ padding: "6px 8px",
1014
+ background: "#eef2ff",
1015
+ border: "1px solid #c7d2fe",
1016
+ borderRadius: "6px",
1017
+ color: "#111827",
1018
+ font: "11px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace",
1019
+ whiteSpace: "pre-wrap",
1020
+ wordBreak: "break-word"
1021
+ };
1022
+ var PAYPAL_DIRECT_RETRY_LIMIT = 2;
1023
+ function isEmptySlotOutput(nodes) {
1024
+ return nodes.every((node) => {
1025
+ if (node.type === Comment) return true;
1026
+ if (node.type === Text) return !String(node.children ?? "").trim();
1027
+ if (node.type === Fragment) {
1028
+ return Array.isArray(node.children) && isEmptySlotOutput(node.children);
1029
+ }
1030
+ return false;
1031
+ });
1032
+ }
1033
+ function defaultCardButtonContent() {
1034
+ const svg = { fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" };
1035
+ return [
1036
+ h3(
1037
+ "svg",
1038
+ {
1039
+ ...svg,
1040
+ width: "20",
1041
+ height: "20",
1042
+ viewBox: "0 0 24 24",
1043
+ stroke: "currentColor",
1044
+ "stroke-width": "1.5",
1045
+ "aria-hidden": "true"
1046
+ },
1047
+ [
1048
+ h3("rect", { width: "20", height: "14", x: "2", y: "5", rx: "2" }),
1049
+ h3("line", { x1: "2", x2: "22", y1: "10", y2: "10" })
1050
+ ]
1051
+ ),
1052
+ "Credit / Debit Card",
1053
+ h3(
1054
+ "svg",
1055
+ {
1056
+ ...svg,
1057
+ width: "16",
1058
+ height: "16",
1059
+ viewBox: "0 0 24 24",
1060
+ stroke: "#9ca3af",
1061
+ "stroke-width": "2",
1062
+ style: { position: "absolute", right: "1rem" },
1063
+ "aria-hidden": "true"
1064
+ },
1065
+ [h3("path", { d: "M9 18l6-6-6-6" })]
1066
+ )
1067
+ ];
1068
+ }
1069
+ var DEFAULT_BUTTONS_LAYOUT_BUTTON_HEIGHT = 44;
1070
+ function resolveFormButtonsStyles(bundleStyles, buttonsTheme, override) {
1071
+ const base = bundleStyles ?? resolveButtonsLayoutTheme(buttonsTheme);
1072
+ if (!override) return base;
1073
+ return {
1074
+ ...base,
1075
+ ...override,
1076
+ cardButton: { ...base.cardButton, ...override.cardButton },
1077
+ cardFormContainer: { ...base.cardFormContainer, ...override.cardFormContainer },
1078
+ backButton: { ...base.backButton, ...override.backButton },
1079
+ backButtonIcon: { ...base.backButtonIcon, ...override.backButtonIcon },
1080
+ submitButton: { ...base.submitButton, ...override.submitButton },
1081
+ title: { ...base.title, ...override.title }
1082
+ };
1083
+ }
1084
+ var ERROR_DELAY_MS = 1200;
1085
+ function isAccountValidationError(error) {
1086
+ return error instanceof FloPayError3 && typeof error.statusCode === "number" && error.statusCode >= 400 && error.statusCode < 500;
1087
+ }
1088
+ function noVaultCapabilityError() {
1089
+ return new FloPayError3(
1090
+ "FloPay: this checkout session has no vault card capability and no usable supported non-card payment methods. Upgrade the billing backend integration.",
1091
+ "api_error",
1092
+ { code: "UnsupportedBackendVaultCapability" }
1093
+ );
1094
+ }
1095
+ var SplitCardForm = defineComponent3({
1096
+ name: "SplitCardForm",
1097
+ props: {
1098
+ session: { type: Object, default: void 0 },
1099
+ sessionId: { type: String, default: void 0 },
1100
+ nonce: { type: String, default: void 0 },
1101
+ billingApiUrl: { type: String, default: void 0 },
1102
+ telemetry: { type: Boolean, default: void 0 },
1103
+ email: { type: String, default: void 0 },
1104
+ firstName: { type: String, default: void 0 },
1105
+ lastName: { type: String, default: void 0 },
1106
+ layout: { type: String, default: "default" },
1107
+ initialCardOpen: { type: Boolean, default: false },
1108
+ showPayPal: { type: Boolean, default: true },
1109
+ // No default here: an unset prop takes the checkout's value, then `'auto'`.
1110
+ walletPresentation: { type: String, default: void 0 },
1111
+ showStripe: { type: Boolean, default: true },
1112
+ showApplePay: { type: Boolean, default: true },
1113
+ showGooglePay: { type: Boolean, default: true },
1114
+ enabledPaymentMethods: { type: Array, default: void 0 },
1115
+ dataAttachmentPending: { type: Boolean, default: void 0 },
1116
+ dataAttachmentRequiresIdentity: { type: Boolean, default: void 0 },
1117
+ submitLabel: { type: String, default: void 0 },
1118
+ enableAVS: {
1119
+ type: [Boolean, Object],
1120
+ default: void 0
1121
+ },
1122
+ avsLayout: { type: String, default: "row" },
1123
+ country: { type: String, default: void 0 },
1124
+ zip: { type: String, default: void 0 },
1125
+ addressLine1: { type: String, default: void 0 },
1126
+ addressLine2: { type: String, default: void 0 },
1127
+ city: { type: String, default: void 0 },
1128
+ state: { type: String, default: void 0 },
1129
+ isProcessing: { type: Boolean, default: void 0 },
1130
+ error: { type: String, default: void 0 },
1131
+ theme: { type: String, default: void 0 },
1132
+ buttonsTheme: { type: String, default: void 0 },
1133
+ buttonsStyles: { type: Object, default: void 0 },
1134
+ appearance: { type: Object, default: void 0 },
1135
+ className: { type: String, default: void 0 },
1136
+ cardFieldOrder: { type: Array, default: void 0 },
1137
+ onBeforeButtonClick: {
1138
+ type: Function,
1139
+ default: void 0
1140
+ },
1141
+ onBuyerIdentityReady: {
1142
+ type: Function,
1143
+ default: void 0
1144
+ },
1145
+ userId: { type: String, default: void 0 },
1146
+ avsCheck: { type: Boolean, default: void 0 },
1147
+ /** @deprecated Not rendered (React's deprecated `showSecurityFooter`). */
1148
+ showSecurityFooter: { type: Boolean, default: void 0 },
1149
+ directPaypal: { type: Object, default: void 0 },
1150
+ currency: { type: String, default: void 0 },
1151
+ isSubscription: { type: Boolean, default: void 0 },
1152
+ /**
1153
+ * React's `totalAmount`: the cart total due today, in minor units
1154
+ * (`FloPayCheckout` passes it). Payment-mode Stripe Elements use
1155
+ * `totalAmount || 100` (React's `amountInCents` placeholder for a $0 cart),
1156
+ * and the wallet row runs in setup mode for $0. Default `0`, as React's.
1157
+ */
1158
+ totalAmount: { type: Number, default: 0 },
1159
+ onTokenizedBody: {
1160
+ type: Function,
1161
+ default: void 0
1162
+ },
1163
+ chv: { type: String, default: void 0 },
1164
+ checkoutType: { type: String, default: void 0 },
1165
+ checkoutLayout: { type: String, default: void 0 },
1166
+ enabledPaymentMethodCountries: {
1167
+ type: Object,
1168
+ default: void 0
1169
+ },
1170
+ debug: { type: Boolean, default: false }
1171
+ },
1172
+ emits: {
1173
+ complete: (_result) => true,
1174
+ error: (_error) => true,
1175
+ decline: (_decline) => true,
1176
+ buttonClick: (_method) => true,
1177
+ countryChange: (_country) => true,
1178
+ // React's `onZipChange`.
1179
+ zipChange: (_zip) => true,
1180
+ // React's `onErrorChange`: the form's own error message changed (`null` when cleared).
1181
+ errorChange: (_error) => true
1182
+ },
1183
+ /**
1184
+ * Slots (React's content props):
1185
+ * - `cardButtonContent`: the buttons-layout card tile's content.
1186
+ * - `cardBackButtonContent`: the label of the card view's back button.
1187
+ * - `cardTitleContent`: the card-form title (default `Secure card checkout`).
1188
+ * - `cardPreFormSlot`: content directly above the hosted card widget.
1189
+ *
1190
+ * An empty `cardBackButtonContent` or `cardTitleContent` slot hides that
1191
+ * label, as React does for `''`, `null` or `false`.
1192
+ */
1193
+ setup(props, { emit, slots }) {
1194
+ const provider = inject2(FLOPAY_CONTEXT, null);
1195
+ const checkout = inject2(CHECKOUT_STATE, null);
1196
+ const defaults = inject2(CHECKOUT_FORM_DEFAULTS, () => ({}));
1197
+ const session = computed2(
1198
+ () => props.session ?? defaults().session ?? checkout?.session ?? null
1199
+ );
1200
+ const sessionId = computed2(() => props.sessionId ?? defaults().sessionId ?? session.value?.id);
1201
+ const nonce = computed2(
1202
+ () => props.nonce ?? defaults().nonce ?? (session.value?.clientSecret || void 0)
1203
+ );
1204
+ const billingApiUrl = computed2(
1205
+ () => resolveBillingApiUrl2(
1206
+ props.billingApiUrl ?? defaults().billingApiUrl ?? provider?.billingApiUrl()
1207
+ )
1208
+ );
1209
+ const submitLabel = computed2(
1210
+ () => props.submitLabel === void 0 ? defaults().submitLabel : props.submitLabel
1211
+ );
1212
+ const pending = computed2(
1213
+ () => props.dataAttachmentPending === true || defaults().dataAttachmentPending === true || checkout?.claimPending === true
1214
+ );
1215
+ const requiresIdentity = computed2(
1216
+ () => props.dataAttachmentRequiresIdentity === true || checkout?.claimRequiresIdentity === true
1217
+ );
1218
+ const nonCardAttachmentBlocked = computed2(() => pending.value && !requiresIdentity.value);
1219
+ const vaultMountContext = computed2(() => ({
1220
+ billingApiUrl: billingApiUrl.value,
1221
+ sessionId: sessionId.value,
1222
+ nonce: nonce.value
1223
+ }));
1224
+ const embeddedVault = computed2(
1225
+ () => resolveVaultMountOptions(session.value?.vault, vaultMountContext.value)
1226
+ );
1227
+ const deferredVaultAdvertised = computed2(() => {
1228
+ const current = session.value;
1229
+ if (!current || !props.showStripe || !sessionId.value) return false;
1230
+ if (isMountableVaultBlock(current.vault)) return false;
1231
+ const advertised = current.gateways?.stripe?.enabledPaymentMethods ?? props.enabledPaymentMethods;
1232
+ return Boolean(advertised?.includes("card")) || canRecoverHostedCardForm(current) || Boolean(current.gateways?.pcivault);
1233
+ });
1234
+ const lazyVault = shallowRef2(null);
1235
+ const vault = computed2(
1236
+ () => embeddedVault.value ?? lazyVault.value
1237
+ );
1238
+ const vaultCapable = computed2(
1239
+ () => Boolean(embeddedVault.value) || deferredVaultAdvertised.value
1240
+ );
1241
+ const themeBundle = computed2(() => resolveTheme(props.theme));
1242
+ const bStyles = computed2(
1243
+ () => resolveFormButtonsStyles(
1244
+ themeBundle.value?.buttonsLayout,
1245
+ props.buttonsTheme,
1246
+ props.buttonsStyles
1247
+ )
1248
+ );
1249
+ const appearance = computed2(() => props.appearance ?? themeBundle.value?.appearance);
1250
+ const vaultTheme = computed2(
1251
+ () => computeVaultCardThemeColors({
1252
+ appearance: appearance.value,
1253
+ buttonsStyles: bStyles.value,
1254
+ layout: props.layout
1255
+ })
1256
+ );
1257
+ const cardTileStyle = computed2(() => {
1258
+ const vars = appearance.value?.variables;
1259
+ const explicitPrimary = typeof vars?.colorPrimary === "string" ? vars.colorPrimary : void 0;
1260
+ const radius = bStyles.value.cardButton?.borderRadius ?? vars?.borderRadius ?? "8px";
1261
+ return {
1262
+ width: "100%",
1263
+ ...bStyles.value.cardButton,
1264
+ ...derivePrimaryTileStyle({
1265
+ themeBundle: themeBundle.value,
1266
+ resolvedPrimaryColor: explicitPrimary ?? "#4A49FF",
1267
+ resolvedBorderRadius: String(radius),
1268
+ submitButtonStyle: bStyles.value.submitButton,
1269
+ explicitPrimaryColor: explicitPrimary
1270
+ }),
1271
+ fontSize: bStyles.value.cardButtonFontSize ?? "0.95rem"
1272
+ };
1273
+ });
1274
+ const directPaypalConfigured = computed2(() => Boolean(props.directPaypal?.clientId));
1275
+ const directPaypal = computed2(
1276
+ () => props.showPayPal && props.directPaypal?.clientId ? props.directPaypal : null
1277
+ );
1278
+ const directPayPalCanWaitForAttachment = computed2(
1279
+ () => !nonCardAttachmentBlocked.value || checkout !== null
1280
+ );
1281
+ const stripePayPalReturnOwner = hasStripePayPalReturn();
1282
+ const resolvedWalletPresentation = computed2(
1283
+ () => props.walletPresentation ?? defaults().walletPresentation ?? "auto"
1284
+ );
1285
+ const stripePayPalPresentation = computed2(
1286
+ () => stripePayPalReturnOwner ? "redirect" : resolveStripePayPalPresentation(
1287
+ detectPayPalEnvironment2(typeof navigator === "undefined" ? "" : void 0),
1288
+ resolvedWalletPresentation.value
1289
+ )
1290
+ );
1291
+ const paypalRetry = shallowRef2(
1292
+ props.directPaypal?.existingOrderId ? { orderId: props.directPaypal.existingOrderId, attempts: 0 } : null
1293
+ );
1294
+ const resolvedCurrency = computed2(() => props.currency ?? "usd");
1295
+ const resolvedIsSubscription = computed2(() => props.isSubscription ?? false);
1296
+ const directPaypalInitializationState = ref3("loading");
1297
+ const directPaypalUnavailable = computed2(
1298
+ () => directPaypalInitializationState.value === "retrying" || directPaypalInitializationState.value === "exhausted"
1299
+ );
1300
+ const stripeKey = computed2(
1301
+ () => session.value?.gateways?.stripe?.publishableKey
1302
+ );
1303
+ const stripeMethods = computed2(() => {
1304
+ if (!props.showStripe || !stripeKey.value) return [];
1305
+ const listed = props.enabledPaymentMethods ?? session.value?.gateways?.stripe?.enabledPaymentMethods ?? [];
1306
+ return listed.filter((method) => {
1307
+ if (method === "card") return false;
1308
+ if (method === "apple_pay") return props.showApplePay;
1309
+ if (method === "google_pay") return props.showGooglePay;
1310
+ if (method === "paypal") {
1311
+ return props.showPayPal && (!directPaypal.value || directPaypalUnavailable.value);
1312
+ }
1313
+ return true;
1314
+ });
1315
+ });
1316
+ const unsupported = computed2(
1317
+ () => Boolean(session.value) && !vaultCapable.value && stripeMethods.value.length === 0 && !directPaypal.value
1318
+ );
1319
+ const apmCountry = computed2(() => avsConfig.value ? country.value : props.country);
1320
+ const renderedStripeMethods = computed2(() => {
1321
+ const others = stripeMethods.value.filter((method) => !isStripeExpressMethod(method));
1322
+ const allowed = filterStripeMethodsByCountryGate(
1323
+ others,
1324
+ apmCountry.value,
1325
+ props.enabledPaymentMethodCountries
1326
+ );
1327
+ return stripeMethods.value.filter(
1328
+ (method) => isStripeExpressMethod(method) || allowed.includes(method)
1329
+ );
1330
+ });
1331
+ const inAppBrowserDetected = () => typeof navigator === "undefined" ? void 0 : detectPayPalEnvironment2().inAppBrowser;
1332
+ const cardOpen = ref3(props.layout !== "buttons" || props.initialCardOpen);
1333
+ const capture = shallowRef2(null);
1334
+ const failure = ref3(null);
1335
+ const overlay = ref3(null);
1336
+ watch2(overlay, (state) => {
1337
+ if (state === "success") markSessionRecentlyCompleted(sessionId.value);
1338
+ });
1339
+ let overlayTimer = null;
1340
+ const methodOverlay = ref3(null);
1341
+ let methodOverlayTimer = null;
1342
+ let methodCompleted = false;
1343
+ let completed = false;
1344
+ let detachOutcomes = null;
1345
+ const avsConfig = computed2(() => resolveAVSConfig(props.enableAVS));
1346
+ const country = ref3((props.country ?? session.value?.customer?.country ?? "").toUpperCase());
1347
+ const zipCode = ref3(props.zip ?? session.value?.customer?.zip ?? "");
1348
+ const addressLine1 = ref3(props.addressLine1 ?? "");
1349
+ const addressLine2 = ref3(props.addressLine2 ?? "");
1350
+ const city = ref3(props.city ?? "");
1351
+ const region = ref3(props.state ?? "");
1352
+ const editedAvs = /* @__PURE__ */ new Set();
1353
+ const adoptAvs = (field, next, model, notify) => {
1354
+ if (editedAvs.has(field)) return;
1355
+ const value = next?.trim();
1356
+ if (!value || value === model.value) return;
1357
+ model.value = value;
1358
+ if (notify) invokeMerchantCallback2(() => notify(value));
1359
+ };
1360
+ watch2(
1361
+ () => props.country,
1362
+ (next) => adoptAvs("country", next?.toUpperCase(), country, (value) => emit("countryChange", value))
1363
+ );
1364
+ watch2(
1365
+ () => props.zip,
1366
+ (next) => adoptAvs("postal_code", next, zipCode, (value) => emit("zipChange", value))
1367
+ );
1368
+ watch2(
1369
+ () => props.addressLine1,
1370
+ (next) => adoptAvs("address_line_1", next, addressLine1)
1371
+ );
1372
+ watch2(
1373
+ () => props.addressLine2,
1374
+ (next) => adoptAvs("address_line_2", next, addressLine2)
1375
+ );
1376
+ watch2(
1377
+ () => props.city,
1378
+ (next) => adoptAvs("city", next, city)
1379
+ );
1380
+ watch2(
1381
+ () => props.state,
1382
+ (next) => adoptAvs("state", next, region)
1383
+ );
1384
+ const externallyProcessing = computed2(() => props.isProcessing === true);
1385
+ const displayError = computed2(() => props.error ?? failure.value);
1386
+ watch2(failure, (message) => invokeMerchantCallback2(() => emit("errorChange", message)));
1387
+ const avsVisible = (field) => {
1388
+ const config = avsConfig.value;
1389
+ return config ? isAVSFieldVisible(config[field], country.value.trim()) : false;
1390
+ };
1391
+ const avsInvalid = computed2(() => {
1392
+ if (!avsConfig.value) return false;
1393
+ const cc = country.value.trim();
1394
+ if (avsVisible("country") && !cc) return true;
1395
+ if (avsVisible("postal_code") && !isValidPostalCode(cc, zipCode.value.trim())) return true;
1396
+ if (avsVisible("address_line_1") && !addressLine1.value.trim()) return true;
1397
+ if (avsVisible("city") && !city.value.trim()) return true;
1398
+ return avsVisible("state") && !region.value.trim();
1399
+ });
1400
+ const gateClosed = computed2(() => pending.value || avsInvalid.value);
1401
+ const emitError = (error) => invokeMerchantCallback2(() => emit("error", error));
1402
+ const emitDecline = (decline) => invokeMerchantCallback2(() => emit("decline", decline));
1403
+ const accountPatch = shallowRef2(void 0);
1404
+ const identityField = (key) => (accountPatch.value?.[key] ?? props[key] ?? session.value?.customer?.[key] ?? "").trim();
1405
+ const billingName = computed2(
1406
+ () => [identityField("firstName"), identityField("lastName")].filter(Boolean).join(" ")
1407
+ );
1408
+ const untilAttached = () => {
1409
+ if (!pending.value) return Promise.resolve();
1410
+ return new Promise((resolve) => {
1411
+ const stop = watch2(pending, (isPending) => {
1412
+ if (isPending) return;
1413
+ stop();
1414
+ resolve();
1415
+ });
1416
+ });
1417
+ };
1418
+ const runBeforeButtonClick = async (method) => {
1419
+ if (method !== "card" && nonCardAttachmentBlocked.value) await untilAttached();
1420
+ const hook = props.onBeforeButtonClick;
1421
+ if (!hook) return { proceed: true };
1422
+ try {
1423
+ const result = await hook({
1424
+ method,
1425
+ sessionId: sessionId.value || void 0,
1426
+ createSession: checkout?.inlineSessionDraft
1427
+ });
1428
+ if (result === false) return { proceed: false };
1429
+ if (result && typeof result === "object") {
1430
+ if (result.account) {
1431
+ accountPatch.value = { ...accountPatch.value ?? {}, ...result.account };
1432
+ }
1433
+ return {
1434
+ proceed: true,
1435
+ accountPatch: result.account,
1436
+ sessionId: sessionId.value,
1437
+ nonce: nonce.value
1438
+ };
1439
+ }
1440
+ return { proceed: true };
1441
+ } catch (error) {
1442
+ const failureError = normalizeBeforeButtonClickError(method, error);
1443
+ failure.value = failureError.message;
1444
+ emitError(failureError);
1445
+ return { proceed: false };
1446
+ }
1447
+ };
1448
+ const recoverStripePayPalAccount = async (returnSessionId) => {
1449
+ const hook = props.onBeforeButtonClick;
1450
+ if (!hook) return void 0;
1451
+ try {
1452
+ const result = await hook({
1453
+ method: "paypal",
1454
+ sessionId: returnSessionId || void 0,
1455
+ createSession: checkout?.inlineSessionDraft
1456
+ });
1457
+ return result && typeof result === "object" ? result.account : void 0;
1458
+ } catch {
1459
+ return void 0;
1460
+ }
1461
+ };
1462
+ const holdFailureOverlay = () => {
1463
+ if (overlayTimer) clearTimeout(overlayTimer);
1464
+ overlay.value = "failed";
1465
+ overlayTimer = setTimeout(() => {
1466
+ overlayTimer = null;
1467
+ overlay.value = null;
1468
+ }, ERROR_DELAY_MS);
1469
+ };
1470
+ const accountSnapshot = () => {
1471
+ const email = identityField("email");
1472
+ if (!isValidEmail3(email)) return null;
1473
+ const cc = country.value.trim();
1474
+ const config = avsConfig.value;
1475
+ const visible = {
1476
+ country: avsVisible("country"),
1477
+ postal_code: avsVisible("postal_code"),
1478
+ address_line_1: avsVisible("address_line_1"),
1479
+ address_line_2: avsVisible("address_line_2"),
1480
+ city: avsVisible("city"),
1481
+ state: avsVisible("state")
1482
+ };
1483
+ const zip = zipCode.value.trim();
1484
+ const derivedState = visible.address_line_1 && !visible.state && visible.postal_code ? getStateFromPostalCode(cc, zip) : null;
1485
+ const state = visible.state ? region.value.trim() : derivedState;
1486
+ const firstName = identityField("firstName");
1487
+ const lastName = identityField("lastName");
1488
+ const userId = accountPatch.value?.userId ?? (props.userId || defaults().userId) ?? "";
1489
+ return {
1490
+ accountData: {
1491
+ userId,
1492
+ email,
1493
+ ...firstName ? { firstName } : {},
1494
+ ...lastName ? { lastName } : {},
1495
+ ...visible.postal_code && zip ? { zip } : {},
1496
+ ...visible.city && city.value.trim() ? { city: city.value.trim() } : {},
1497
+ ...state ? { state } : {},
1498
+ ...visible.address_line_1 && addressLine1.value.trim() ? { addressLine1: addressLine1.value.trim() } : {},
1499
+ ...visible.address_line_2 && addressLine2.value.trim() ? { addressLine2: addressLine2.value.trim() } : {},
1500
+ ...cc ? { country: cc } : {}
1501
+ },
1502
+ // React sends `avsCheck` only when the prop is set (FloPayCheckout passes `!!enableAVS`).
1503
+ ...props.avsCheck === void 0 ? {} : { avsCheck: props.avsCheck },
1504
+ ...config ? { avsConfig: visible } : {}
1505
+ };
1506
+ };
1507
+ const paypalOverlay = ref3(null);
1508
+ let paypalOverlayTimer = null;
1509
+ const stripeMethodsProcessing = computed2(
1510
+ () => props.isProcessing ?? paypalOverlay.value === "processing"
1511
+ );
1512
+ let tokenizedProcessing = false;
1513
+ const setPaypalOverlay = (state) => {
1514
+ if (paypalOverlayTimer) clearTimeout(paypalOverlayTimer);
1515
+ paypalOverlayTimer = null;
1516
+ paypalOverlay.value = state;
1517
+ };
1518
+ const holdPaypalFailure = (message) => {
1519
+ failure.value = message;
1520
+ setPaypalOverlay("failed");
1521
+ paypalOverlayTimer = setTimeout(() => {
1522
+ paypalOverlayTimer = null;
1523
+ paypalOverlay.value = null;
1524
+ }, ERROR_DELAY_MS);
1525
+ };
1526
+ const processAvsInput = () => ({
1527
+ avsConfig: avsConfig.value,
1528
+ country: country.value,
1529
+ zip: zipCode.value,
1530
+ city: city.value,
1531
+ state: region.value,
1532
+ addressLine1: addressLine1.value,
1533
+ addressLine2: addressLine2.value
1534
+ });
1535
+ const buildTokenizedProcessRequest = (body, overrides, effectiveSessionId) => {
1536
+ const attemptPatch = overrides?.accountPatch;
1537
+ const identity = (key) => attemptPatch?.[key] ?? identityField(key);
1538
+ const userId = attemptPatch?.userId ?? accountPatch.value?.userId ?? (props.userId || defaults().userId) ?? "";
1539
+ return {
1540
+ userId,
1541
+ params: {
1542
+ sessionId: effectiveSessionId,
1543
+ // The nonce stays paired with the (possibly overridden) session.
1544
+ nonce: overrides?.nonce ?? nonce.value,
1545
+ paymentMethodCategory: "paypal",
1546
+ tokenizedData: body,
1547
+ accountData: {
1548
+ ...attemptPatch,
1549
+ userId,
1550
+ email: identity("email"),
1551
+ firstName: identity("firstName") || void 0,
1552
+ lastName: identity("lastName") || void 0,
1553
+ ...buildProcessPaymentAvsAccount(processAvsInput())
1554
+ },
1555
+ ...buildCheckoutProcessMetadata({
1556
+ ...processAvsInput(),
1557
+ chv: props.chv,
1558
+ avsCheck: props.avsCheck,
1559
+ checkoutType: props.checkoutType,
1560
+ checkoutLayout: props.checkoutLayout
1561
+ })
1562
+ }
1563
+ };
1564
+ };
1565
+ const completeTokenizedProcess = (result, effectiveSessionId) => {
1566
+ markSessionRecentlyCompleted(effectiveSessionId);
1567
+ setPaypalOverlay("success");
1568
+ const authorized = session.value?.captureMethod === "manual" || result?.status === "authorized";
1569
+ const paymentId = typeof result?.paymentId === "string" ? result.paymentId : session.value?.paymentId;
1570
+ const authorizationExpiresAt = typeof result?.authorizationExpiresAt === "string" ? result.authorizationExpiresAt : session.value?.authorizationExpiresAt;
1571
+ const completion = authorized ? {
1572
+ status: "authorized",
1573
+ ...paymentId === void 0 ? {} : { paymentId },
1574
+ sessionId: typeof result?.sessionId === "string" ? result.sessionId : effectiveSessionId,
1575
+ ...authorizationExpiresAt === void 0 ? {} : { authorizationExpiresAt },
1576
+ checkoutMethod: "paypal"
1577
+ } : { status: "succeeded", checkoutMethod: "paypal" };
1578
+ setTimeout(() => {
1579
+ setPaypalOverlay(null);
1580
+ invokeMerchantCallback2(() => emit("complete", completion));
1581
+ }, SUCCESS_DELAY_MS);
1582
+ };
1583
+ const rebindDirectPayPal = (json, effectiveSessionId) => {
1584
+ const orderId = typeof json.orderId === "string" ? json.orderId : "";
1585
+ if (!orderId) {
1586
+ holdPaypalFailure("PayPal retry required but no order id provided.");
1587
+ return;
1588
+ }
1589
+ const attempts = paypalRetry.value?.attempts ?? 0;
1590
+ if (attempts >= PAYPAL_DIRECT_RETRY_LIMIT) {
1591
+ holdPaypalFailure("PayPal payment could not be completed after multiple attempts.");
1592
+ emitDecline({
1593
+ method: "paypal",
1594
+ message: "paypal_direct_required retry limit exceeded",
1595
+ ...effectiveSessionId ? { sessionId: effectiveSessionId } : {}
1596
+ });
1597
+ return;
1598
+ }
1599
+ paypalRetry.value = { orderId, attempts: attempts + 1 };
1600
+ setPaypalOverlay(null);
1601
+ };
1602
+ const rejectTokenizedProcess = (json, effectiveSessionId) => {
1603
+ if (json?.type === "paypal_direct_required") {
1604
+ rebindDirectPayPal(json, effectiveSessionId);
1605
+ return;
1606
+ }
1607
+ if (json?.type === "paypal_redirect_required") {
1608
+ if (!json.threeDSecureToken) {
1609
+ holdPaypalFailure("PayPal authorization required but no token provided.");
1610
+ return;
1611
+ }
1612
+ failure.value = "PayPal is not available.";
1613
+ setPaypalOverlay(null);
1614
+ return;
1615
+ }
1616
+ const message = typeof json?.message === "string" ? json.message : "Payment failed. Please try again.";
1617
+ const code = json?.code ?? json?.gatewayErrorCode;
1618
+ const declineCode = json?.declineCode ?? json?.gatewayDeclineReason;
1619
+ holdPaypalFailure(message);
1620
+ emitDecline({
1621
+ method: "paypal",
1622
+ message,
1623
+ ...code ? { code } : {},
1624
+ ...declineCode ? { declineCode } : {},
1625
+ ...effectiveSessionId ? { sessionId: effectiveSessionId } : {}
1626
+ });
1627
+ };
1628
+ const processTokenizedBody = async (body, overrides) => {
1629
+ if (tokenizedProcessing) return;
1630
+ tokenizedProcessing = true;
1631
+ setPaypalOverlay("processing");
1632
+ failure.value = null;
1633
+ const effectiveSessionId = overrides?.sessionId ?? sessionId.value ?? "";
1634
+ const request = buildTokenizedProcessRequest(body, overrides, effectiveSessionId);
1635
+ try {
1636
+ const response = await new PaymentAPI2(billingApiUrl.value).processPayment(
1637
+ request.userId,
1638
+ request.params
1639
+ );
1640
+ if (response.ok) {
1641
+ const result = response.status === 204 ? null : await response.json().catch(() => null);
1642
+ completeTokenizedProcess(result, effectiveSessionId);
1643
+ return;
1644
+ }
1645
+ const json = await response.json().catch(() => null);
1646
+ rejectTokenizedProcess(json, effectiveSessionId);
1647
+ } catch (error) {
1648
+ holdPaypalFailure(error instanceof Error ? error.message : "An unexpected error occurred");
1649
+ } finally {
1650
+ tokenizedProcessing = false;
1651
+ }
1652
+ };
1653
+ const dispatchTokenizedBody = (body, overrides) => {
1654
+ if (props.onTokenizedBody) {
1655
+ props.onTokenizedBody(body);
1656
+ return;
1657
+ }
1658
+ void processTokenizedBody(body, overrides);
1659
+ };
1660
+ const onSubmitting = () => {
1661
+ if (overlayTimer) clearTimeout(overlayTimer);
1662
+ overlay.value = "processing";
1663
+ failure.value = null;
1664
+ const id = sessionId.value;
1665
+ const token = nonce.value;
1666
+ if (!id || !token) return;
1667
+ const snapshot = accountSnapshot();
1668
+ if (!snapshot) {
1669
+ failure.value = "Enter a valid email address.";
1670
+ overlay.value = null;
1671
+ return;
1672
+ }
1673
+ void new PaymentAPI2(billingApiUrl.value).patchAccountSnapshot(id, token, snapshot, { telemetryMode: "best_effort" }).catch((error) => {
1674
+ if (!isAccountValidationError(error)) return;
1675
+ const message = error instanceof Error ? error.message : "Please check your billing address and try again.";
1676
+ failure.value = message;
1677
+ overlay.value = null;
1678
+ emitError(error instanceof FloPayError3 ? error : new FloPayError3(message, "api_error"));
1679
+ });
1680
+ };
1681
+ const bindOutcomes = (adapter) => {
1682
+ const offSubmitting = adapter.on("submitting", onSubmitting);
1683
+ const offComplete = adapter.on("complete", (event) => {
1684
+ if (overlayTimer) clearTimeout(overlayTimer);
1685
+ overlay.value = "success";
1686
+ setTimeout(() => {
1687
+ if (completed) return;
1688
+ completed = true;
1689
+ const authorized = event.outcome === "authorized" || session.value?.captureMethod === "manual";
1690
+ const result = authorized ? {
1691
+ status: "authorized",
1692
+ paymentIntentId: event.intentId,
1693
+ paymentId: event.paymentId ?? session.value?.paymentId,
1694
+ sessionId: event.sessionId ?? sessionId.value,
1695
+ authorizationExpiresAt: event.authorizationExpiresAt ?? session.value?.authorizationExpiresAt,
1696
+ checkoutMethod: "card"
1697
+ } : { status: "succeeded", paymentIntentId: event.intentId, checkoutMethod: "card" };
1698
+ invokeMerchantCallback2(() => emit("complete", result));
1699
+ }, SUCCESS_DELAY_MS);
1700
+ });
1701
+ const offDecline = adapter.on("decline", (event) => {
1702
+ const message = event.message ?? "Your payment was declined. Please try another card or contact your bank.";
1703
+ failure.value = message;
1704
+ holdFailureOverlay();
1705
+ const code = event.failureOutcome ?? event.declineReason ?? "authorization_declined";
1706
+ emitDecline({
1707
+ method: "card",
1708
+ message,
1709
+ code,
1710
+ declineCode: code,
1711
+ ...sessionId.value ? { sessionId: sessionId.value } : {}
1712
+ });
1713
+ });
1714
+ const offError = adapter.on("error", (event) => {
1715
+ const message = event.message ?? "There was a problem processing your card.";
1716
+ failure.value = message;
1717
+ holdFailureOverlay();
1718
+ emitError(new FloPayError3(message, "api_error"));
1719
+ });
1720
+ return () => {
1721
+ offSubmitting();
1722
+ offComplete();
1723
+ offDecline();
1724
+ offError();
1725
+ };
1726
+ };
1727
+ const cardRequested = ref3(cardOpen.value);
1728
+ const vaultRecoveryAttempt = ref3(0);
1729
+ let lazyVerificationShell = null;
1730
+ let lazyVaultRequest = 0;
1731
+ const recoverVault = () => {
1732
+ const request = ++lazyVaultRequest;
1733
+ const id = sessionId.value;
1734
+ if (embeddedVault.value || !deferredVaultAdvertised.value || !id || !cardRequested.value) {
1735
+ lazyVault.value = null;
1736
+ return;
1737
+ }
1738
+ const context = { ...vaultMountContext.value, sessionId: id };
1739
+ const shellKey = `${context.billingApiUrl}\0${id}\0${context.nonce ?? ""}`;
1740
+ if (lazyVerificationShell?.key === shellKey) {
1741
+ lazyVault.value = lazyVerificationShell.mount;
1742
+ return;
1743
+ }
1744
+ lazyVault.value = null;
1745
+ new PaymentAPI2(context.billingApiUrl).getVaultCapture(id, context.nonce).then((block) => {
1746
+ if (request !== lazyVaultRequest) return;
1747
+ const mount = resolveVaultMountOptions(block, context);
1748
+ if (mount?.verificationRequired && !block.html) {
1749
+ lazyVerificationShell = { key: shellKey, mount };
1750
+ }
1751
+ if (mount) lazyVault.value = mount;
1752
+ else failure.value = "Failed to load the secure card form.";
1753
+ }).catch((error) => {
1754
+ if (request !== lazyVaultRequest) return;
1755
+ failure.value = error instanceof Error ? error.message : "Failed to load the secure card form.";
1756
+ });
1757
+ };
1758
+ const ensureCapture = () => {
1759
+ detachOutcomes?.();
1760
+ detachOutcomes = null;
1761
+ capture.value = null;
1762
+ const id = sessionId.value;
1763
+ if (!vault.value || !id) return;
1764
+ const captureMethod = session.value?.captureMethod;
1765
+ const flopay = provider?.flopay.value;
1766
+ const adapter = flopay ? flopay.cardCapture({ sessionId: id, ...captureMethod ? { captureMethod } : {} }) : new PciVaultCardCapture({
1767
+ sessionId: id,
1768
+ ...captureMethod ? { captureMethod } : {},
1769
+ billingApiUrl: billingApiUrl.value,
1770
+ telemetry: props.telemetry
1771
+ });
1772
+ detachOutcomes = bindOutcomes(adapter);
1773
+ adapter.setSubmitGate?.(gateClosed.value);
1774
+ capture.value = adapter;
1775
+ };
1776
+ onMounted2(() => {
1777
+ watch2(
1778
+ unsupported,
1779
+ (isUnsupported) => {
1780
+ if (isUnsupported) emitError(noVaultCapabilityError());
1781
+ },
1782
+ { immediate: true }
1783
+ );
1784
+ watch2(
1785
+ () => [
1786
+ embeddedVault.value,
1787
+ deferredVaultAdvertised.value,
1788
+ vaultMountContext.value.billingApiUrl,
1789
+ sessionId.value,
1790
+ nonce.value,
1791
+ cardRequested.value,
1792
+ vaultRecoveryAttempt.value
1793
+ ],
1794
+ recoverVault,
1795
+ { immediate: true }
1796
+ );
1797
+ watch2(() => [sessionId.value, vault.value?.html], ensureCapture, { immediate: true });
1798
+ watch2(
1799
+ () => [capture.value, props.cardFieldOrder, avsConfig.value],
1800
+ ([adapter, order, avs]) => adapter?.setCardFieldOrder?.(order ?? null, avs == null),
1801
+ { immediate: true }
1802
+ );
1803
+ watch2(gateClosed, (closed) => capture.value?.setSubmitGate?.(closed));
1804
+ });
1805
+ onBeforeUnmount3(() => {
1806
+ lazyVaultRequest += 1;
1807
+ detachOutcomes?.();
1808
+ detachOutcomes = null;
1809
+ if (overlayTimer) clearTimeout(overlayTimer);
1810
+ if (methodOverlayTimer) clearTimeout(methodOverlayTimer);
1811
+ if (paypalOverlayTimer) clearTimeout(paypalOverlayTimer);
1812
+ });
1813
+ const avsInput = (field, label, model, autocomplete, onChange) => h3("label", { "data-testid": `flopay-avs-${field}` }, [
1814
+ h3("span", label),
1815
+ h3("input", {
1816
+ name: field,
1817
+ autocomplete,
1818
+ value: model.value,
1819
+ disabled: externallyProcessing.value,
1820
+ "aria-invalid": field === "postal_code" && avsInvalid.value ? "true" : void 0,
1821
+ onInput: (event) => {
1822
+ const value = event.target.value;
1823
+ editedAvs.add(field);
1824
+ model.value = value;
1825
+ onChange?.(model.value);
1826
+ }
1827
+ })
1828
+ ]);
1829
+ const countrySelect = () => h3("label", { "data-testid": "flopay-avs-country" }, [
1830
+ h3("span", "Country"),
1831
+ h3(
1832
+ "select",
1833
+ {
1834
+ id: "flopay-billing-country",
1835
+ name: "billing-country",
1836
+ autocomplete: "billing country",
1837
+ "data-testid": "flopay-country",
1838
+ value: country.value,
1839
+ disabled: externallyProcessing.value,
1840
+ onChange: (event) => {
1841
+ const value = event.target.value;
1842
+ editedAvs.add("country");
1843
+ country.value = value;
1844
+ region.value = "";
1845
+ invokeMerchantCallback2(() => emit("countryChange", value));
1846
+ }
1847
+ },
1848
+ [
1849
+ country.value === "" ? h3("option", { value: "" }, "Select country") : null,
1850
+ ...COUNTRY_OPTIONS.map(
1851
+ (option) => h3(
1852
+ "option",
1853
+ { key: option.code, value: option.code },
1854
+ `${option.flag} ${option.name}`
1855
+ )
1856
+ )
1857
+ ]
1858
+ )
1859
+ ]);
1860
+ const avsFields = () => {
1861
+ if (!avsConfig.value) return null;
1862
+ return h3(
1863
+ "div",
1864
+ {
1865
+ "data-testid": "flopay-avs-fields",
1866
+ style: `display:flex;flex-wrap:wrap;gap:8px;flex-direction:${props.avsLayout === "column" ? "column" : "row"}`
1867
+ },
1868
+ [
1869
+ avsVisible("country") ? countrySelect() : null,
1870
+ avsVisible("address_line_1") ? avsInput("address_line_1", "Address", addressLine1, "address-line1") : null,
1871
+ avsVisible("address_line_2") ? avsInput("address_line_2", "Apt, suite, etc.", addressLine2, "address-line2") : null,
1872
+ avsVisible("city") ? avsInput("city", "City", city, "address-level2") : null,
1873
+ avsVisible("state") ? avsInput("state", "State / Province", region, "address-level1") : null,
1874
+ avsVisible("postal_code") ? avsInput(
1875
+ "postal_code",
1876
+ "ZIP / Postal code",
1877
+ zipCode,
1878
+ "postal-code",
1879
+ (value) => invokeMerchantCallback2(() => emit("zipChange", value))
1880
+ ) : null
1881
+ ]
1882
+ );
1883
+ };
1884
+ const OVERLAY_TEXT = {
1885
+ processing: "Processing payment\u2026",
1886
+ success: "Payment successful",
1887
+ failed: "Payment failed"
1888
+ };
1889
+ const overlayNode = (state) => state ? h3(
1890
+ "div",
1891
+ {
1892
+ role: "status",
1893
+ "aria-live": "polite",
1894
+ "data-testid": "flopay-processing-overlay",
1895
+ "data-state": state
1896
+ },
1897
+ OVERLAY_TEXT[state]
1898
+ ) : null;
1899
+ const processingOverlay = () => overlayNode(overlay.value);
1900
+ const setMethodOverlay = (state) => {
1901
+ if (methodOverlayTimer) clearTimeout(methodOverlayTimer);
1902
+ methodOverlayTimer = null;
1903
+ methodOverlay.value = state;
1904
+ };
1905
+ const holdMethodFailure = () => {
1906
+ setMethodOverlay("failed");
1907
+ methodOverlayTimer = setTimeout(() => {
1908
+ methodOverlayTimer = null;
1909
+ methodOverlay.value = null;
1910
+ }, ERROR_DELAY_MS);
1911
+ };
1912
+ let walletSheetRefusal = null;
1913
+ const methodRelay = {
1914
+ onSubmitting: () => setMethodOverlay("processing"),
1915
+ onComplete: (result) => {
1916
+ setMethodOverlay("success");
1917
+ setTimeout(() => {
1918
+ if (methodCompleted) return;
1919
+ methodCompleted = true;
1920
+ invokeMerchantCallback2(() => emit("complete", result));
1921
+ }, SUCCESS_DELAY_MS);
1922
+ },
1923
+ onDecline: (decline) => {
1924
+ holdMethodFailure();
1925
+ emitDecline(decline);
1926
+ },
1927
+ onError: (error) => {
1928
+ if (methodOverlay.value === "processing") holdMethodFailure();
1929
+ if (error.details?.providerCode === WALLET_SHEET_WINDOW_TOO_SMALL_CODE) {
1930
+ walletSheetRefusal = error.message;
1931
+ failure.value = error.message;
1932
+ }
1933
+ emitError(error);
1934
+ }
1935
+ };
1936
+ const runStripeBeforeButtonClick = (method) => {
1937
+ if (walletSheetRefusal !== null && failure.value === walletSheetRefusal) {
1938
+ failure.value = null;
1939
+ }
1940
+ walletSheetRefusal = null;
1941
+ return runBeforeButtonClick(method);
1942
+ };
1943
+ const cardSection = () => {
1944
+ if (!vaultCapable.value) return null;
1945
+ const mount = vault.value;
1946
+ const fields = cardOpen.value && capture.value && mount ? h3(VaultCardFields, {
1947
+ capture: capture.value,
1948
+ html: mount.html,
1949
+ messageToken: mount.messageToken,
1950
+ expectedOrigin: mount.expectedOrigin,
1951
+ verificationRequired: mount.verificationRequired,
1952
+ theme: vaultTheme.value,
1953
+ submitLabel: submitLabel.value,
1954
+ onError: (message) => {
1955
+ failure.value = message;
1956
+ },
1957
+ onValidation: (message) => {
1958
+ failure.value = message;
1959
+ }
1960
+ }) : null;
1961
+ const isButtons = props.layout === "buttons";
1962
+ const card = fields ? h3(
1963
+ "div",
1964
+ {
1965
+ "data-testid": "flopay-card-container",
1966
+ style: { ...bStyles.value.cardFormContainer, position: "relative" }
1967
+ },
1968
+ [
1969
+ isButtons ? null : defaultLayoutTitle(),
1970
+ slots.cardPreFormSlot ? h3(
1971
+ "div",
1972
+ { "data-testid": "flopay-card-pre-form", style: { width: "100%" } },
1973
+ slots.cardPreFormSlot()
1974
+ ) : null,
1975
+ avsFields(),
1976
+ fields,
1977
+ processingOverlay()
1978
+ ]
1979
+ ) : null;
1980
+ if (!isButtons) return card;
1981
+ return h3("div", { "data-testid": "flopay-card-section" }, [cardHeader(), card]);
1982
+ };
1983
+ const slotContent = (slot, fallback) => {
1984
+ if (!slot) return { content: fallback, empty: false };
1985
+ const nodes = slot();
1986
+ return { content: nodes, empty: isEmptySlotOutput(nodes) };
1987
+ };
1988
+ const defaultLayoutTitle = () => {
1989
+ const title = slotContent(slots.cardTitleContent, "Secure card checkout");
1990
+ if (title.empty) return null;
1991
+ return h3(
1992
+ "div",
1993
+ {
1994
+ "data-testid": "flopay-card-title",
1995
+ style: {
1996
+ textAlign: "center",
1997
+ fontWeight: 600,
1998
+ fontSize: "1.1rem",
1999
+ padding: "0.5rem 0",
2000
+ color: "#262833",
2001
+ ...bStyles.value.title
2002
+ }
2003
+ },
2004
+ title.content
2005
+ );
2006
+ };
2007
+ const expandCard = () => {
2008
+ if (cardOpen.value) return;
2009
+ cardOpen.value = true;
2010
+ cardRequested.value = true;
2011
+ vaultRecoveryAttempt.value += 1;
2012
+ failure.value = null;
2013
+ provider?.emitInstrument({ class: "card_surface" });
2014
+ invokeMerchantCallback2(() => emit("buttonClick", "card"));
2015
+ };
2016
+ const collapseToButtons = () => {
2017
+ cardOpen.value = false;
2018
+ failure.value = null;
2019
+ };
2020
+ const cardHeader = () => {
2021
+ const back = slotContent(slots.cardBackButtonContent, "Go back");
2022
+ const title = slotContent(slots.cardTitleContent, "Secure card checkout");
2023
+ return h3(
2024
+ "div",
2025
+ {
2026
+ "data-testid": "flopay-card-header",
2027
+ style: { display: "flex", alignItems: "center", padding: "0.75rem 0 0.625rem" }
2028
+ },
2029
+ [
2030
+ h3(
2031
+ "button",
2032
+ {
2033
+ type: "button",
2034
+ "aria-label": "Back to payment methods",
2035
+ "data-testid": "flopay-card-back-button",
2036
+ onClick: collapseToButtons,
2037
+ style: {
2038
+ display: "inline-flex",
2039
+ alignItems: "center",
2040
+ gap: back.empty ? 0 : "0.5rem",
2041
+ background: "none",
2042
+ border: "none",
2043
+ cursor: "pointer",
2044
+ color: "#4b5563",
2045
+ fontSize: bStyles.value.backButtonFontSize ?? "0.85rem",
2046
+ fontWeight: 500,
2047
+ padding: 0,
2048
+ flexShrink: 0,
2049
+ ...bStyles.value.backButton
2050
+ }
2051
+ },
2052
+ [
2053
+ h3(
2054
+ "span",
2055
+ {
2056
+ style: {
2057
+ display: "inline-flex",
2058
+ alignItems: "center",
2059
+ justifyContent: "center",
2060
+ width: "28px",
2061
+ height: "28px",
2062
+ borderRadius: "50%",
2063
+ backgroundColor: "#f3f4f6",
2064
+ ...bStyles.value.backButtonIcon
2065
+ }
2066
+ },
2067
+ [
2068
+ h3(
2069
+ "svg",
2070
+ {
2071
+ "aria-hidden": "true",
2072
+ width: "14",
2073
+ height: "14",
2074
+ viewBox: "0 0 24 24",
2075
+ fill: "none",
2076
+ stroke: "currentColor",
2077
+ "stroke-width": "2.5",
2078
+ "stroke-linecap": "round",
2079
+ "stroke-linejoin": "round"
2080
+ },
2081
+ [h3("path", { d: "M15 18l-6-6 6-6" })]
2082
+ )
2083
+ ]
2084
+ ),
2085
+ back.empty ? null : back.content
2086
+ ]
2087
+ ),
2088
+ title.empty ? h3("div", { style: { flex: 1 } }) : h3(
2089
+ "div",
2090
+ {
2091
+ "data-testid": "flopay-card-title",
2092
+ style: {
2093
+ flex: 1,
2094
+ textAlign: "center",
2095
+ fontWeight: 600,
2096
+ fontSize: bStyles.value.titleFontSize ?? "1.05rem",
2097
+ color: "#262833",
2098
+ paddingRight: "80px",
2099
+ ...bStyles.value.title
2100
+ }
2101
+ },
2102
+ title.content
2103
+ )
2104
+ ]
2105
+ );
2106
+ };
2107
+ const cardTile = () => h3(
2108
+ "button",
2109
+ {
2110
+ type: "button",
2111
+ "aria-expanded": false,
2112
+ "data-testid": "flopay-card-button",
2113
+ disabled: externallyProcessing.value,
2114
+ style: {
2115
+ // React sizes the default tile to the other buttons; custom content pads instead.
2116
+ ...slots.cardButtonContent ? { padding: "0.9rem 1rem" } : {
2117
+ boxSizing: "border-box",
2118
+ height: `${DEFAULT_BUTTONS_LAYOUT_BUTTON_HEIGHT}px`,
2119
+ padding: "0 1rem"
2120
+ },
2121
+ ...cardTileStyle.value,
2122
+ fontWeight: 600,
2123
+ // React dims the tile and shows a not-allowed cursor while processing.
2124
+ cursor: externallyProcessing.value ? "not-allowed" : "pointer",
2125
+ ...externallyProcessing.value ? { opacity: 0.6 } : {},
2126
+ display: "flex",
2127
+ alignItems: "center",
2128
+ justifyContent: "center",
2129
+ gap: "0.625rem",
2130
+ position: "relative"
2131
+ },
2132
+ onClick: () => {
2133
+ if (externallyProcessing.value) return;
2134
+ if (!props.onBeforeButtonClick) {
2135
+ expandCard();
2136
+ return;
2137
+ }
2138
+ void runBeforeButtonClick("card").then((beforeClick) => {
2139
+ if (beforeClick.proceed) expandCard();
2140
+ });
2141
+ }
2142
+ },
2143
+ slots.cardButtonContent ? slots.cardButtonContent() : defaultCardButtonContent()
2144
+ );
2145
+ return () => {
2146
+ if (!session.value && !sessionId.value) return h3("div", { "data-testid": "flopay-form" });
2147
+ if (unsupported.value) {
2148
+ return h3("div", { role: "alert", "data-testid": "flopay-form-error" }, [
2149
+ noVaultCapabilityError().message
2150
+ ]);
2151
+ }
2152
+ const relay = {
2153
+ onComplete: (result) => invokeMerchantCallback2(() => emit("complete", result)),
2154
+ onDecline: emitDecline,
2155
+ onError: emitError
2156
+ };
2157
+ const paypalButton = directPaypal.value && sessionId.value && directPayPalCanWaitForAttachment.value ? h3(DirectPayPalButton, {
2158
+ sessionId: sessionId.value,
2159
+ nonce: nonce.value,
2160
+ billingApiUrl: billingApiUrl.value,
2161
+ email: accountPatch.value?.email ?? props.email,
2162
+ // React's pass-through: identity, the click gate, the processing
2163
+ // state, the click notice and the error channel.
2164
+ billingName: billingName.value || void 0,
2165
+ onBuyerIdentityReady: props.onBuyerIdentityReady,
2166
+ runBeforeButtonClick,
2167
+ isProcessing: externallyProcessing.value,
2168
+ clientId: directPaypal.value.clientId,
2169
+ environment: directPaypal.value.environment,
2170
+ // React's `directPaypal` pass-through. The session's object type
2171
+ // and presentation still win inside the button.
2172
+ providerObjectType: directPaypal.value.providerObjectType,
2173
+ approvalPresentation: directPaypal.value.approvalPresentation,
2174
+ existingOrderId: paypalRetry.value?.orderId,
2175
+ currency: resolvedCurrency.value.toUpperCase(),
2176
+ isSubscription: resolvedIsSubscription.value,
2177
+ session: session.value,
2178
+ // React's `debug={debug}`: the button's lifecycle tracer.
2179
+ debug: props.debug,
2180
+ ...relay,
2181
+ // React's `onTokenizedBody={dispatchTokenizedBody}`: the form
2182
+ // continues the approval (or hands it to the host's handler).
2183
+ onTokenizedBody: dispatchTokenizedBody,
2184
+ onButtonClick: (method) => invokeMerchantCallback2(() => emit("buttonClick", method)),
2185
+ onErrorChange: (message) => {
2186
+ failure.value = message;
2187
+ },
2188
+ // React's `onInitializationStateChange={setDirectPaypalInitializationState}`.
2189
+ onInitializationStateChange: (state) => {
2190
+ directPaypalInitializationState.value = state;
2191
+ }
2192
+ }) : null;
2193
+ const paypalNode = paypalButton ? h3(Fragment, null, [
2194
+ paypalRetry.value?.orderId ? h3(
2195
+ "div",
2196
+ {
2197
+ "data-testid": "flopay-paypal-direct-retry-notice",
2198
+ style: {
2199
+ padding: "8px 10px",
2200
+ background: "#fef3c7",
2201
+ border: "1px solid #fcd34d",
2202
+ borderRadius: "6px",
2203
+ color: "#78350f",
2204
+ fontSize: "13px",
2205
+ lineHeight: 1.4
2206
+ }
2207
+ },
2208
+ "Please confirm your PayPal payment to complete checkout."
2209
+ ) : null,
2210
+ paypalButton,
2211
+ overlayNode(paypalOverlay.value)
2212
+ ]) : null;
2213
+ const amountInCents = props.totalAmount || 100;
2214
+ const renderStripe = renderedStripeMethods.value.length > 0 && Boolean(stripeKey.value) && Boolean(sessionId.value) && !nonCardAttachmentBlocked.value;
2215
+ const stripeNode = renderStripe && stripeKey.value && sessionId.value ? h3("div", { style: "position:relative", "data-testid": "flopay-stripe-section" }, [
2216
+ h3(StripePaymentMethods, {
2217
+ // Stripe's Elements are created once per mount, so the row
2218
+ // remounts when the direct PayPal fallback adds or removes
2219
+ // Stripe PayPal.
2220
+ key: renderedStripeMethods.value.includes("paypal") ? "stripe-with-paypal" : "stripe-without-paypal",
2221
+ session: session.value,
2222
+ sessionId: sessionId.value,
2223
+ nonce: nonce.value,
2224
+ billingApiUrl: billingApiUrl.value,
2225
+ publishableKey: stripeKey.value,
2226
+ methods: renderedStripeMethods.value,
2227
+ currency: resolvedCurrency.value,
2228
+ amount: amountInCents,
2229
+ // The wallet row's mode follows the real total (React's
2230
+ // `resolveWalletElementsMode(totalAmount, …)`).
2231
+ totalAmount: props.totalAmount,
2232
+ appearance: appearance.value,
2233
+ // React's Stripe PayPal group: with a redirect presentation,
2234
+ // PayPal leaves the wallet row for the redirect tile, which
2235
+ // runs the click gate and continues on the form's `/process`.
2236
+ paypalPresentation: stripePayPalPresentation.value,
2237
+ runBeforeButtonClick: runStripeBeforeButtonClick,
2238
+ onTokenizedBody: dispatchTokenizedBody,
2239
+ // React's `isProcessing={isSubmitting}`.
2240
+ isProcessing: stripeMethodsProcessing.value,
2241
+ // React restores the click-time patch from storage; Vue asks
2242
+ // the merchant's gate again on an authorized redirect return.
2243
+ recoverAccountPatch: recoverStripePayPalAccount,
2244
+ ...methodRelay
2245
+ }),
2246
+ overlayNode(methodOverlay.value)
2247
+ ]) : null;
2248
+ const failureNode = displayError.value ? h3("div", { role: "alert" }, displayError.value) : null;
2249
+ const gateDebug = (suffix) => {
2250
+ if (!props.debug) return [];
2251
+ const listed = props.enabledPaymentMethods ?? session.value?.gateways?.stripe?.enabledPaymentMethods;
2252
+ const express = renderedStripeMethods.value.filter(isStripeExpressMethod);
2253
+ const listedOthers = stripeMethods.value.filter((m) => !isStripeExpressMethod(m));
2254
+ const renderedOthers = renderedStripeMethods.value.filter((m) => !isStripeExpressMethod(m));
2255
+ const panel = (testId, lines) => h3(
2256
+ "pre",
2257
+ { "data-testid": `${testId}${suffix}`, style: GATE_DEBUG_STYLE },
2258
+ lines.join("\n")
2259
+ );
2260
+ return [
2261
+ !props.showPayPal || !directPaypalConfigured.value ? panel("flopay-direct-paypal-gate-debug", ["FloPay/DirectPayPal-debug - not enabled"]) : panel("flopay-direct-paypal-gate-debug", [
2262
+ "FloPay/DirectPayPal-debug (parent gate)",
2263
+ ` showPayPal=${props.showPayPal}`,
2264
+ ` directPaypalConfigured=${directPaypalConfigured.value}`,
2265
+ ` inAppBrowserDetected=${String(inAppBrowserDetected())}`,
2266
+ ` shouldRenderDirectPayPal=${Boolean(paypalButton)}`,
2267
+ ` shouldRenderStripePayPal=${renderStripe && renderedStripeMethods.value.includes("paypal")}`
2268
+ ]),
2269
+ !props.showStripe || !stripeKey.value ? panel("flopay-stripe-gate-debug", ["FloPay/Stripe-debug - not enabled"]) : panel("flopay-stripe-gate-debug", [
2270
+ "FloPay/Stripe-debug (parent gate)",
2271
+ ` showStripe=${props.showStripe}`,
2272
+ ` currency=${resolvedCurrency.value}`,
2273
+ ` amountInCents=${amountInCents}`,
2274
+ ` enabledPaymentMethods: ${JSON.stringify(listed ?? [])}`,
2275
+ ` enabledPaymentMethodsProvided=${Array.isArray(listed)}`,
2276
+ ` hasEnabledMethods=${Array.isArray(listed) && listed.length > 0}`,
2277
+ ` expressMethods: ${JSON.stringify(express)}`,
2278
+ ` paymentElementMethods: ${JSON.stringify(listedOthers)}`,
2279
+ ` paymentElementMethodsForCurrency: ${JSON.stringify(renderedOthers)}`,
2280
+ ` shouldRenderWallets=${renderStripe && express.length > 0}`,
2281
+ ` shouldRenderPaymentElement=${renderStripe && renderedOthers.length > 0}`
2282
+ ])
2283
+ ];
2284
+ };
2285
+ if (props.layout !== "buttons") {
2286
+ return h3("div", { "data-testid": "flopay-form", class: props.className }, [
2287
+ failureNode,
2288
+ ...gateDebug("-default"),
2289
+ paypalNode,
2290
+ stripeNode,
2291
+ cardSection()
2292
+ ]);
2293
+ }
2294
+ const cardView = cardOpen.value && vaultCapable.value;
2295
+ return h3("div", { "data-testid": "flopay-form", class: props.className }, [
2296
+ failureNode,
2297
+ h3(
2298
+ "div",
2299
+ {
2300
+ "data-testid": "flopay-buttons-panel",
2301
+ "aria-hidden": cardView ? "true" : void 0,
2302
+ style: cardView ? { display: "none" } : { display: "flex", flexDirection: "column", gap: "0.5rem" }
2303
+ },
2304
+ [...gateDebug(""), paypalNode, stripeNode, vaultCapable.value ? cardTile() : null]
2305
+ ),
2306
+ cardView ? cardSection() : null
2307
+ ]);
2308
+ };
2309
+ }
2310
+ });
2311
+
2312
+ // src/flopay-checkout.ts
2313
+ function toFloPayError(error, fallback) {
2314
+ if (error instanceof FloPayError4) return error;
2315
+ return new FloPayError4(error instanceof Error ? error.message : fallback, "api_error");
2316
+ }
2317
+ async function resolveInlineSession(api, params) {
2318
+ const canonicalIdentity = serializeCreateParams(params);
2319
+ const cacheKey = hashCreateParams(params);
2320
+ const suppliedIdempotencyKey = params.idempotencyKey === void 0 ? void 0 : resolveIdempotencyKey(params.idempotencyKey);
2321
+ const cached = readCachedInlineSession(cacheKey, canonicalIdentity);
2322
+ let cachedIdempotency = readCachedInlineIdempotencyKey(cacheKey, canonicalIdentity);
2323
+ if (cached?.canonicalIdentity === canonicalIdentity && cachedIdempotency && cachedIdempotency.canonicalIdentity === void 0) {
2324
+ persistCachedInlineIdempotencyKey(
2325
+ cacheKey,
2326
+ canonicalIdentity,
2327
+ cachedIdempotency.idempotencyKey
2328
+ );
2329
+ cachedIdempotency = { ...cachedIdempotency, canonicalIdentity };
2330
+ }
2331
+ let idempotencyKey = suppliedIdempotencyKey ?? (cachedIdempotency?.canonicalIdentity === canonicalIdentity ? cachedIdempotency.idempotencyKey : void 0);
2332
+ let rotateIdempotencyKey = false;
2333
+ if (cached) {
2334
+ try {
2335
+ const reused = await api.getUnifiedCheckoutSession(
2336
+ cached.sid,
2337
+ cached.nonce
2338
+ );
2339
+ let reusable = true;
2340
+ if (cached.canonicalIdentity === void 0) {
2341
+ if (legacyCachedSessionMatchesDraft(reused.data.session ?? void 0, params)) {
2342
+ persistCachedInlineSession(
2343
+ cacheKey,
2344
+ canonicalIdentity,
2345
+ cached.sid,
2346
+ cached.nonce ?? reused.data.session?.clientSecret
2347
+ );
2348
+ if (cachedIdempotency && cachedIdempotency.canonicalIdentity === void 0) {
2349
+ persistCachedInlineIdempotencyKey(
2350
+ cacheKey,
2351
+ canonicalIdentity,
2352
+ cachedIdempotency.idempotencyKey
2353
+ );
2354
+ idempotencyKey ?? (idempotencyKey = cachedIdempotency.idempotencyKey);
2355
+ }
2356
+ } else {
2357
+ clearCachedInlineCheckout(cacheKey, canonicalIdentity);
2358
+ idempotencyKey = suppliedIdempotencyKey;
2359
+ reusable = false;
2360
+ }
2361
+ }
2362
+ if (reusable && reused.data.session?.status === "complete") {
2363
+ if (wasSessionRecentlyCompleted(cached.sid)) return { result: reused };
2364
+ clearCachedInlineCheckout(cacheKey, canonicalIdentity);
2365
+ reusable = false;
2366
+ rotateIdempotencyKey = true;
2367
+ }
2368
+ if (reusable) return { result: reused };
2369
+ } catch {
2370
+ if (cached.canonicalIdentity === void 0) {
2371
+ clearCachedInlineCheckout(cacheKey, canonicalIdentity);
2372
+ idempotencyKey = suppliedIdempotencyKey;
2373
+ } else {
2374
+ clearCachedInlineSession(cacheKey, canonicalIdentity);
2375
+ }
2376
+ }
2377
+ }
2378
+ if (!suppliedIdempotencyKey && (rotateIdempotencyKey || !idempotencyKey)) {
2379
+ idempotencyKey = resolveIdempotencyKey();
2380
+ }
2381
+ if (idempotencyKey) {
2382
+ persistCachedInlineIdempotencyKey(cacheKey, canonicalIdentity, idempotencyKey);
2383
+ }
2384
+ const createParams = { ...params, idempotencyKey };
2385
+ if (!isDetachedSessionEligible(createParams)) {
2386
+ const created = await api.createAndFetchSession(createParams);
2387
+ const sid = created.data.session?.id;
2388
+ if (sid) {
2389
+ persistCachedInlineSession(
2390
+ cacheKey,
2391
+ canonicalIdentity,
2392
+ sid,
2393
+ created.data.session?.clientSecret
2394
+ );
2395
+ }
2396
+ return { result: created };
2397
+ }
2398
+ const detached = await api.createDetachedSession({ ...createParams, deferDataAttachment: true });
2399
+ const claimed = detached.claimed.then(
2400
+ (claimedResult) => {
2401
+ const claimedSid = claimedResult.data.session?.id ?? detached.sessionId;
2402
+ if (claimedSid) {
2403
+ persistCachedInlineSession(
2404
+ cacheKey,
2405
+ canonicalIdentity,
2406
+ claimedSid,
2407
+ claimedResult.data.session?.clientSecret ?? detached.nonce
2408
+ );
2409
+ }
2410
+ return claimedResult;
2411
+ },
2412
+ (error) => {
2413
+ clearCachedInlineSession(cacheKey, canonicalIdentity);
2414
+ throw error;
2415
+ }
2416
+ );
2417
+ return { result: detached.shell, claimed };
2418
+ }
2419
+ var FloPayCheckout = defineComponent4({
2420
+ name: "FloPayCheckout",
2421
+ props: {
2422
+ sessionId: { type: String, default: void 0 },
2423
+ nonce: { type: String, default: void 0 },
2424
+ createSession: { type: Object, default: void 0 },
2425
+ checkoutMode: { type: String, default: void 0 },
2426
+ billingApiUrl: { type: String, default: void 0 },
2427
+ telemetry: { type: Boolean, default: void 0 },
2428
+ locale: { type: String, default: void 0 },
2429
+ layout: { type: String, default: "default" },
2430
+ /**
2431
+ * React's `walletPresentation` (default `'auto'`): the preferred approval
2432
+ * presentation for Stripe-routed PayPal, passed to the built-in form and
2433
+ * to a custom-layout form that leaves its own unset.
2434
+ */
2435
+ walletPresentation: { type: String, default: "auto" },
2436
+ showPayPal: { type: Boolean, default: true },
2437
+ showStripe: { type: Boolean, default: true },
2438
+ enabledPaymentMethods: { type: Array, default: void 0 },
2439
+ /** @deprecated Session-driven visibility decides (React parity). */
2440
+ showApplePay: { type: Boolean, default: true },
2441
+ /** @deprecated Session-driven visibility decides (React parity). */
2442
+ showGooglePay: { type: Boolean, default: true },
2443
+ theme: { type: String, default: void 0 },
2444
+ appearance: { type: Object, default: void 0 },
2445
+ buttonsTheme: { type: String, default: void 0 },
2446
+ buttonsStyles: { type: Object, default: void 0 },
2447
+ className: { type: String, default: void 0 },
2448
+ submitLabel: { type: String, default: void 0 },
2449
+ enableAVS: {
2450
+ type: [Boolean, Object],
2451
+ default: void 0
2452
+ },
2453
+ avsLayout: { type: String, default: void 0 },
2454
+ cardFieldOrder: { type: Array, default: void 0 },
2455
+ /** @deprecated Not rendered (React parity); see {@link FloPayCheckoutProps.showSecurityFooter}. */
2456
+ showSecurityFooter: { type: Boolean, default: void 0 },
2457
+ /** @deprecated Ignored (React parity); see {@link FloPayCheckoutProps.fallbackPublishableKey}. */
2458
+ fallbackPublishableKey: { type: String, default: void 0 },
2459
+ confirmLabel: { type: String, default: void 0 },
2460
+ initialErrorMessage: { type: String, default: null },
2461
+ /**
2462
+ * React's `onBeforeButtonClick` (a function prop: its result is read),
2463
+ * passed to the built-in `SplitCardForm`. It runs before the buttons-layout
2464
+ * card tile opens and before PayPal creates its order.
2465
+ */
2466
+ onBeforeButtonClick: {
2467
+ type: Function,
2468
+ default: void 0
2469
+ },
2470
+ initialPayPalOrderId: { type: String, default: void 0 },
2471
+ debug: { type: Boolean, default: false }
2472
+ },
2473
+ emits: {
2474
+ complete: (_result) => true,
2475
+ error: (_error) => true,
2476
+ instrument: (_event) => true,
2477
+ decline: (_decline) => true,
2478
+ buttonClick: (_method) => true,
2479
+ countryChange: (_country) => true,
2480
+ // React's `onZipChange`.
2481
+ zipChange: (_zip) => true,
2482
+ // React's `onSessionCompleted`: the session was already paid when it loaded.
2483
+ sessionCompleted: (_successUrl) => true
2484
+ },
2485
+ setup(props, { emit, slots }) {
2486
+ const state = reactive({
2487
+ session: null,
2488
+ loading: true,
2489
+ error: null,
2490
+ claimPending: false,
2491
+ // React's `claimRequiresIdentity`. React's checkout sets it from its
2492
+ // session bootstrap, where the value is currently always `false`
2493
+ // (`bootstrapClaimRequiresIdentity`), so it stays `false` here too.
2494
+ claimRequiresIdentity: false,
2495
+ inlineSessionDraft: props.createSession
2496
+ });
2497
+ watch3(
2498
+ () => props.createSession,
2499
+ (draft) => {
2500
+ state.inlineSessionDraft = draft;
2501
+ }
2502
+ );
2503
+ const flopay = shallowRef3(null);
2504
+ const stripeKey = shallowRef3(void 0);
2505
+ const stripeCountryGate = shallowRef3(void 0);
2506
+ let active = true;
2507
+ const billingApiUrl = () => resolveBillingApiUrl3(props.billingApiUrl);
2508
+ const mode = ref4("full");
2509
+ const modeError = ref4(props.initialErrorMessage ?? null);
2510
+ const savedPaymentRunning = ref4(false);
2511
+ const settled = ref4(false);
2512
+ const lifecycle = new AbortController();
2513
+ watch3(
2514
+ () => props.initialErrorMessage,
2515
+ (message) => {
2516
+ modeError.value = message ?? null;
2517
+ }
2518
+ );
2519
+ provide(CHECKOUT_STATE, state);
2520
+ provide(CHECKOUT_FORM_DEFAULTS, () => ({
2521
+ sessionId: state.session?.id ?? props.sessionId,
2522
+ nonce: state.session?.clientSecret || props.nonce,
2523
+ billingApiUrl: billingApiUrl(),
2524
+ session: state.session,
2525
+ submitLabel: props.submitLabel,
2526
+ dataAttachmentPending: Boolean(state.claimPending),
2527
+ // React's `SessionInjector`: `if (session?.customer) … injected.userId =
2528
+ // session.customer.id` for a child whose own `userId` is empty.
2529
+ userId: state.session?.customer?.id || void 0,
2530
+ // React's `SessionInjector`: `if (existing.walletPresentation === undefined)
2531
+ // injected.walletPresentation = walletPresentation`.
2532
+ walletPresentation: props.walletPresentation
2533
+ }));
2534
+ const fail = (error) => {
2535
+ if (!active) return;
2536
+ const failure = toFloPayError(error, "Checkout failed to load.");
2537
+ state.error = failure;
2538
+ state.loading = false;
2539
+ state.claimPending = false;
2540
+ invokeMerchantCallback3(() => emit("error", failure));
2541
+ };
2542
+ const adopt = (result) => {
2543
+ if (!active) return;
2544
+ state.session = result.data.session ?? state.session;
2545
+ stripeCountryGate.value = result.data.stripe?.enabledPaymentMethodCountries;
2546
+ const publishableKey = result.data.stripe?.publishableKey ?? state.session?.gateways?.stripe?.publishableKey;
2547
+ if (publishableKey && publishableKey !== stripeKey.value) {
2548
+ stripeKey.value = publishableKey;
2549
+ const loading = loadFloPay(publishableKey, {
2550
+ billingApiUrl: billingApiUrl(),
2551
+ ...props.locale ? { locale: props.locale } : {},
2552
+ ...props.telemetry === void 0 ? {} : { telemetry: props.telemetry }
2553
+ });
2554
+ void loading.catch(() => {
2555
+ });
2556
+ flopay.value = loading;
2557
+ }
2558
+ };
2559
+ const createApi = () => new PaymentAPI3(billingApiUrl(), props.telemetry === false ? { telemetry: false } : void 0);
2560
+ const confirmReady = () => Boolean(
2561
+ (state.session?.id || props.sessionId) && (state.session?.clientSecret || props.nonce)
2562
+ );
2563
+ const runSavedPayment = async (unified) => {
2564
+ const session = state.session;
2565
+ if (!session || savedPaymentRunning.value) return;
2566
+ modeError.value = null;
2567
+ savedPaymentRunning.value = true;
2568
+ const sessionId = session.id || props.sessionId || null;
2569
+ try {
2570
+ const result = await chargeSavedPayment({
2571
+ api: createApi(),
2572
+ billingApiUrl: billingApiUrl(),
2573
+ session,
2574
+ nonce: session.clientSecret || props.nonce,
2575
+ unified,
2576
+ signal: lifecycle.signal
2577
+ });
2578
+ if (sessionId) markSessionRecentlyCompleted2(sessionId);
2579
+ settled.value = true;
2580
+ await new Promise((resolve) => setTimeout(resolve, SAVED_PAYMENT_SUCCESS_DELAY_MS));
2581
+ invokeMerchantCallback3(() => emit("complete", result));
2582
+ } catch (error) {
2583
+ const failure = coerceError(error, "Payment failed. Please try again.");
2584
+ if (failure.code === "three_ds_aborted") return;
2585
+ modeError.value = failure.message;
2586
+ mode.value = "full";
2587
+ invokeMerchantCallback3(() => emit("error", failure));
2588
+ if (sessionId) {
2589
+ const decline = {
2590
+ method: "card",
2591
+ message: failure.message,
2592
+ ...failure.code ? { code: failure.code } : {},
2593
+ ...failure.declineCode ? { declineCode: failure.declineCode } : {},
2594
+ sessionId
2595
+ };
2596
+ invokeMerchantCallback3(() => emit("decline", decline));
2597
+ }
2598
+ } finally {
2599
+ savedPaymentRunning.value = false;
2600
+ }
2601
+ };
2602
+ const confirmPurchase = () => {
2603
+ if (savedPaymentRunning.value || !confirmReady()) return;
2604
+ void runSavedPayment();
2605
+ };
2606
+ const settleMode = async (result) => {
2607
+ const session = state.session;
2608
+ if (!active || !session) return;
2609
+ const sessionMode = session.checkoutMode;
2610
+ if (sessionMode === "setup") {
2611
+ state.session = null;
2612
+ throw new FloPayError4(
2613
+ "This is a card setup session. Use FloPayCardSetup instead of FloPayCheckout.",
2614
+ "validation_error",
2615
+ { code: "InvalidCardSetupSurface", param: "sessionId" }
2616
+ );
2617
+ }
2618
+ if (session.status === "authorized" || session.status === "complete") {
2619
+ settled.value = true;
2620
+ const successUrl = session.successUrl ?? "";
2621
+ if (session.status === "authorized") {
2622
+ const completed = terminalResult(session, session.id || props.sessionId || null);
2623
+ invokeMerchantCallback3(() => emit("complete", completed));
2624
+ }
2625
+ invokeMerchantCallback3(() => emit("sessionCompleted", successUrl));
2626
+ return;
2627
+ }
2628
+ mode.value = props.checkoutMode ?? sessionMode ?? "full";
2629
+ const paypalReturn = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("payment_intent");
2630
+ if (mode.value === "auto" && !paypalReturn) {
2631
+ await runSavedPayment(result);
2632
+ }
2633
+ };
2634
+ const start = async () => {
2635
+ const api = createApi();
2636
+ if (props.sessionId) {
2637
+ const result = await api.getUnifiedCheckoutSession(props.sessionId, props.nonce);
2638
+ adopt(result);
2639
+ await settleMode(result);
2640
+ return;
2641
+ }
2642
+ if (!props.createSession) {
2643
+ throw new FloPayError4(
2644
+ "FloPayCheckout requires a sessionId or createSession.",
2645
+ "validation_error"
2646
+ );
2647
+ }
2648
+ const params = {
2649
+ ...props.createSession,
2650
+ ...props.checkoutMode ? { checkoutMode: props.checkoutMode } : {}
2651
+ };
2652
+ Object.assign(params, {
2653
+ checkoutType: "embedded_checkout",
2654
+ checkoutLayout: props.layout,
2655
+ // AVS analytics metadata, as React sends it.
2656
+ ...props.enableAVS ? {
2657
+ avsCheck: true,
2658
+ avsConfig: typeof props.enableAVS === "object" ? props.enableAVS : void 0
2659
+ } : {}
2660
+ });
2661
+ const resolved = await resolveInlineSession(api, params);
2662
+ if (!resolved.claimed) {
2663
+ adopt(resolved.result);
2664
+ await settleMode(resolved.result);
2665
+ return;
2666
+ }
2667
+ state.claimPending = true;
2668
+ adopt(resolved.result);
2669
+ resolved.claimed.then(
2670
+ (claimed) => {
2671
+ if (!active) return;
2672
+ adopt(claimed);
2673
+ state.claimPending = false;
2674
+ },
2675
+ (error) => fail(error)
2676
+ );
2677
+ };
2678
+ onMounted3(() => {
2679
+ start().then(
2680
+ () => {
2681
+ if (active) state.loading = false;
2682
+ },
2683
+ (error) => fail(error)
2684
+ );
2685
+ });
2686
+ onBeforeUnmount4(() => {
2687
+ active = false;
2688
+ lifecycle.abort();
2689
+ });
2690
+ const clearModeError = () => {
2691
+ modeError.value = null;
2692
+ };
2693
+ const relay = {
2694
+ onComplete: (result) => {
2695
+ clearModeError();
2696
+ invokeMerchantCallback3(() => emit("complete", result));
2697
+ },
2698
+ onError: (error) => {
2699
+ clearModeError();
2700
+ invokeMerchantCallback3(() => emit("error", error));
2701
+ },
2702
+ onDecline: (decline) => {
2703
+ clearModeError();
2704
+ invokeMerchantCallback3(() => emit("decline", decline));
2705
+ },
2706
+ onButtonClick: (method) => {
2707
+ clearModeError();
2708
+ invokeMerchantCallback3(() => emit("buttonClick", method));
2709
+ }
2710
+ };
2711
+ const withProvider = (content) => h4(
2712
+ FloPayProvider,
2713
+ {
2714
+ flopay: flopay.value,
2715
+ options: { billingApiUrl: billingApiUrl() },
2716
+ // React's `onInstrument`: the provider's privacy-safe instrument feed.
2717
+ onInstrument: (event) => invokeMerchantCallback3(() => emit("instrument", event))
2718
+ },
2719
+ { default: content }
2720
+ );
2721
+ const confirmView = () => {
2722
+ const unavailable = savedPaymentRunning.value || !confirmReady();
2723
+ const button = slots.renderConfirmButton ? slots.renderConfirmButton({ onConfirm: confirmPurchase, isProcessing: unavailable }) : h4(
2724
+ "button",
2725
+ {
2726
+ type: "button",
2727
+ "data-testid": "flopay-confirm-button",
2728
+ onClick: confirmPurchase,
2729
+ disabled: unavailable,
2730
+ style: {
2731
+ width: "100%",
2732
+ padding: "0.875rem",
2733
+ backgroundColor: "#4A49FF",
2734
+ color: "white",
2735
+ border: "none",
2736
+ borderRadius: "8px",
2737
+ fontSize: "1rem",
2738
+ fontWeight: 600,
2739
+ cursor: unavailable ? "not-allowed" : "pointer",
2740
+ opacity: unavailable ? 0.6 : 1
2741
+ }
2742
+ },
2743
+ savedPaymentRunning.value ? "Processing..." : props.confirmLabel ?? "Confirm Purchase"
2744
+ );
2745
+ return h4("div", { class: props.className, "data-testid": "flopay-confirm" }, [
2746
+ modeError.value ? h4(
2747
+ "div",
2748
+ {
2749
+ role: "alert",
2750
+ "data-testid": "flopay-mode-error",
2751
+ style: {
2752
+ color: "#dc2626",
2753
+ fontSize: "0.875rem",
2754
+ marginBottom: "0.75rem",
2755
+ textAlign: "center"
2756
+ }
2757
+ },
2758
+ modeError.value
2759
+ ) : null,
2760
+ button
2761
+ ]);
2762
+ };
2763
+ return () => {
2764
+ if (state.error && !state.session) {
2765
+ if (slots.error) return slots.error({ error: state.error });
2766
+ return h4("div", { role: "alert", "data-testid": "flopay-load-error" }, state.error.message);
2767
+ }
2768
+ if (!state.session || mode.value === "auto" && savedPaymentRunning.value) {
2769
+ return h4(
2770
+ "div",
2771
+ { role: "status", "aria-busy": "true", "data-testid": "flopay-checkout-loading" },
2772
+ slots.loading?.() ?? "Loading checkout\u2026"
2773
+ );
2774
+ }
2775
+ if (settled.value) return null;
2776
+ if (mode.value === "confirm") {
2777
+ return withProvider(() => [slots.default?.(), confirmView()]);
2778
+ }
2779
+ const session = state.session;
2780
+ const modeNotice = modeError.value ? h4(
2781
+ "div",
2782
+ {
2783
+ role: "alert",
2784
+ "data-testid": "flopay-mode-error",
2785
+ style: {
2786
+ padding: "0.75rem 1rem",
2787
+ marginBottom: "0.75rem",
2788
+ backgroundColor: "#FEF3C7",
2789
+ border: "1px solid #F59E0B",
2790
+ borderRadius: "8px",
2791
+ color: "#92400E",
2792
+ fontSize: "0.875rem"
2793
+ }
2794
+ },
2795
+ modeError.value
2796
+ ) : null;
2797
+ const account = props.createSession?.account;
2798
+ const layoutSlot = slots.layout?.();
2799
+ const customLayout = layoutSlot && props.className ? h4("div", { class: props.className }, layoutSlot) : layoutSlot;
2800
+ const cardSlots = Object.fromEntries(
2801
+ Object.entries({
2802
+ cardButtonContent: slots.cardButtonContent,
2803
+ cardBackButtonContent: slots.cardBackButtonContent,
2804
+ cardTitleContent: slots.cardTitleContent,
2805
+ cardPreFormSlot: slots.cardPreFormSlot
2806
+ }).filter(([, slot]) => slot !== void 0)
2807
+ );
2808
+ const form = customLayout ?? h4(
2809
+ SplitCardForm,
2810
+ {
2811
+ sessionId: session.id || props.sessionId,
2812
+ nonce: session.clientSecret || props.nonce,
2813
+ billingApiUrl: billingApiUrl(),
2814
+ telemetry: props.telemetry,
2815
+ email: session.customer?.email || account?.email,
2816
+ firstName: session.customer?.firstName || account?.firstName,
2817
+ lastName: session.customer?.lastName || account?.lastName,
2818
+ // React: `userId={session?.customer?.id || effectiveCreateSession?.account?.userId}`.
2819
+ userId: session.customer?.id || account?.userId,
2820
+ layout: props.layout,
2821
+ showPayPal: props.showPayPal,
2822
+ showStripe: props.showStripe,
2823
+ enabledPaymentMethods: props.enabledPaymentMethods,
2824
+ showApplePay: props.showApplePay,
2825
+ showGooglePay: props.showGooglePay,
2826
+ // React: the bundle comes through `theme`; an explicit `appearance`
2827
+ // wins over the bundle's for the Stripe-side half.
2828
+ theme: props.theme,
2829
+ buttonsTheme: props.buttonsTheme,
2830
+ buttonsStyles: props.buttonsStyles,
2831
+ appearance: props.appearance ?? resolveTheme2(props.theme)?.appearance,
2832
+ className: props.className,
2833
+ submitLabel: props.submitLabel,
2834
+ enableAVS: props.enableAVS,
2835
+ // React: `avsCheck={!!enableAVS}`, reported in the `/account` snapshot.
2836
+ avsCheck: !!props.enableAVS,
2837
+ // React: `checkoutType={createSessionParams ? 'embedded_checkout' : 'standard_checkout'}`
2838
+ // and `checkoutLayout={resolveCheckoutLogLayout(children, layout)}`, the
2839
+ // analytics metadata on the form's own `/process`. The built-in form
2840
+ // renders only without a custom layout, as in React.
2841
+ checkoutType: props.createSession ? "embedded_checkout" : "standard_checkout",
2842
+ checkoutLayout: resolveCheckoutLogLayout(false, props.layout),
2843
+ avsLayout: props.avsLayout,
2844
+ cardFieldOrder: props.cardFieldOrder,
2845
+ session,
2846
+ dataAttachmentPending: Boolean(state.claimPending),
2847
+ dataAttachmentRequiresIdentity: Boolean(state.claimRequiresIdentity),
2848
+ // React: `walletPresentation={walletPresentation}` on its built-in form.
2849
+ walletPresentation: props.walletPresentation,
2850
+ onBeforeButtonClick: props.onBeforeButtonClick,
2851
+ // React fills these from the session for its built-in form only:
2852
+ // `directPaypal={directPaypalConfig}`,
2853
+ // `currency={session?.currency?.toLowerCase() ?? 'usd'}`,
2854
+ // `isSubscription={session?.mode === 'subscription'}`, the Stripe
2855
+ // country gate, and `debug={debug}`.
2856
+ // The shared resolver React uses. React passes the normalized
2857
+ // `data.paypal`, which `@flopay/js` copies field for field from
2858
+ // this `gateways.paypal` when it has a client id.
2859
+ directPaypal: resolveDirectPaypalConfig(
2860
+ session.gateways?.paypal,
2861
+ props.initialPayPalOrderId
2862
+ ),
2863
+ currency: session.currency?.toLowerCase() ?? "usd",
2864
+ isSubscription: session.mode === "subscription",
2865
+ // React: `totalAmount={session ? Math.round(buildCheckoutDisplayData(session).total * 100) : 0}`.
2866
+ totalAmount: Math.round(buildCheckoutDisplayData(session).total * 100),
2867
+ enabledPaymentMethodCountries: stripeCountryGate.value,
2868
+ debug: props.debug,
2869
+ ...relay,
2870
+ onCountryChange: (country) => invokeMerchantCallback3(() => emit("countryChange", country)),
2871
+ onZipChange: (zip) => invokeMerchantCallback3(() => emit("zipChange", zip))
2872
+ },
2873
+ cardSlots
2874
+ );
2875
+ return withProvider(() => [slots.default?.(), modeNotice, form]);
2876
+ };
2877
+ }
2878
+ });
2879
+
2880
+ // src/automatic-payment-button.ts
2881
+ var SUCCESS_DELAY_MS2 = SAVED_PAYMENT_SUCCESS_DELAY_MS;
2882
+ function resolveDraft(props) {
2883
+ if (props.createSession) return { ...props.createSession, checkoutMode: "auto" };
2884
+ if (!props.clientId || !props.account || !props.successUrl || !props.cancelUrl) return null;
2885
+ const hasProducts = props.products != null;
2886
+ return {
2887
+ clientId: props.clientId,
2888
+ products: props.products,
2889
+ items: hasProducts ? void 0 : props.items,
2890
+ subscriptions: hasProducts ? void 0 : props.subscriptions,
2891
+ account: props.account,
2892
+ successUrl: props.successUrl,
2893
+ cancelUrl: props.cancelUrl,
2894
+ couponCodes: props.couponCodes,
2895
+ tagsData: props.tagsData,
2896
+ utmMetadata: props.utmMetadata,
2897
+ checkoutMode: "auto"
2898
+ };
2899
+ }
2900
+ function canFallBack(error, sessionId) {
2901
+ if (!sessionId || error.type === "validation_error") return false;
2902
+ return error.code !== "stored_payment_method_unsupported";
2903
+ }
2904
+ var FloPayAutomaticPaymentButton = defineComponent5({
2905
+ name: "FloPayAutomaticPaymentButton",
2906
+ // The root is a fragment (button, notice, fallback checkout): native button
2907
+ // attributes are bound to the `<button>` explicitly, as React spreads them.
2908
+ inheritAttrs: false,
2909
+ props: {
2910
+ sessionId: { type: String, default: void 0 },
2911
+ nonce: { type: String, default: void 0 },
2912
+ createSession: { type: Object, default: void 0 },
2913
+ // Deprecated and ignored, as in React (see the props interface).
2914
+ paymentMethodId: { type: String, default: void 0 },
2915
+ checkoutMethod: { type: String, default: void 0 },
2916
+ clientId: { type: String, default: void 0 },
2917
+ products: { type: Array, default: void 0 },
2918
+ items: { type: Array, default: void 0 },
2919
+ subscriptions: { type: Array, default: void 0 },
2920
+ account: { type: Object, default: void 0 },
2921
+ successUrl: { type: String, default: void 0 },
2922
+ cancelUrl: { type: String, default: void 0 },
2923
+ couponCodes: { type: Array, default: void 0 },
2924
+ tagsData: { type: Object, default: void 0 },
2925
+ utmMetadata: {
2926
+ type: Object,
2927
+ default: void 0
2928
+ },
2929
+ billingApiUrl: { type: String, default: void 0 },
2930
+ locale: { type: String, default: void 0 },
2931
+ disabled: { type: Boolean, default: false },
2932
+ theme: { type: String, default: void 0 },
2933
+ appearance: { type: Object, default: void 0 },
2934
+ buttonsTheme: { type: String, default: void 0 },
2935
+ buttonsStyles: { type: Object, default: void 0 }
2936
+ },
2937
+ emits: {
2938
+ success: (_event) => true,
2939
+ error: (_error) => true,
2940
+ decline: (_decline) => true
2941
+ },
2942
+ setup(props, { attrs, emit, slots }) {
2943
+ const processing = ref5(false);
2944
+ const notice = ref5(null);
2945
+ const fallback = ref5(null);
2946
+ let mounted = true;
2947
+ const lifecycle = new AbortController();
2948
+ onBeforeUnmount5(() => {
2949
+ mounted = false;
2950
+ lifecycle.abort();
2951
+ });
2952
+ const billingApiUrl = () => resolveBillingApiUrl4(props.billingApiUrl);
2953
+ const themeBundle = computed3(() => resolveTheme3(props.theme));
2954
+ const bStyles = computed3(() => {
2955
+ const base = themeBundle.value?.buttonsLayout ?? resolveButtonsLayoutTheme2(props.buttonsTheme);
2956
+ const override = props.buttonsStyles;
2957
+ if (!override) return base;
2958
+ return { ...base, ...override, cardButton: { ...base.cardButton, ...override.cardButton } };
2959
+ });
2960
+ const resolvedAppearance = computed3(() => {
2961
+ const bundleAppearance = themeBundle.value?.appearance;
2962
+ if (!props.appearance) return bundleAppearance;
2963
+ if (!bundleAppearance) return props.appearance;
2964
+ return {
2965
+ ...bundleAppearance,
2966
+ ...props.appearance,
2967
+ variables: { ...bundleAppearance.variables, ...props.appearance.variables }
2968
+ };
2969
+ });
2970
+ const tile = computed3(() => {
2971
+ const vars = resolvedAppearance.value?.variables;
2972
+ const explicitPrimary = typeof vars?.colorPrimary === "string" ? vars.colorPrimary : void 0;
2973
+ const primary = explicitPrimary ?? "#4A49FF";
2974
+ const hover = typeof vars?.colorPrimaryHover === "string" ? vars.colorPrimaryHover : darkenHex(primary, 0.12);
2975
+ const radius = typeof vars?.borderRadius === "string" ? vars.borderRadius : "8px";
2976
+ return {
2977
+ primary,
2978
+ hover,
2979
+ // Only a primary-filled button (a bundle or an explicit colorPrimary) hovers.
2980
+ hovers: Boolean(themeBundle.value || explicitPrimary),
2981
+ style: {
2982
+ width: "100%",
2983
+ ...bStyles.value.cardButton,
2984
+ ...derivePrimaryTileStyle2({
2985
+ themeBundle: themeBundle.value,
2986
+ resolvedPrimaryColor: primary,
2987
+ resolvedBorderRadius: radius,
2988
+ submitButtonStyle: bStyles.value.submitButton,
2989
+ explicitPrimaryColor: explicitPrimary
2990
+ }),
2991
+ fontSize: bStyles.value.cardButtonFontSize ?? "0.95rem",
2992
+ fontWeight: 600
2993
+ }
2994
+ };
2995
+ });
2996
+ const succeed = async (event) => {
2997
+ await new Promise((resolve) => setTimeout(resolve, SUCCESS_DELAY_MS2));
2998
+ invokeMerchantCallback4(() => emit("success", event));
2999
+ };
3000
+ const fail = (error, sessionId, nonce) => {
3001
+ if (!mounted && error.code === "three_ds_aborted") return;
3002
+ if (mounted) notice.value = error.message;
3003
+ invokeMerchantCallback4(() => emit("error", error));
3004
+ if (sessionId) {
3005
+ const decline = {
3006
+ method: "card",
3007
+ message: error.message,
3008
+ ...error.code ? { code: error.code } : {},
3009
+ ...error.declineCode ? { declineCode: error.declineCode } : {},
3010
+ sessionId
3011
+ };
3012
+ invokeMerchantCallback4(() => emit("decline", decline));
3013
+ }
3014
+ if (mounted && canFallBack(error, sessionId) && sessionId) {
3015
+ fallback.value = { sessionId, ...nonce ? { nonce } : {} };
3016
+ }
3017
+ };
3018
+ const run = async () => {
3019
+ if (processing.value || props.disabled) return;
3020
+ fallback.value = null;
3021
+ notice.value = null;
3022
+ const draft = resolveDraft(props);
3023
+ if (Boolean(props.sessionId) === Boolean(draft)) {
3024
+ fail(
3025
+ new FloPayError5(
3026
+ props.sessionId ? "Provide either sessionId or create-session props, not both." : "Provide a sessionId or the props required to create an automatic payment session.",
3027
+ "validation_error"
3028
+ ),
3029
+ null
3030
+ );
3031
+ return;
3032
+ }
3033
+ processing.value = true;
3034
+ let sessionId = props.sessionId ?? null;
3035
+ let nonce = props.nonce;
3036
+ try {
3037
+ const api = new PaymentAPI4(billingApiUrl());
3038
+ let result;
3039
+ if (props.sessionId) {
3040
+ result = await api.getUnifiedCheckoutSession(props.sessionId, props.nonce);
3041
+ } else {
3042
+ try {
3043
+ result = await api.createAndFetchSession(draft);
3044
+ } catch (error) {
3045
+ if (readErrorCode(error) === "session_auto_completed") {
3046
+ await succeed({
3047
+ result: { status: "succeeded" },
3048
+ session: null,
3049
+ sessionId: null,
3050
+ autoCompleted: true
3051
+ });
3052
+ return;
3053
+ }
3054
+ throw error;
3055
+ }
3056
+ }
3057
+ const session = result.data.session;
3058
+ if (!session) throw new FloPayError5("The checkout session could not be read.", "api_error");
3059
+ sessionId = session.id || sessionId;
3060
+ nonce = nonce ?? (session.clientSecret || void 0);
3061
+ if (isTerminalSuccess(session)) {
3062
+ await succeed({
3063
+ result: terminalResult(session, sessionId),
3064
+ session,
3065
+ sessionId,
3066
+ autoCompleted: false
3067
+ });
3068
+ return;
3069
+ }
3070
+ if (session.status === "expired") {
3071
+ throw new FloPayError5("This checkout session has expired.", "api_error", {
3072
+ code: "session_expired"
3073
+ });
3074
+ }
3075
+ const settled = await chargeSavedPayment({
3076
+ api,
3077
+ billingApiUrl: billingApiUrl(),
3078
+ session,
3079
+ nonce,
3080
+ unified: result,
3081
+ signal: lifecycle.signal
3082
+ });
3083
+ await succeed({ result: settled, session, sessionId, autoCompleted: false });
3084
+ } catch (error) {
3085
+ fail(coerceError(error, "Automatic payment failed. Please try again."), sessionId, nonce);
3086
+ } finally {
3087
+ if (mounted) processing.value = false;
3088
+ }
3089
+ };
3090
+ const handleClick = (event) => {
3091
+ const consumer = attrs.onClick;
3092
+ for (const listener of [consumer].flat()) {
3093
+ if (typeof listener === "function") listener(event);
3094
+ }
3095
+ if (event.defaultPrevented) return;
3096
+ void run();
3097
+ };
3098
+ const runConsumer = (name, event) => {
3099
+ const consumer = attrs[name];
3100
+ for (const listener of [consumer].flat()) {
3101
+ if (typeof listener === "function") listener(event);
3102
+ }
3103
+ };
3104
+ const handleMouseEnter = (event) => {
3105
+ runConsumer("onMouseenter", event);
3106
+ if (event.defaultPrevented || props.disabled || processing.value || !tile.value.hovers)
3107
+ return;
3108
+ event.currentTarget.style.backgroundColor = tile.value.hover;
3109
+ };
3110
+ const handleMouseLeave = (event) => {
3111
+ runConsumer("onMouseleave", event);
3112
+ if (event.defaultPrevented || !tile.value.hovers) return;
3113
+ event.currentTarget.style.backgroundColor = tile.value.primary;
3114
+ };
3115
+ return () => {
3116
+ const {
3117
+ onClick: _consumerClick,
3118
+ onMouseenter: _consumerEnter,
3119
+ onMouseleave: _consumerLeave,
3120
+ style: consumerStyle,
3121
+ ...buttonAttrs
3122
+ } = attrs;
3123
+ const busy = props.disabled || processing.value;
3124
+ return [
3125
+ h5(
3126
+ "button",
3127
+ {
3128
+ // React's `type = 'button'` default, overridable like any attribute.
3129
+ type: "button",
3130
+ ...buttonAttrs,
3131
+ // The theme first; the consumer's inline `style` still wins.
3132
+ style: [
3133
+ tile.value.style,
3134
+ { cursor: busy ? "not-allowed" : "pointer", opacity: busy ? 0.6 : 1 },
3135
+ consumerStyle
3136
+ ],
3137
+ disabled: busy,
3138
+ "aria-busy": processing.value ? "true" : void 0,
3139
+ onClick: handleClick,
3140
+ onMouseenter: handleMouseEnter,
3141
+ onMouseleave: handleMouseLeave
3142
+ },
3143
+ slots.default?.() ?? (processing.value ? "Processing\u2026" : "Pay now")
3144
+ ),
3145
+ notice.value ? h5("div", { role: "alert" }, notice.value) : null,
3146
+ fallback.value ? h5(FloPayCheckout, {
3147
+ sessionId: fallback.value.sessionId,
3148
+ nonce: fallback.value.nonce,
3149
+ billingApiUrl: billingApiUrl(),
3150
+ // React: the modal inherits the button's look.
3151
+ theme: props.theme,
3152
+ ...resolvedAppearance.value ? { appearance: resolvedAppearance.value } : {},
3153
+ buttonsTheme: props.buttonsTheme,
3154
+ buttonsStyles: props.buttonsStyles,
3155
+ onComplete: (result) => invokeMerchantCallback4(
3156
+ () => emit("success", {
3157
+ result,
3158
+ session: null,
3159
+ sessionId: fallback.value?.sessionId ?? null,
3160
+ autoCompleted: false
3161
+ })
3162
+ ),
3163
+ onError: (error) => invokeMerchantCallback4(() => emit("error", error)),
3164
+ onDecline: (decline) => invokeMerchantCallback4(() => emit("decline", decline))
3165
+ }) : null
3166
+ ];
3167
+ };
3168
+ }
3169
+ });
3170
+
3171
+ // src/hooks.ts
3172
+ import { inject as inject3, reactive as reactive2, shallowRef as shallowRef4 } from "vue";
3173
+ function useFloPay() {
3174
+ return inject3(FLOPAY_CONTEXT, null)?.flopay ?? shallowRef4(null);
3175
+ }
3176
+ function usePayPalFloPay() {
3177
+ return inject3(FLOPAY_CONTEXT, null)?.paypalFlopay ?? shallowRef4(null);
3178
+ }
3179
+ function useCheckout() {
3180
+ return inject3(CHECKOUT_STATE, null) ?? reactive2({ session: null, loading: false, error: null });
3181
+ }
3182
+ export {
3183
+ DirectPayPalButton,
3184
+ FloPayAutomaticPaymentButton,
3185
+ FloPayCardSetup,
3186
+ FloPayCardSetupError,
3187
+ FloPayCheckout,
3188
+ FloPayPaymentMethodSetup,
3189
+ FloPayProvider,
3190
+ SplitCardForm,
3191
+ VaultCardFields,
3192
+ dropThirdPartyOnlyError,
3193
+ useCheckout,
3194
+ useFloPay,
3195
+ usePayPalFloPay
3196
+ };