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