PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.8
Booking for Appointments and Events Calendar – Amelia v2.4.8
2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / v3 / src / assets / js / public / stripePaymentIntent.js
ameliabooking / v3 / src / assets / js / public Last commit date
actions.js 1 month ago booking.js 2 days ago cabinet.js 1 month ago cart.js 2 days ago catalog.js 2 days ago coupon.js 1 month ago customFields.js 1 month ago customFontFace.js 1 month ago eventFormCssVars.js 1 month ago events.js 1 month ago facebookPixel.js 1 month ago ivy.js 1 month ago package.js 2 days ago panel.js 1 month ago public.js 1 month ago renderActions.js 1 month ago restore.js 2 days ago slots.js 2 days ago stripePaymentIntent.js 2 days ago translation.js 1 month ago user.js 1 month ago
stripePaymentIntent.js
314 lines
1 import httpClient from '../../../plugins/axios'
2
3 const STRIPE_CANCEL_TIMEOUT_MS = 10000
4 const STRIPE_POLL_INTERVAL_MS = 3000
5 const STRIPE_POLL_MAX_ATTEMPTS = 40
6 const STRIPE_COMPLETE_TIMEOUT_MS = 2500
7
8 const stripeInstances = new Map()
9
10 function getStripeInstance(instanceId) {
11 const key = String(instanceId ?? 'default')
12
13 if (!stripeInstances.has(key)) {
14 stripeInstances.set(key, {
15 activeStripeCacheName: null,
16 cancelInFlight: null,
17 paymentCompleted: false,
18 activeCacheKey: null,
19 activeRequest: null,
20 activeRequestId: 0,
21 })
22 }
23
24 return stripeInstances.get(key)
25 }
26
27 export function buildStripeIntentCacheKey(parts = {}) {
28 return JSON.stringify(parts)
29 }
30
31 export function invalidateStripePaymentIntentRequest(instanceId) {
32 const instance = getStripeInstance(instanceId)
33
34 instance.activeCacheKey = null
35 instance.activeRequest = null
36 instance.activeRequestId += 1
37 }
38
39 export function setActiveStripeCacheName(instanceId, cacheName) {
40 getStripeInstance(instanceId).activeStripeCacheName = cacheName || null
41 }
42
43 export function getActiveStripeCacheName(instanceId) {
44 return getStripeInstance(instanceId).activeStripeCacheName
45 }
46
47 export function setStripePaymentCompleted(instanceId, completed) {
48 getStripeInstance(instanceId).paymentCompleted = !!completed
49 }
50
51 export function cancelStripePaymentIntentByName(cacheName) {
52 if (!cacheName) {
53 return Promise.resolve()
54 }
55
56 return httpClient
57 .post(
58 '/payment/stripe/cancel',
59 { name: cacheName },
60 {
61 timeout: STRIPE_CANCEL_TIMEOUT_MS,
62 },
63 )
64 .catch(() => {
65 // Ignore cancel errors; the pending reservation may already be released.
66 })
67 }
68
69 export function cancelActiveStripePaymentIntent(instanceId) {
70 const instance = getStripeInstance(instanceId)
71
72 if (instance.paymentCompleted) {
73 return Promise.resolve()
74 }
75
76 if (instance.cancelInFlight) {
77 return instance.cancelInFlight
78 }
79
80 const cacheName = instance.activeStripeCacheName
81
82 if (!cacheName) {
83 return Promise.resolve()
84 }
85
86 invalidateStripePaymentIntentRequest(instanceId)
87
88 instance.cancelInFlight = cancelStripePaymentIntentByName(cacheName).finally(() => {
89 if (instance.activeStripeCacheName === cacheName) {
90 instance.activeStripeCacheName = null
91 }
92
93 instance.cancelInFlight = null
94 })
95
96 return instance.cancelInFlight
97 }
98
99 export async function waitForStripeCleanup(instanceId) {
100 const instance = getStripeInstance(instanceId)
101 const pendingIntent = instance.activeRequest
102
103 invalidateStripePaymentIntentRequest(instanceId)
104
105 if (pendingIntent) {
106 try {
107 const response = await pendingIntent
108 await cancelStaleIntentResponse(instanceId, response)
109 } catch (e) {
110 // Stale in-flight intents release their reservation before rejecting.
111 }
112 }
113
114 await cancelActiveStripePaymentIntent(instanceId)
115
116 if (instance.cancelInFlight) {
117 await instance.cancelInFlight
118 }
119 }
120
121 export function prefetchStripePaymentIntent(instanceId, cacheKey, payload, options) {
122 return requestStripePaymentIntent(instanceId, cacheKey, payload, options)
123 }
124
125 export function createStripePaymentElementOptions({ billingDetails, includeAddress = false }) {
126 return {
127 layout: {
128 type: 'accordion',
129 defaultCollapsed: false,
130 radios: 'always',
131 spacedAccordionItems: false,
132 },
133 defaultValues: {
134 billingDetails,
135 },
136 ...(includeAddress
137 ? {
138 fields: {
139 billingDetails: {
140 address: 'auto',
141 },
142 },
143 }
144 : {}),
145 }
146 }
147
148 export function bindStripePaymentElementChange(
149 paymentElement,
150 initVersion,
151 getInitVersion,
152 onTypeChange,
153 ) {
154 paymentElement.on('change', (event) => {
155 if (initVersion !== getInitVersion()) {
156 return
157 }
158
159 onTypeChange(event.value?.type || null, event)
160 })
161 }
162
163 export function persistStripePendingPayment({
164 componentProps,
165 paymentIntentId,
166 cacheName,
167 bookableType,
168 }) {
169 if (!componentProps) {
170 return
171 }
172
173 sessionStorage.setItem(
174 'ameliaCacheData',
175 JSON.stringify({
176 request: componentProps,
177 paymentMethod: 'stripe',
178 type: bookableType,
179 stripePaymentIntentId: paymentIntentId,
180 cacheName,
181 status: 'processing',
182 }),
183 )
184 }
185
186 export function clearStripePendingPayment() {
187 sessionStorage.removeItem('ameliaCacheData')
188 }
189
190 function isStripeCompletionPending(data) {
191 if (!data) {
192 return true
193 }
194
195 if (data.type) {
196 return false
197 }
198
199 const pendingStatuses = [
200 'processing',
201 'requires_action',
202 'requires_confirmation',
203 'requires_source_action',
204 ]
205
206 return !!(data.status && pendingStatuses.includes(data.status))
207 }
208
209 export async function pollStripePaymentCompletion({
210 cacheName,
211 paymentIntentId,
212 recaptcha = null,
213 maxAttempts = STRIPE_POLL_MAX_ATTEMPTS,
214 intervalMs = STRIPE_POLL_INTERVAL_MS,
215 }) {
216 for (let attempt = 0; attempt < maxAttempts; attempt++) {
217 if (attempt > 0) {
218 await new Promise((resolve) => setTimeout(resolve, intervalMs))
219 }
220
221 let response
222
223 try {
224 response = await httpClient.post(
225 '/payment/stripe/complete',
226 {
227 name: cacheName,
228 paymentIntentId,
229 recaptcha,
230 },
231 {
232 timeout: STRIPE_COMPLETE_TIMEOUT_MS,
233 },
234 )
235 } catch (error) {
236 if (error.code === 'ECONNABORTED') {
237 continue
238 }
239
240 throw error
241 }
242
243 const data = response?.data?.data
244
245 if (!isStripeCompletionPending(data)) {
246 if (data?.type) {
247 return response
248 }
249
250 throw new Error(data?.message || 'Payment was not completed')
251 }
252 }
253
254 throw new Error('Payment is still processing')
255 }
256
257 async function cancelStaleIntentResponse(instanceId, response) {
258 const cacheName = response?.data?.data?.cacheName
259
260 if (!cacheName) {
261 return
262 }
263
264 const instance = getStripeInstance(instanceId)
265
266 if (instance.activeStripeCacheName === cacheName) {
267 instance.activeStripeCacheName = null
268 }
269
270 await cancelStripePaymentIntentByName(cacheName)
271 }
272
273 export function requestStripePaymentIntent(instanceId, cacheKey, payload, options) {
274 const instance = getStripeInstance(instanceId)
275
276 if (instance.activeCacheKey === cacheKey && instance.activeRequest) {
277 return instance.activeRequest
278 }
279
280 const requestId = ++instance.activeRequestId
281 instance.activeCacheKey = cacheKey
282 instance.activeRequest = httpClient
283 .post('/payment/stripe/intent', payload, options)
284 .then(async (response) => {
285 if (instance.activeRequestId !== requestId || instance.activeCacheKey !== cacheKey) {
286 await cancelStaleIntentResponse(instanceId, response)
287
288 throw new Error('Stale Stripe payment intent request')
289 }
290
291 instance.activeCacheKey = null
292 instance.activeRequest = null
293
294 const cacheName = response?.data?.data?.cacheName
295
296 if (cacheName) {
297 setStripePaymentCompleted(instanceId, false)
298 setActiveStripeCacheName(instanceId, cacheName)
299 }
300
301 return response
302 })
303 .catch((error) => {
304 if (instance.activeRequestId === requestId && instance.activeCacheKey === cacheKey) {
305 instance.activeCacheKey = null
306 instance.activeRequest = null
307 }
308
309 throw error
310 })
311
312 return instance.activeRequest
313 }
314