@flopay/vue 0.0.0-stage → 1.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -2
- package/dist/card-setup-entry.cjs +614 -0
- package/dist/card-setup-entry.d.cts +160 -0
- package/dist/card-setup-entry.d.ts +160 -0
- package/dist/card-setup-entry.mjs +10 -0
- package/dist/chunk-2PLBNCM6.mjs +332 -0
- package/dist/chunk-DUU6SMKP.mjs +97 -0
- package/dist/chunk-VXYNYMHI.mjs +833 -0
- package/dist/chunk-W52CQTJI.mjs +527 -0
- package/dist/hosted-setup-jpaaXDFA.d.cts +33 -0
- package/dist/hosted-setup-jpaaXDFA.d.ts +33 -0
- package/dist/index.cjs +4987 -0
- package/dist/index.d.cts +1940 -0
- package/dist/index.d.ts +1940 -0
- package/dist/index.mjs +3196 -0
- package/dist/payment-method-setup-entry.cjs +1809 -0
- package/dist/payment-method-setup-entry.d.cts +156 -0
- package/dist/payment-method-setup-entry.d.ts +156 -0
- package/dist/payment-method-setup-entry.mjs +7 -0
- package/dist/payment-method-setup-paypal-XPSU4CHA.mjs +148 -0
- package/dist/stripe-elements-QGT45VIE.mjs +6 -0
- package/package.json +60 -3
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
|
+
};
|