@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,833 @@
|
|
|
1
|
+
// src/stripe-elements.ts
|
|
2
|
+
import { PaymentAPI } from "@flopay/js/card-setup";
|
|
3
|
+
import {
|
|
4
|
+
buildWalletSheetWindowMessage,
|
|
5
|
+
denyPaymentPermissionToNewFrames,
|
|
6
|
+
detectPayPalEnvironment,
|
|
7
|
+
FloPayError,
|
|
8
|
+
invokeMerchantCallback as invokeMerchantCallback2,
|
|
9
|
+
isProviderDecline,
|
|
10
|
+
isWalletSheetRefusedForWindowSize,
|
|
11
|
+
readStripeRedirectParams as readStripeRedirectParams2,
|
|
12
|
+
resolveSetupFutureUsage,
|
|
13
|
+
resolveWalletModeOptions,
|
|
14
|
+
shouldPreferWalletPopup,
|
|
15
|
+
toStripeAppearance,
|
|
16
|
+
WALLET_SHEET_WINDOW_TOO_SMALL_CODE
|
|
17
|
+
} from "@flopay/shared";
|
|
18
|
+
import { defineComponent as defineComponent2, h, inject, onBeforeUnmount, onMounted as onMounted2, ref } from "vue";
|
|
19
|
+
|
|
20
|
+
// src/context.ts
|
|
21
|
+
var FLOPAY_CONTEXT = /* @__PURE__ */ Symbol("flopay.vue.provider");
|
|
22
|
+
var CHECKOUT_STATE = /* @__PURE__ */ Symbol("flopay.vue.checkout");
|
|
23
|
+
var CHECKOUT_FORM_DEFAULTS = /* @__PURE__ */ Symbol(
|
|
24
|
+
"flopay.vue.checkout-form-defaults"
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// src/provider.ts
|
|
28
|
+
import {
|
|
29
|
+
FLO_INSTRUMENT_ONCE_NAMES,
|
|
30
|
+
invokeMerchantCallback,
|
|
31
|
+
projectFloInstrument,
|
|
32
|
+
resolveBillingApiUrl
|
|
33
|
+
} from "@flopay/shared";
|
|
34
|
+
import {
|
|
35
|
+
defineComponent,
|
|
36
|
+
getCurrentInstance,
|
|
37
|
+
nextTick,
|
|
38
|
+
onMounted,
|
|
39
|
+
provide,
|
|
40
|
+
shallowRef,
|
|
41
|
+
watch
|
|
42
|
+
} from "vue";
|
|
43
|
+
var FLOPAY_TELEMETRY_BRIDGE = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.bridge.v1");
|
|
44
|
+
function telemetryBridge(flopay) {
|
|
45
|
+
return flopay[FLOPAY_TELEMETRY_BRIDGE];
|
|
46
|
+
}
|
|
47
|
+
function isPromiseLike(value) {
|
|
48
|
+
return typeof value?.then === "function";
|
|
49
|
+
}
|
|
50
|
+
function createInstrumentEmitter(notify) {
|
|
51
|
+
const emittedOnce = /* @__PURE__ */ new Set();
|
|
52
|
+
return (source) => {
|
|
53
|
+
const event = projectFloInstrument(source);
|
|
54
|
+
if (!event) return;
|
|
55
|
+
if (FLO_INSTRUMENT_ONCE_NAMES.has(event.name)) {
|
|
56
|
+
if (emittedOnce.has(event.name)) return;
|
|
57
|
+
emittedOnce.add(event.name);
|
|
58
|
+
}
|
|
59
|
+
invokeMerchantCallback(() => notify(event));
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
var FloPayProvider = defineComponent({
|
|
63
|
+
name: "FloPayProvider",
|
|
64
|
+
props: {
|
|
65
|
+
flopay: {
|
|
66
|
+
type: [Object, Promise],
|
|
67
|
+
default: null
|
|
68
|
+
},
|
|
69
|
+
paypalFlopay: {
|
|
70
|
+
type: [Object, Promise],
|
|
71
|
+
default: null
|
|
72
|
+
},
|
|
73
|
+
options: { type: Object, default: void 0 }
|
|
74
|
+
},
|
|
75
|
+
emits: { instrument: (_event) => true },
|
|
76
|
+
setup(props, { emit, slots }) {
|
|
77
|
+
const initial = (source) => isPromiseLike(source) ? null : source ?? null;
|
|
78
|
+
const flopay = shallowRef(initial(props.flopay));
|
|
79
|
+
const paypalFlopay = shallowRef(initial(props.paypalFlopay));
|
|
80
|
+
const component = getCurrentInstance();
|
|
81
|
+
const emitInstrument = createInstrumentEmitter((event) => {
|
|
82
|
+
const listener = component?.vnode.props?.onInstrument;
|
|
83
|
+
if (!listener) return emit("instrument", event);
|
|
84
|
+
for (const handler of [listener].flat()) handler(event);
|
|
85
|
+
});
|
|
86
|
+
provide(FLOPAY_CONTEXT, {
|
|
87
|
+
flopay,
|
|
88
|
+
paypalFlopay,
|
|
89
|
+
billingApiUrl: () => resolveBillingApiUrl(props.options?.billingApiUrl),
|
|
90
|
+
emitInstrument
|
|
91
|
+
});
|
|
92
|
+
const follow = (read, target, provider) => watch(
|
|
93
|
+
read,
|
|
94
|
+
(source, _previous, onCleanup) => {
|
|
95
|
+
let active = true;
|
|
96
|
+
onCleanup(() => {
|
|
97
|
+
active = false;
|
|
98
|
+
});
|
|
99
|
+
if (!isPromiseLike(source)) {
|
|
100
|
+
target.value = source ?? null;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (provider === "paypal") target.value = null;
|
|
104
|
+
source.then(
|
|
105
|
+
(instance) => {
|
|
106
|
+
if (active) target.value = instance;
|
|
107
|
+
},
|
|
108
|
+
() => {
|
|
109
|
+
if (!active) return;
|
|
110
|
+
emitInstrument({
|
|
111
|
+
class: "technical_error",
|
|
112
|
+
errorCode: "SDK_INITIALIZATION_FAILED",
|
|
113
|
+
...provider ? { provider } : {}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
{ immediate: true }
|
|
119
|
+
);
|
|
120
|
+
onMounted(() => {
|
|
121
|
+
emitInstrument({ class: "lifecycle", name: "checkout.mount" });
|
|
122
|
+
follow(() => props.flopay, flopay);
|
|
123
|
+
follow(() => props.paypalFlopay, paypalFlopay, "paypal");
|
|
124
|
+
watch(
|
|
125
|
+
flopay,
|
|
126
|
+
(instance, _previous, onCleanup) => {
|
|
127
|
+
if (!instance) return;
|
|
128
|
+
emitInstrument({ class: "lifecycle", name: "sdk.initialize.ready" });
|
|
129
|
+
const unsubscribe = telemetryBridge(instance)?.subscribe?.(emitInstrument);
|
|
130
|
+
onCleanup(() => unsubscribe?.());
|
|
131
|
+
void nextTick(() => emitInstrument({ class: "lifecycle", name: "checkout.rendered" }));
|
|
132
|
+
},
|
|
133
|
+
{ immediate: true }
|
|
134
|
+
);
|
|
135
|
+
watch(paypalFlopay, (instance, _previous, onCleanup) => {
|
|
136
|
+
if (!instance || instance === flopay.value) return;
|
|
137
|
+
const unsubscribe = telemetryBridge(instance)?.subscribe?.(emitInstrument);
|
|
138
|
+
onCleanup(() => unsubscribe?.());
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
return () => slots.default?.();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// src/stripe-paypal-continuation.ts
|
|
146
|
+
import { readStripeRedirectParams, stripeRedirectReturnUrl } from "@flopay/shared";
|
|
147
|
+
var STRIPE_PAYPAL_CONTINUATION_KEY = "flopay_vue_paypal_resume";
|
|
148
|
+
var STRIPE_PAYPAL_ATTEMPT_PARAM = "flopay_paypal_attempt";
|
|
149
|
+
function sessionStore() {
|
|
150
|
+
if (typeof window === "undefined") return null;
|
|
151
|
+
try {
|
|
152
|
+
return window.sessionStorage;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function createStripePayPalAttempt() {
|
|
158
|
+
const random = globalThis.crypto?.randomUUID?.();
|
|
159
|
+
if (random) return random;
|
|
160
|
+
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
|
|
161
|
+
}
|
|
162
|
+
function saveStripePayPalContinuation(continuation) {
|
|
163
|
+
const store = sessionStore();
|
|
164
|
+
if (!store) return false;
|
|
165
|
+
try {
|
|
166
|
+
store.setItem(
|
|
167
|
+
STRIPE_PAYPAL_CONTINUATION_KEY,
|
|
168
|
+
JSON.stringify({
|
|
169
|
+
attempt: continuation.attempt,
|
|
170
|
+
sessionId: continuation.sessionId,
|
|
171
|
+
...continuation.nonce ? { nonce: continuation.nonce } : {}
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function clearStripePayPalContinuation() {
|
|
180
|
+
try {
|
|
181
|
+
sessionStore()?.removeItem(STRIPE_PAYPAL_CONTINUATION_KEY);
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function stripePayPalReturnUrl(attempt) {
|
|
186
|
+
const url = new URL(stripeRedirectReturnUrl());
|
|
187
|
+
url.searchParams.delete(STRIPE_PAYPAL_ATTEMPT_PARAM);
|
|
188
|
+
if (attempt) url.searchParams.set(STRIPE_PAYPAL_ATTEMPT_PARAM, attempt);
|
|
189
|
+
return url.toString();
|
|
190
|
+
}
|
|
191
|
+
function readStripePayPalReturnContinuation() {
|
|
192
|
+
if (typeof window === "undefined") return null;
|
|
193
|
+
const [paymentIntentId] = readStripeRedirectParams();
|
|
194
|
+
if (!paymentIntentId) return null;
|
|
195
|
+
const attempt = new URLSearchParams(window.location.search).get(STRIPE_PAYPAL_ATTEMPT_PARAM);
|
|
196
|
+
if (!attempt) return null;
|
|
197
|
+
try {
|
|
198
|
+
const raw = sessionStore()?.getItem(STRIPE_PAYPAL_CONTINUATION_KEY);
|
|
199
|
+
if (!raw) return null;
|
|
200
|
+
const parsed = JSON.parse(raw);
|
|
201
|
+
if (!parsed || parsed.attempt !== attempt || typeof parsed.sessionId !== "string") return null;
|
|
202
|
+
return {
|
|
203
|
+
attempt,
|
|
204
|
+
sessionId: parsed.sessionId,
|
|
205
|
+
...typeof parsed.nonce === "string" ? { nonce: parsed.nonce } : {}
|
|
206
|
+
};
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function hasStripePayPalReturn() {
|
|
212
|
+
return readStripePayPalReturnContinuation() !== null;
|
|
213
|
+
}
|
|
214
|
+
function consumeStripePayPalReturn() {
|
|
215
|
+
clearStripePayPalContinuation();
|
|
216
|
+
window.history.replaceState({}, "", stripePayPalReturnUrl());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/stripe-elements.ts
|
|
220
|
+
var WALLET_METHODS = /* @__PURE__ */ new Set(["apple_pay", "google_pay", "link", "paypal", "amazon_pay"]);
|
|
221
|
+
var EXPRESS_CHECKOUT_CLICK_BUDGET_MS = 900;
|
|
222
|
+
function checkoutButtonMethodFromProviderMethod(method) {
|
|
223
|
+
if (method === "paypal") return "paypal";
|
|
224
|
+
if (method === "apple_pay") return "apple_pay";
|
|
225
|
+
return "google_pay";
|
|
226
|
+
}
|
|
227
|
+
var PAYPAL_REDIRECT_TILE_HEIGHT = 44;
|
|
228
|
+
var stripePayPalResumeAttempts = /* @__PURE__ */ new Map();
|
|
229
|
+
function walletClockNow() {
|
|
230
|
+
return globalThis.performance?.now() ?? Date.now();
|
|
231
|
+
}
|
|
232
|
+
function stripeMethodCategory(method) {
|
|
233
|
+
return WALLET_METHODS.has(method) ? "wallet" : "apm";
|
|
234
|
+
}
|
|
235
|
+
function declineReason(error) {
|
|
236
|
+
return error.decline_code ?? error.code ?? "provider_declined";
|
|
237
|
+
}
|
|
238
|
+
var StripePaymentMethods = defineComponent2({
|
|
239
|
+
name: "FloPayStripePaymentMethods",
|
|
240
|
+
props: {
|
|
241
|
+
session: { type: Object, default: null },
|
|
242
|
+
sessionId: { type: String, required: true },
|
|
243
|
+
nonce: { type: String, default: void 0 },
|
|
244
|
+
billingApiUrl: { type: String, required: true },
|
|
245
|
+
publishableKey: { type: String, required: true },
|
|
246
|
+
methods: { type: Array, required: true },
|
|
247
|
+
/** `payment` for checkout, `setup` for payment-method setup. */
|
|
248
|
+
intentKind: { type: String, default: "payment" },
|
|
249
|
+
currency: { type: String, default: void 0 },
|
|
250
|
+
amount: { type: Number, default: 0 },
|
|
251
|
+
/**
|
|
252
|
+
* React's `totalAmount`: the cart total due today, in minor units. When it
|
|
253
|
+
* is given on a checkout (`intentKind: 'payment'`), the wallet row follows
|
|
254
|
+
* React's wallet group: its Elements mode, amount and capture method come
|
|
255
|
+
* from the shared `resolveWalletModeOptions` (setup mode for a $0 total),
|
|
256
|
+
* and its intent is `totalAmount > 0 ? 'payment' : 'setup'`.
|
|
257
|
+
*/
|
|
258
|
+
totalAmount: { type: Number, default: void 0 },
|
|
259
|
+
returnUrl: { type: String, default: void 0 },
|
|
260
|
+
/** The theme's appearance, normalized for Stripe Elements (React's setup branch does the same). */
|
|
261
|
+
appearance: { type: Object, default: void 0 },
|
|
262
|
+
/**
|
|
263
|
+
* React's `stripePayPalPresentation`, which the form resolves from its
|
|
264
|
+
* `walletPresentation` prop through the shared
|
|
265
|
+
* `resolveStripePayPalPresentation`. With `redirect`, Stripe-routed PayPal
|
|
266
|
+
* leaves the wallet row and renders as React's full-page "Pay with PayPal"
|
|
267
|
+
* redirect tile, which resumes the payment when the buyer returns.
|
|
268
|
+
*/
|
|
269
|
+
paypalPresentation: { type: String, default: "popup" },
|
|
270
|
+
/**
|
|
271
|
+
* The form's click gate (React's `runBeforeButtonClick`), run by the
|
|
272
|
+
* redirect tile and by the wallet row's Express Checkout click.
|
|
273
|
+
*/
|
|
274
|
+
runBeforeButtonClick: {
|
|
275
|
+
type: Function,
|
|
276
|
+
default: void 0
|
|
277
|
+
},
|
|
278
|
+
/**
|
|
279
|
+
* React's `onTokenizedBody={dispatchTokenizedBody}` for the redirect tile:
|
|
280
|
+
* the form posts `/process` with the approved payment. Without it, the
|
|
281
|
+
* tile reports `complete` itself, as this component's wallet row does.
|
|
282
|
+
*/
|
|
283
|
+
onTokenizedBody: {
|
|
284
|
+
type: Function,
|
|
285
|
+
default: void 0
|
|
286
|
+
},
|
|
287
|
+
/**
|
|
288
|
+
* React's `isProcessing={isSubmitting}`: while the form is processing, the
|
|
289
|
+
* PayPal redirect tile and the APM button are disabled and ignore clicks.
|
|
290
|
+
*/
|
|
291
|
+
isProcessing: { type: Boolean, default: false },
|
|
292
|
+
/**
|
|
293
|
+
* The form's storage-free stand-in for the click-time account patch React
|
|
294
|
+
* restores from its `localStorage` continuation (issue #410, triage AC 4:
|
|
295
|
+
* Vue never stores it). On an authorized redirect return it is asked once
|
|
296
|
+
* for the account, with the returned session id; what it answers goes to
|
|
297
|
+
* `onTokenizedBody` as the attempt's `accountPatch`. A throw or no answer
|
|
298
|
+
* continues without one, because the payment is already authorized.
|
|
299
|
+
*/
|
|
300
|
+
recoverAccountPatch: {
|
|
301
|
+
type: Function,
|
|
302
|
+
default: void 0
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
emits: {
|
|
306
|
+
/** The buyer confirmed a wallet or APM and its element validated: the attempt starts. */
|
|
307
|
+
submitting: () => true,
|
|
308
|
+
complete: (_result) => true,
|
|
309
|
+
decline: (_decline) => true,
|
|
310
|
+
error: (_error) => true
|
|
311
|
+
},
|
|
312
|
+
setup(props, { emit }) {
|
|
313
|
+
const walletContainer = ref(null);
|
|
314
|
+
const apmContainer = ref(null);
|
|
315
|
+
const processing = ref(false);
|
|
316
|
+
const busy = () => processing.value || props.isProcessing;
|
|
317
|
+
const cleanups = [];
|
|
318
|
+
let apmElements = null;
|
|
319
|
+
let stripe = null;
|
|
320
|
+
const elementsOptions = (methods, surface) => {
|
|
321
|
+
const setupFutureUsage = resolveSetupFutureUsage(props.session, methods, surface);
|
|
322
|
+
const currency = (props.currency ?? props.session?.currency ?? "usd").toLowerCase();
|
|
323
|
+
const appearance = props.appearance ? { appearance: toStripeAppearance(props.appearance) } : {};
|
|
324
|
+
return props.intentKind === "setup" ? { mode: "setup", currency, paymentMethodTypes: methods, ...appearance } : {
|
|
325
|
+
mode: props.session?.mode === "subscription" ? "subscription" : "payment",
|
|
326
|
+
amount: props.amount,
|
|
327
|
+
currency,
|
|
328
|
+
paymentMethodTypes: methods,
|
|
329
|
+
...setupFutureUsage ? { setupFutureUsage } : {},
|
|
330
|
+
...appearance
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
const walletFollowsTotal = () => props.intentKind === "payment" && typeof props.totalAmount === "number";
|
|
334
|
+
const walletIntentKind = () => {
|
|
335
|
+
if (!walletFollowsTotal()) return props.intentKind;
|
|
336
|
+
return (props.totalAmount ?? 0) > 0 ? "payment" : "setup";
|
|
337
|
+
};
|
|
338
|
+
const walletElementsOptions = (methods) => {
|
|
339
|
+
if (!walletFollowsTotal()) return elementsOptions(methods, "wallet");
|
|
340
|
+
const setupFutureUsage = resolveSetupFutureUsage(props.session, methods, "wallet");
|
|
341
|
+
const currency = (props.currency ?? props.session?.currency ?? "usd").toLowerCase();
|
|
342
|
+
return {
|
|
343
|
+
...resolveWalletModeOptions(props.totalAmount, props.session?.captureMethod),
|
|
344
|
+
currency,
|
|
345
|
+
paymentMethodTypes: methods,
|
|
346
|
+
...setupFutureUsage ? { setupFutureUsage } : {},
|
|
347
|
+
...props.appearance ? { appearance: toStripeAppearance(props.appearance) } : {}
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
let walletAttempt = null;
|
|
351
|
+
const floPayContext = inject(FLOPAY_CONTEXT, null);
|
|
352
|
+
const walletTelemetry = () => {
|
|
353
|
+
const instance = floPayContext?.flopay.value;
|
|
354
|
+
return instance ? telemetryBridge(instance) : void 0;
|
|
355
|
+
};
|
|
356
|
+
let walletSheetClick = null;
|
|
357
|
+
let walletSheetAttempts = 0;
|
|
358
|
+
const endWalletSheetClick = () => {
|
|
359
|
+
walletSheetClick?.stopObserving();
|
|
360
|
+
walletSheetClick = null;
|
|
361
|
+
};
|
|
362
|
+
const observeWalletSheetClick = (method, startedAt) => {
|
|
363
|
+
endWalletSheetClick();
|
|
364
|
+
walletSheetAttempts += 1;
|
|
365
|
+
const click = {
|
|
366
|
+
attempt: walletSheetAttempts,
|
|
367
|
+
method,
|
|
368
|
+
startedAt,
|
|
369
|
+
handoffObserved: false,
|
|
370
|
+
stopObserving: () => {
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const onBlur = () => {
|
|
374
|
+
click.handoffObserved = true;
|
|
375
|
+
};
|
|
376
|
+
const onVisibilityChange = () => {
|
|
377
|
+
if (document.visibilityState === "hidden") click.handoffObserved = true;
|
|
378
|
+
};
|
|
379
|
+
window.addEventListener("blur", onBlur);
|
|
380
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
381
|
+
click.stopObserving = () => {
|
|
382
|
+
window.removeEventListener("blur", onBlur);
|
|
383
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
384
|
+
};
|
|
385
|
+
walletSheetClick = click;
|
|
386
|
+
};
|
|
387
|
+
cleanups.push(endWalletSheetClick);
|
|
388
|
+
const reportWalletSheetRefusal = (click) => {
|
|
389
|
+
const environment = detectPayPalEnvironment();
|
|
390
|
+
walletTelemetry()?.error?.({
|
|
391
|
+
errorCode: "PROVIDER_RUNTIME_FAILED",
|
|
392
|
+
failureCategory: "provider_runtime",
|
|
393
|
+
stage: "overlay_open",
|
|
394
|
+
provider: "stripe",
|
|
395
|
+
paymentMethodCategory: "wallet",
|
|
396
|
+
requestCategory: "provider_sdk",
|
|
397
|
+
terminalReason: "provider_error",
|
|
398
|
+
attempt: click.attempt,
|
|
399
|
+
walletType: "google_pay",
|
|
400
|
+
...environment,
|
|
401
|
+
presentationMode: "popup"
|
|
402
|
+
});
|
|
403
|
+
const failure = new FloPayError(buildWalletSheetWindowMessage("Google Pay"), "api_error", {
|
|
404
|
+
code: "external_payment_method_failed",
|
|
405
|
+
param: click.method,
|
|
406
|
+
details: {
|
|
407
|
+
providerCode: WALLET_SHEET_WINDOW_TOO_SMALL_CODE,
|
|
408
|
+
popupBlocked: false,
|
|
409
|
+
...environment,
|
|
410
|
+
presentationMode: "popup"
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
invokeMerchantCallback2(() => emit("error", failure));
|
|
414
|
+
};
|
|
415
|
+
const gateWalletClick = async (event) => {
|
|
416
|
+
const clickStartedAt = walletClockNow();
|
|
417
|
+
walletAttempt = null;
|
|
418
|
+
endWalletSheetClick();
|
|
419
|
+
const gate = props.runBeforeButtonClick;
|
|
420
|
+
if (!gate) {
|
|
421
|
+
observeWalletSheetClick(event.expressPaymentType, clickStartedAt);
|
|
422
|
+
event.resolve();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
let clickActive = true;
|
|
426
|
+
const clickTimer = setTimeout(() => {
|
|
427
|
+
clickActive = false;
|
|
428
|
+
event.reject();
|
|
429
|
+
}, EXPRESS_CHECKOUT_CLICK_BUDGET_MS);
|
|
430
|
+
let beforeClick;
|
|
431
|
+
try {
|
|
432
|
+
beforeClick = await gate(checkoutButtonMethodFromProviderMethod(event.expressPaymentType));
|
|
433
|
+
} catch {
|
|
434
|
+
beforeClick = { proceed: false };
|
|
435
|
+
} finally {
|
|
436
|
+
clearTimeout(clickTimer);
|
|
437
|
+
}
|
|
438
|
+
if (!clickActive) return;
|
|
439
|
+
if (!beforeClick.proceed) {
|
|
440
|
+
event.reject();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
walletAttempt = {
|
|
444
|
+
method: event.expressPaymentType,
|
|
445
|
+
...beforeClick.sessionId ? { sessionId: beforeClick.sessionId } : {},
|
|
446
|
+
...beforeClick.nonce ? { nonce: beforeClick.nonce } : {}
|
|
447
|
+
};
|
|
448
|
+
observeWalletSheetClick(event.expressPaymentType, clickStartedAt);
|
|
449
|
+
event.resolve();
|
|
450
|
+
};
|
|
451
|
+
const confirm = async (elements, method, intentKind = props.intentKind, attempt = null) => {
|
|
452
|
+
if (!stripe || processing.value) return;
|
|
453
|
+
processing.value = true;
|
|
454
|
+
const api = new PaymentAPI(props.billingApiUrl, { telemetry: false });
|
|
455
|
+
const category = stripeMethodCategory(method);
|
|
456
|
+
const sessionId = attempt?.sessionId ?? props.sessionId;
|
|
457
|
+
const nonce = attempt?.nonce ?? props.nonce;
|
|
458
|
+
try {
|
|
459
|
+
const submitted = await elements.submit();
|
|
460
|
+
if (submitted.error) return;
|
|
461
|
+
invokeMerchantCallback2(() => emit("submitting"));
|
|
462
|
+
const intent = await api.createSessionIntent(sessionId, nonce ?? "", {
|
|
463
|
+
provider: "stripe",
|
|
464
|
+
paymentMethodCategory: category,
|
|
465
|
+
paymentMethodType: method,
|
|
466
|
+
paymentMethodId: null,
|
|
467
|
+
intentKind
|
|
468
|
+
});
|
|
469
|
+
const clientSecret = intent.clientSecret ?? "";
|
|
470
|
+
const returnUrl = props.returnUrl ?? window.location.href;
|
|
471
|
+
const { error } = intentKind === "setup" ? await stripe.confirmSetup({
|
|
472
|
+
elements,
|
|
473
|
+
clientSecret,
|
|
474
|
+
confirmParams: { return_url: returnUrl },
|
|
475
|
+
redirect: "if_required"
|
|
476
|
+
}) : await stripe.confirmPayment({
|
|
477
|
+
elements,
|
|
478
|
+
clientSecret,
|
|
479
|
+
confirmParams: { return_url: returnUrl },
|
|
480
|
+
redirect: "if_required"
|
|
481
|
+
});
|
|
482
|
+
if (error) {
|
|
483
|
+
if (intentKind === "payment") {
|
|
484
|
+
void api.reportSessionIntentDecline(sessionId, nonce ?? "", {
|
|
485
|
+
provider: "stripe",
|
|
486
|
+
paymentMethodCategory: category,
|
|
487
|
+
paymentMethodType: method,
|
|
488
|
+
providerDeclineReason: declineReason(error),
|
|
489
|
+
providerIntentId: intent.providerObjectId
|
|
490
|
+
}).catch(() => {
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
const decline = {
|
|
494
|
+
method,
|
|
495
|
+
message: error.message ?? "Your payment was declined.",
|
|
496
|
+
code: error.code,
|
|
497
|
+
declineCode: error.decline_code ?? error.code,
|
|
498
|
+
sessionId
|
|
499
|
+
};
|
|
500
|
+
invokeMerchantCallback2(() => emit("decline", decline));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const result = {
|
|
504
|
+
status: "succeeded",
|
|
505
|
+
paymentIntentId: intent.providerObjectId,
|
|
506
|
+
checkoutMethod: method
|
|
507
|
+
};
|
|
508
|
+
invokeMerchantCallback2(() => emit("complete", result));
|
|
509
|
+
} catch (error) {
|
|
510
|
+
const failure = error instanceof FloPayError ? error : new FloPayError(
|
|
511
|
+
error instanceof Error ? error.message : "Payment failed.",
|
|
512
|
+
"api_error"
|
|
513
|
+
);
|
|
514
|
+
invokeMerchantCallback2(() => emit("error", failure));
|
|
515
|
+
} finally {
|
|
516
|
+
processing.value = false;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
const paypalRedirect = () => props.paypalPresentation === "redirect" && props.intentKind === "payment" && props.methods.includes("paypal");
|
|
520
|
+
let active = true;
|
|
521
|
+
const emitPayPalDecline = (message, sessionId, error) => {
|
|
522
|
+
const decline = {
|
|
523
|
+
method: "paypal",
|
|
524
|
+
message,
|
|
525
|
+
...error ? { code: error.code, declineCode: error.decline_code ?? error.code } : {},
|
|
526
|
+
sessionId
|
|
527
|
+
};
|
|
528
|
+
invokeMerchantCallback2(() => emit("decline", decline));
|
|
529
|
+
};
|
|
530
|
+
const emitPayPalFailure = (message, code) => {
|
|
531
|
+
const failure = new FloPayError(message, "api_error", code ? { code } : void 0);
|
|
532
|
+
invokeMerchantCallback2(() => emit("error", failure));
|
|
533
|
+
};
|
|
534
|
+
const handOffPayPal = (body, overrides) => {
|
|
535
|
+
if (props.onTokenizedBody) {
|
|
536
|
+
props.onTokenizedBody(body, overrides);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const result = {
|
|
540
|
+
status: "succeeded",
|
|
541
|
+
paymentIntentId: body.threeDSecureActionResultTokenId,
|
|
542
|
+
checkoutMethod: "paypal"
|
|
543
|
+
};
|
|
544
|
+
invokeMerchantCallback2(() => emit("complete", result));
|
|
545
|
+
};
|
|
546
|
+
const redirectToPayPal = async () => {
|
|
547
|
+
if (!stripe || busy()) return;
|
|
548
|
+
processing.value = true;
|
|
549
|
+
const api = new PaymentAPI(props.billingApiUrl, { telemetry: false });
|
|
550
|
+
let sessionId = props.sessionId;
|
|
551
|
+
try {
|
|
552
|
+
const beforeClick = props.runBeforeButtonClick ? await props.runBeforeButtonClick("paypal") : { proceed: true };
|
|
553
|
+
if (!beforeClick.proceed) return;
|
|
554
|
+
sessionId = beforeClick.sessionId ?? props.sessionId;
|
|
555
|
+
const nonce = beforeClick.nonce ?? props.nonce;
|
|
556
|
+
const createPaymentMethod = stripe.createPaymentMethod;
|
|
557
|
+
const { error: pmError, paymentMethod } = await createPaymentMethod.call(stripe, {
|
|
558
|
+
type: "paypal"
|
|
559
|
+
});
|
|
560
|
+
if (pmError) {
|
|
561
|
+
emitPayPalFailure(
|
|
562
|
+
pmError.message ?? "PayPal could not be started.",
|
|
563
|
+
"stripe_paypal_create_payment_method_failed"
|
|
564
|
+
);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const intent = await api.createSessionIntent(sessionId, nonce ?? "", {
|
|
568
|
+
provider: "stripe",
|
|
569
|
+
paymentMethodCategory: "wallet",
|
|
570
|
+
paymentMethodType: "paypal",
|
|
571
|
+
paymentMethodId: paymentMethod?.id ?? null,
|
|
572
|
+
intentKind: "payment"
|
|
573
|
+
});
|
|
574
|
+
if (intent.provider !== "stripe") {
|
|
575
|
+
throw new Error("Invalid provider returned for PayPal intent");
|
|
576
|
+
}
|
|
577
|
+
const clientSecret = intent.clientSecret ?? "";
|
|
578
|
+
const attempt = createStripePayPalAttempt();
|
|
579
|
+
const continuationSaved = saveStripePayPalContinuation({ attempt, sessionId, nonce });
|
|
580
|
+
if (!continuationSaved) {
|
|
581
|
+
throw new Error("Unable to save PayPal checkout progress. Please try again.");
|
|
582
|
+
}
|
|
583
|
+
const { error, paymentIntent } = await stripe.confirmPayment({
|
|
584
|
+
clientSecret,
|
|
585
|
+
confirmParams: {
|
|
586
|
+
return_url: stripePayPalReturnUrl(attempt),
|
|
587
|
+
...paymentMethod?.id ? { payment_method: paymentMethod.id } : {}
|
|
588
|
+
},
|
|
589
|
+
redirect: "if_required"
|
|
590
|
+
});
|
|
591
|
+
clearStripePayPalContinuation();
|
|
592
|
+
if (error) {
|
|
593
|
+
if (isProviderDecline(error)) {
|
|
594
|
+
void api.reportSessionIntentDecline(sessionId, nonce ?? "", {
|
|
595
|
+
provider: "stripe",
|
|
596
|
+
paymentMethodCategory: "wallet",
|
|
597
|
+
paymentMethodType: "paypal",
|
|
598
|
+
providerDeclineReason: error.code ?? "provider_declined",
|
|
599
|
+
providerIntentId: intent.providerObjectId
|
|
600
|
+
}).catch(() => {
|
|
601
|
+
});
|
|
602
|
+
emitPayPalDecline(error.message ?? "Your payment was declined.", sessionId, error);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
emitPayPalFailure(
|
|
606
|
+
error.message ?? "PayPal payment failed.",
|
|
607
|
+
"stripe_paypal_confirm_failed"
|
|
608
|
+
);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const paymentMethodId = typeof paymentIntent?.payment_method === "string" ? paymentIntent.payment_method : paymentIntent?.payment_method?.id;
|
|
612
|
+
handOffPayPal(
|
|
613
|
+
{
|
|
614
|
+
id: paymentMethodId ?? paymentIntent?.id,
|
|
615
|
+
type: "card",
|
|
616
|
+
threeDSecureActionResultTokenId: paymentIntent?.id,
|
|
617
|
+
isPaypal: true
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
accountPatch: beforeClick.accountPatch,
|
|
621
|
+
paymentMethodCategory: "paypal",
|
|
622
|
+
presentationMode: "redirect",
|
|
623
|
+
sessionId,
|
|
624
|
+
nonce
|
|
625
|
+
}
|
|
626
|
+
);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
emitPayPalFailure(
|
|
629
|
+
error instanceof Error ? error.message : "PayPal payment failed.",
|
|
630
|
+
"stripe_paypal_failed"
|
|
631
|
+
);
|
|
632
|
+
} finally {
|
|
633
|
+
processing.value = false;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
const recoverReturnAccount = async (sessionId) => {
|
|
637
|
+
const recover = props.recoverAccountPatch;
|
|
638
|
+
if (!recover) return void 0;
|
|
639
|
+
try {
|
|
640
|
+
return await recover(sessionId) ?? void 0;
|
|
641
|
+
} catch {
|
|
642
|
+
return void 0;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
const continueAuthorizedReturn = async (paymentIntent, continuation) => {
|
|
646
|
+
const paymentMethodId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
|
|
647
|
+
const accountPatch = await recoverReturnAccount(continuation.sessionId);
|
|
648
|
+
if (!active) return;
|
|
649
|
+
handOffPayPal(
|
|
650
|
+
{
|
|
651
|
+
id: paymentMethodId ?? paymentIntent.id,
|
|
652
|
+
type: "card",
|
|
653
|
+
threeDSecureActionResultTokenId: paymentIntent.id,
|
|
654
|
+
isPaypal: true
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
...accountPatch ? { accountPatch } : {},
|
|
658
|
+
sessionId: continuation.sessionId,
|
|
659
|
+
nonce: continuation.nonce,
|
|
660
|
+
presentationMode: "redirect"
|
|
661
|
+
}
|
|
662
|
+
);
|
|
663
|
+
};
|
|
664
|
+
const resumeStripePayPal = async (provider) => {
|
|
665
|
+
const [paymentIntentId, clientSecret, redirectStatus] = readStripeRedirectParams2();
|
|
666
|
+
if (!paymentIntentId || !clientSecret) return;
|
|
667
|
+
const continuation = readStripePayPalReturnContinuation();
|
|
668
|
+
if (!continuation) return;
|
|
669
|
+
const sessionId = continuation.sessionId;
|
|
670
|
+
const retrievalKey = continuation.attempt;
|
|
671
|
+
let retrieval = stripePayPalResumeAttempts.get(retrievalKey);
|
|
672
|
+
if (!retrieval) {
|
|
673
|
+
retrieval = redirectStatus === "failed" ? Promise.resolve(null) : Promise.resolve().then(() => provider.retrievePaymentIntent(clientSecret));
|
|
674
|
+
stripePayPalResumeAttempts.set(retrievalKey, retrieval);
|
|
675
|
+
}
|
|
676
|
+
processing.value = true;
|
|
677
|
+
try {
|
|
678
|
+
const result = await retrieval;
|
|
679
|
+
if (!active) return;
|
|
680
|
+
if (result?.error) {
|
|
681
|
+
stripePayPalResumeAttempts.delete(retrievalKey);
|
|
682
|
+
emitPayPalFailure(
|
|
683
|
+
result.error.message ?? "Failed to retrieve PayPal payment status.",
|
|
684
|
+
"REDIRECT_RESUME_FAILED"
|
|
685
|
+
);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
consumeStripePayPalReturn();
|
|
689
|
+
stripePayPalResumeAttempts.delete(retrievalKey);
|
|
690
|
+
if (redirectStatus === "failed") {
|
|
691
|
+
emitPayPalDecline("PayPal payment was declined. Please try again.", sessionId);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const paymentIntent = result?.paymentIntent;
|
|
695
|
+
if (paymentIntent && paymentIntent.id !== paymentIntentId) {
|
|
696
|
+
emitPayPalFailure(
|
|
697
|
+
"Failed to match the returned PayPal payment.",
|
|
698
|
+
"REDIRECT_RESUME_FAILED"
|
|
699
|
+
);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (paymentIntent && (paymentIntent.status === "requires_capture" || paymentIntent.status === "succeeded")) {
|
|
703
|
+
await continueAuthorizedReturn(paymentIntent, continuation);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
emitPayPalDecline("PayPal payment was not completed. Please try again.", sessionId);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
if (!active) return;
|
|
709
|
+
stripePayPalResumeAttempts.delete(retrievalKey);
|
|
710
|
+
emitPayPalFailure(
|
|
711
|
+
error instanceof Error ? error.message : "Failed to complete PayPal payment.",
|
|
712
|
+
"REDIRECT_RESUME_FAILED"
|
|
713
|
+
);
|
|
714
|
+
} finally {
|
|
715
|
+
if (active) processing.value = false;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
onMounted2(async () => {
|
|
719
|
+
const wallets = props.methods.filter(
|
|
720
|
+
(method) => WALLET_METHODS.has(method) && !(method === "paypal" && paypalRedirect())
|
|
721
|
+
);
|
|
722
|
+
const apms = props.methods.filter((method) => !WALLET_METHODS.has(method));
|
|
723
|
+
try {
|
|
724
|
+
const { loadStripe } = await import("@stripe/stripe-js");
|
|
725
|
+
stripe = await loadStripe(props.publishableKey);
|
|
726
|
+
if (!stripe) throw new Error("Stripe.js did not initialize.");
|
|
727
|
+
if (paypalRedirect()) void resumeStripePayPal(stripe);
|
|
728
|
+
if (wallets.length > 0 && walletContainer.value) {
|
|
729
|
+
if (shouldPreferWalletPopup()) cleanups.push(denyPaymentPermissionToNewFrames(document));
|
|
730
|
+
const elements = stripe.elements(walletElementsOptions(wallets));
|
|
731
|
+
const express = elements.create("expressCheckout");
|
|
732
|
+
express.on("click", (event) => gateWalletClick(event));
|
|
733
|
+
express.on("confirm", (event) => {
|
|
734
|
+
const attempt = walletAttempt;
|
|
735
|
+
walletAttempt = null;
|
|
736
|
+
endWalletSheetClick();
|
|
737
|
+
void confirm(elements, event.expressPaymentType, walletIntentKind(), attempt);
|
|
738
|
+
});
|
|
739
|
+
express.on("cancel", () => {
|
|
740
|
+
walletAttempt = null;
|
|
741
|
+
const click = walletSheetClick;
|
|
742
|
+
endWalletSheetClick();
|
|
743
|
+
if (click && isWalletSheetRefusedForWindowSize({
|
|
744
|
+
method: click.method,
|
|
745
|
+
elapsedMs: walletClockNow() - click.startedAt,
|
|
746
|
+
handoffObserved: click.handoffObserved
|
|
747
|
+
})) {
|
|
748
|
+
reportWalletSheetRefusal(click);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
express.mount(walletContainer.value);
|
|
752
|
+
cleanups.push(() => express.destroy());
|
|
753
|
+
}
|
|
754
|
+
if (apms.length > 0 && apmContainer.value) {
|
|
755
|
+
apmElements = stripe.elements(elementsOptions(apms, "inline_apm"));
|
|
756
|
+
const payment = apmElements.create("payment", { layout: "accordion" });
|
|
757
|
+
payment.mount(apmContainer.value);
|
|
758
|
+
cleanups.push(() => payment.destroy());
|
|
759
|
+
}
|
|
760
|
+
} catch (error) {
|
|
761
|
+
const failure = new FloPayError(
|
|
762
|
+
error instanceof Error ? error.message : "Payment methods failed to load.",
|
|
763
|
+
"api_error",
|
|
764
|
+
{ code: "PROVIDER_LOAD_FAILED" }
|
|
765
|
+
);
|
|
766
|
+
invokeMerchantCallback2(() => emit("error", failure));
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
onBeforeUnmount(() => {
|
|
770
|
+
active = false;
|
|
771
|
+
for (const cleanup of cleanups.splice(0)) cleanup();
|
|
772
|
+
});
|
|
773
|
+
return () => h("div", { "data-testid": "flopay-stripe-methods" }, [
|
|
774
|
+
// React renders the Stripe PayPal group above the wallet row.
|
|
775
|
+
paypalRedirect() ? h(
|
|
776
|
+
"div",
|
|
777
|
+
{
|
|
778
|
+
role: "group",
|
|
779
|
+
"aria-label": "PayPal payment method",
|
|
780
|
+
"data-testid": "flopay-paypal-focus-target",
|
|
781
|
+
style: { borderRadius: "8px" }
|
|
782
|
+
},
|
|
783
|
+
[
|
|
784
|
+
h(
|
|
785
|
+
"button",
|
|
786
|
+
{
|
|
787
|
+
type: "button",
|
|
788
|
+
"data-testid": "flopay-stripe-paypal-redirect",
|
|
789
|
+
disabled: busy(),
|
|
790
|
+
onClick: () => void redirectToPayPal(),
|
|
791
|
+
style: {
|
|
792
|
+
width: "100%",
|
|
793
|
+
minHeight: `${PAYPAL_REDIRECT_TILE_HEIGHT}px`,
|
|
794
|
+
border: "1px solid #d1d5db",
|
|
795
|
+
borderRadius: "8px",
|
|
796
|
+
background: "#ffc439",
|
|
797
|
+
color: "#111827",
|
|
798
|
+
cursor: busy() ? "not-allowed" : "pointer",
|
|
799
|
+
fontSize: "1rem",
|
|
800
|
+
fontWeight: 700
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
"Pay with PayPal"
|
|
804
|
+
)
|
|
805
|
+
]
|
|
806
|
+
) : null,
|
|
807
|
+
h("div", { ref: walletContainer, "data-testid": "flopay-express-checkout" }),
|
|
808
|
+
h("div", { ref: apmContainer, "data-testid": "flopay-payment-element" }),
|
|
809
|
+
props.methods.some((method) => !WALLET_METHODS.has(method)) ? h(
|
|
810
|
+
"button",
|
|
811
|
+
{
|
|
812
|
+
type: "button",
|
|
813
|
+
disabled: busy(),
|
|
814
|
+
onClick: () => {
|
|
815
|
+
if (props.isProcessing) return;
|
|
816
|
+
const method = props.methods.find((candidate) => !WALLET_METHODS.has(candidate));
|
|
817
|
+
if (apmElements && method) void confirm(apmElements, method);
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
processing.value ? "Processing\u2026" : "Continue"
|
|
821
|
+
) : null
|
|
822
|
+
]);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
export {
|
|
827
|
+
FLOPAY_CONTEXT,
|
|
828
|
+
CHECKOUT_STATE,
|
|
829
|
+
CHECKOUT_FORM_DEFAULTS,
|
|
830
|
+
FloPayProvider,
|
|
831
|
+
hasStripePayPalReturn,
|
|
832
|
+
StripePaymentMethods
|
|
833
|
+
};
|