@verbb/formie-browser 1.0.14 → 1.0.16
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/CHANGELOG.md +66 -0
- package/dist/chunks/{address-country-Dn71jroa.js → address-country-DLJF-OkC.js} +5 -9
- package/dist/chunks/{address-finder-O49uDRoR.js → address-finder-QK1m1eyv.js} +2 -2
- package/dist/chunks/{address-state-ByYatUur.js → address-state-CHKit8AW.js} +113 -112
- package/dist/chunks/{api-DYYsVIub.js → api-BPtH5RBA.js} +162 -130
- package/dist/chunks/{api-D6kJ7kXZ.js → api-BR2Gqy37.js} +313 -303
- package/dist/chunks/{api-ghNr2uxX.js → api-CmuOZ0i3.js} +1 -1
- package/dist/chunks/{async-nPFRNQ06.js → async-2GdXNFCE.js} +18 -1
- package/dist/chunks/{bpoint-D0RKFM4F.js → bpoint-GvYfyPjD.js} +1 -1
- package/dist/chunks/{calculations-YEoVCG2q.js → calculations-DeEmdUDq.js} +5 -7
- package/dist/chunks/{captcha-eu-DERJbXnH.js → captcha-eu-eU_M6snp.js} +2 -2
- package/dist/chunks/{checkbox-radio-gsm8QPJg.js → checkbox-radio-CSXo6svm.js} +4 -12
- package/dist/chunks/{combobox-CWNdx4Fk.js → combobox-CJh2DoSO.js} +323 -363
- package/dist/chunks/{conditions-DqNHLTbN.js → conditions-Dz3068K0.js} +140 -153
- package/dist/chunks/{country-from-ip-DDj_mFki.js → country-from-ip-BqQ-BqLH.js} +1 -1
- package/dist/chunks/{custom-google-maps-C4l2S3GO.js → custom-google-maps-rNm5VFbb.js} +2 -6
- package/dist/chunks/{custom-link-CLazgKYH.js → custom-link-CLqFCssQ.js} +1 -1
- package/dist/chunks/{custom-maps-DqcAlD0v.js → custom-maps-DgjYQDLj.js} +4 -8
- package/dist/chunks/{date-picker-RUnevF7l.js → date-picker-BVJxJVYT.js} +42 -58
- package/dist/chunks/{debug-BV0DvdHx.js → debug-rhzqP1j2.js} +2 -14
- package/dist/chunks/{dist-ZLeW0zZ3.js → dist-CJReDlDK.js} +13 -8
- package/dist/chunks/{eway-or6dtjoT.js → eway-BZuTBAUW.js} +3 -3
- package/dist/chunks/{field-references.resolver-Ca8MguFv.js → field-references.resolver-uSHf5-NP.js} +1 -1
- package/dist/chunks/{file-upload-Bm-TXyNB.js → file-upload-vEUr_EuP.js} +131 -113
- package/dist/chunks/{friendly-captcha-v1-GFqCt1Mh.js → friendly-captcha-v1-C_cF1WD2.js} +2 -2
- package/dist/chunks/{friendly-captcha-v2-WoW246JG.js → friendly-captcha-v2-BenESH-n.js} +2 -2
- package/dist/chunks/{friendly-challenge-DPG9MJb1.js → friendly-challenge-BgPBEMXx.js} +21 -35
- package/dist/chunks/{go-cardless-DzJ-0FKg.js → go-cardless-DWZeB0Bq.js} +2 -6
- package/dist/chunks/{google-address-C1P1nTDM.js → google-address-DQR6rsbh.js} +15 -10
- package/dist/chunks/{hcaptcha-D0T8aIHz.js → hcaptcha-COilLmRT.js} +2 -2
- package/dist/chunks/{hidden-BZhXnNPe.js → hidden-Bc2TpcgF.js} +1 -1
- package/dist/chunks/{http-C2QdHuWO.js → http-Dj_skqaa.js} +1 -1
- package/dist/chunks/{leaflet-src-DE6rv6M-.js → leaflet-src-Cg-LUOD8.js} +74 -122
- package/dist/chunks/{loqate-DD1S7PY9.js → loqate-ZZClcacr.js} +2 -2
- package/dist/chunks/{mollie-DG8Ju83x.js → mollie-8OMXIYa8.js} +2 -6
- package/dist/chunks/{moneris-C8m3Fvqu.js → moneris-qYHJ9EFy.js} +1 -1
- package/dist/chunks/opayo-DlWPEnla.js +159 -0
- package/dist/chunks/{paddle-BIYR9Um3.js → paddle-CArfWih0.js} +2 -2
- package/dist/chunks/{password-validation-C8_uO_c_.js → password-validation-HGOU9kf3.js} +2 -2
- package/dist/chunks/{paypal-B_yxIo2d.js → paypal-BcaMCMGY.js} +2 -2
- package/dist/chunks/{payway-CeOzS0sL.js → payway-vxC7_bfR.js} +4 -12
- package/dist/chunks/{phone-country-bAqje17P.js → phone-country-Bx9EG50h.js} +124 -141
- package/dist/chunks/{place-kit-BE5gi3vo.js → place-kit-BKk8xDXf.js} +10 -10
- package/dist/chunks/{placekit-autocomplete.esm-CZZERYur.js → placekit-autocomplete.esm-D2A_0iVU.js} +2 -5
- package/dist/chunks/{recaptcha-enterprise-CruXddZq.js → recaptcha-enterprise-Cx6HUpMa.js} +6 -12
- package/dist/chunks/recaptcha-shared-CcMI-r8t.js +33 -0
- package/dist/chunks/{recaptcha-v2-checkbox-CCnJYCnG.js → recaptcha-v2-checkbox-DQ9BJzcv.js} +2 -2
- package/dist/chunks/{recaptcha-v2-invisible-BuVqRRYn.js → recaptcha-v2-invisible-BP0T8Pan.js} +2 -2
- package/dist/chunks/recaptcha-v3-DKMGQ27K.js +38 -0
- package/dist/chunks/{repeater-DwE27jI1.js → repeater-BR0RUvTs.js} +50 -40
- package/dist/chunks/{rich-text-uV4o_pGk.js → rich-text-H2A6cm1t.js} +33 -11
- package/dist/chunks/{rolldown-runtime-DtPi1Y-2.js → rolldown-runtime-VHNaYcLd.js} +1 -1
- package/dist/chunks/{scripts-CbQ7agX3.js → scripts-DrccjrFv.js} +1 -1
- package/dist/chunks/{sdk-lkwSvlpM.js → sdk-_cmrRYgp.js} +4 -16
- package/dist/chunks/{shared-OKhSay59.js → shared-CJ1LIEh3.js} +1 -3
- package/dist/chunks/{signature-7cldFbqj.js → signature-CfKA_poI.js} +9 -41
- package/dist/chunks/skip-CEW89ExY.js +7 -0
- package/dist/chunks/{snaptcha-C04kNGYQ.js → snaptcha-P3EoSQud.js} +1 -1
- package/dist/chunks/{square-DEzxv9D9.js → square-Dyz0p_GU.js} +2 -2
- package/dist/chunks/{stripe-CVPQGDz3.js → stripe-Bfd_jp_k.js} +4 -7
- package/dist/chunks/{summary-BF_8mrXI.js → summary-B3CAebLV.js} +45 -37
- package/dist/chunks/{survey-likert-fk1hUPff.js → survey-likert-CEtUceag.js} +1 -1
- package/dist/chunks/{survey-rank-BsLGl2LX.js → survey-rank-Dtt97y6s.js} +3 -7
- package/dist/chunks/{survey-rating-DVS6e6fM.js → survey-rating-mlDMI0uq.js} +9 -23
- package/dist/chunks/{table-DnIBdeKF.js → table-0k6Md0IO.js} +2 -2
- package/dist/chunks/{text-limit-LGMPhFuC.js → text-limit-CTVML6s7.js} +10 -10
- package/dist/chunks/{turnstile-n9DNyPxi.js → turnstile-Sp4ubowK.js} +2 -2
- package/dist/chunks/{upload-manager-BbiUosZq.js → upload-manager-Ccg3d_4I.js} +258 -260
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +743 -734
- package/dist/js/core/create-formie-client.d.ts.map +1 -1
- package/dist/js/core/dom-events.d.ts +5 -0
- package/dist/js/core/dom-events.d.ts.map +1 -1
- package/dist/js/core/page-client-event.d.ts.map +1 -1
- package/dist/js/core/submit-result-state.d.ts.map +1 -1
- package/dist/js/core/submit-result-ui.d.ts +3 -1
- package/dist/js/core/submit-result-ui.d.ts.map +1 -1
- package/dist/js/core/validation-focus.d.ts.map +1 -1
- package/dist/js/modules/address/google-address.d.ts +10 -0
- package/dist/js/modules/address/google-address.d.ts.map +1 -1
- package/dist/js/modules/address/place-kit.d.ts.map +1 -1
- package/dist/js/modules/captchas/factories.d.ts.map +1 -1
- package/dist/js/modules/captchas/host.d.ts +4 -1
- package/dist/js/modules/captchas/host.d.ts.map +1 -1
- package/dist/js/modules/captchas/recaptcha-shared.d.ts +1 -1
- package/dist/js/modules/captchas/recaptcha-shared.d.ts.map +1 -1
- package/dist/js/modules/captchas/recaptcha-v3.d.ts.map +1 -1
- package/dist/js/modules/fields/address-state.d.ts.map +1 -1
- package/dist/js/modules/fields/combobox.d.ts +2 -0
- package/dist/js/modules/fields/combobox.d.ts.map +1 -1
- package/dist/js/modules/fields/conditions/effects.d.ts.map +1 -1
- package/dist/js/modules/fields/conditions.d.ts.map +1 -1
- package/dist/js/modules/fields/date-picker.d.ts.map +1 -1
- package/dist/js/modules/fields/file-upload.d.ts.map +1 -1
- package/dist/js/modules/fields/phone-country.d.ts.map +1 -1
- package/dist/js/modules/fields/repeater.d.ts.map +1 -1
- package/dist/js/modules/fields/rich-text.d.ts.map +1 -1
- package/dist/js/modules/fields/summary.d.ts.map +1 -1
- package/dist/js/modules/fields/text-limit.d.ts.map +1 -1
- package/dist/js/modules/fields/upload-manager.d.ts.map +1 -1
- package/dist/js/modules/payments/factories.d.ts.map +1 -1
- package/dist/js/modules/payments/opayo.d.ts.map +1 -1
- package/dist/js/submit/pipeline.d.ts +2 -0
- package/dist/js/submit/pipeline.d.ts.map +1 -1
- package/dist/js/utils/async.d.ts +1 -0
- package/dist/js/utils/async.d.ts.map +1 -1
- package/dist/js/utils/form-started-at.d.ts +1 -0
- package/dist/js/utils/form-started-at.d.ts.map +1 -1
- package/dist/js/utils/unload-warning.d.ts +4 -0
- package/dist/js/utils/unload-warning.d.ts.map +1 -1
- package/dist/js/validation/rules/required.d.ts.map +1 -1
- package/dist/js/validation/submit-readiness.d.ts.map +1 -1
- package/dist/js/validation/validator.d.ts +1 -0
- package/dist/js/validation/validator.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/js/core/create-formie-client.ts +37 -28
- package/src/js/core/dom-events.ts +30 -1
- package/src/js/core/page-client-event.ts +5 -7
- package/src/js/core/submit-result-state.ts +30 -2
- package/src/js/core/submit-result-ui.ts +25 -3
- package/src/js/core/validation-focus.ts +17 -3
- package/src/js/modules/address/google-address.ts +95 -37
- package/src/js/modules/address/place-kit.ts +13 -7
- package/src/js/modules/captchas/factories.ts +241 -101
- package/src/js/modules/captchas/host.ts +16 -0
- package/src/js/modules/captchas/recaptcha-enterprise.ts +1 -1
- package/src/js/modules/captchas/recaptcha-shared.ts +23 -5
- package/src/js/modules/captchas/recaptcha-v3.ts +23 -14
- package/src/js/modules/fields/address-country.ts +5 -5
- package/src/js/modules/fields/address-state.ts +80 -27
- package/src/js/modules/fields/combobox.ts +22 -1
- package/src/js/modules/fields/conditions/effects.ts +4 -5
- package/src/js/modules/fields/conditions.ts +9 -0
- package/src/js/modules/fields/date-picker.ts +6 -3
- package/src/js/modules/fields/file-upload.ts +55 -18
- package/src/js/modules/fields/phone-country.ts +25 -2
- package/src/js/modules/fields/repeater.ts +55 -4
- package/src/js/modules/fields/rich-text.ts +33 -0
- package/src/js/modules/fields/summary.ts +63 -10
- package/src/js/modules/fields/text-limit.ts +11 -0
- package/src/js/modules/fields/upload-manager.ts +54 -18
- package/src/js/modules/payments/factories.ts +87 -54
- package/src/js/modules/payments/opayo.ts +7 -0
- package/src/js/submit/pipeline.ts +16 -9
- package/src/js/utils/async.ts +34 -0
- package/src/js/utils/form-started-at.ts +19 -4
- package/src/js/utils/unload-warning.ts +63 -7
- package/src/js/validation/rules/required.ts +18 -0
- package/src/js/validation/submit-readiness.ts +2 -0
- package/src/js/validation/validator.ts +29 -6
- package/src/vendor.d.ts +2 -0
- package/dist/chunks/opayo-B3EvrVxw.js +0 -188
- package/dist/chunks/recaptcha-shared-AQRMDMme.js +0 -27
- package/dist/chunks/recaptcha-v3-CloqhO2A.js +0 -28
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '#modules/captchas/host';
|
|
17
17
|
import { createDebug } from '#utils/debug';
|
|
18
18
|
import { getFormStateEventName } from '#utils/event-names';
|
|
19
|
+
import type { FormUnloadWarningExclusionsEventDetail } from '#utils/unload-warning';
|
|
19
20
|
|
|
20
21
|
type Cleanup = () => void;
|
|
21
22
|
const debug = createDebug('captchas');
|
|
@@ -91,40 +92,112 @@ export function createCaptchaModule<TProvider extends Record<string, unknown> =
|
|
|
91
92
|
surfaces: ['server-rendered', 'client-rendered'],
|
|
92
93
|
kind: 'captcha',
|
|
93
94
|
match: () => true,
|
|
94
|
-
setup: async(ctx) => {
|
|
95
|
+
setup: async (ctx) => {
|
|
95
96
|
// Every captcha module, passive or widget-backed, starts from the
|
|
96
97
|
// same two building blocks:
|
|
97
98
|
// 1. normalized options from the backend manifest
|
|
98
99
|
// 2. shared host services for placeholders, errors, tokens, and
|
|
99
100
|
// refresh-token events
|
|
100
|
-
const options = normalizeCaptchaModuleOptions<TProvider>(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
const options = normalizeCaptchaModuleOptions<TProvider>(
|
|
102
|
+
moduleId.split(':')[1],
|
|
103
|
+
ctx.options || {},
|
|
104
|
+
{
|
|
105
|
+
defaultPlaceholderSelector,
|
|
106
|
+
defaultTokenFieldNames,
|
|
107
|
+
defaultWaitForValueMs,
|
|
108
|
+
},
|
|
109
|
+
);
|
|
105
110
|
debug.log('Setup module.', {
|
|
106
111
|
moduleId: moduleId,
|
|
107
112
|
placeholderSelector: options.ui.placeholderSelector,
|
|
108
113
|
tokenFieldNames: options.transport.tokenFieldNames,
|
|
109
114
|
});
|
|
110
115
|
let placeholder: HTMLElement | undefined;
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
if (
|
|
117
|
+
ctx.surface === 'client-rendered' &&
|
|
118
|
+
!ctx.root.querySelector(options.ui.placeholderSelector)
|
|
119
|
+
) {
|
|
120
|
+
const attribute = /^\[(data-[a-z0-9-]+)\]$/.exec(
|
|
121
|
+
options.ui.placeholderSelector,
|
|
122
|
+
)?.[1];
|
|
123
|
+
if (!attribute)
|
|
124
|
+
throw new Error(
|
|
125
|
+
'The CAPTCHA needs a supported placeholder in the client-rendered form.',
|
|
126
|
+
);
|
|
114
127
|
placeholder = document.createElement('div');
|
|
115
128
|
placeholder.setAttribute(attribute, '');
|
|
116
129
|
(ctx.form ?? ctx.root).append(placeholder);
|
|
117
130
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
// Providers own their transport names. Keep refreshed names until
|
|
132
|
+
// teardown so removing or rotating a token cannot dirty the baseline.
|
|
133
|
+
const excludedFieldNames = new Set(
|
|
134
|
+
options.transport.tokenFieldNames,
|
|
135
|
+
);
|
|
136
|
+
let active = true;
|
|
137
|
+
const trackTokenNames = (names: string[]) => {
|
|
138
|
+
names
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.forEach((name) => excludedFieldNames.add(name));
|
|
141
|
+
};
|
|
142
|
+
trackTokenNames(
|
|
143
|
+
options.transport.sessionKey
|
|
144
|
+
? [options.transport.sessionKey]
|
|
145
|
+
: [],
|
|
146
|
+
);
|
|
147
|
+
const services = createCaptchaHostServices(ctx, options, {
|
|
148
|
+
onTokenFieldNames: trackTokenNames,
|
|
149
|
+
isActive: () => active,
|
|
124
150
|
});
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
151
|
+
const stopCollecting = services.events.onForm(
|
|
152
|
+
'formie:unload-warning:collect-exclusions',
|
|
153
|
+
(event) => {
|
|
154
|
+
const detail = (
|
|
155
|
+
event as CustomEvent<FormUnloadWarningExclusionsEventDetail>
|
|
156
|
+
).detail;
|
|
157
|
+
if (detail?.form === ctx.form) {
|
|
158
|
+
detail.exclude(...excludedFieldNames);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
const stopTrackingRefreshes = services.refresh.onTokensRefreshed(
|
|
163
|
+
(entry) => {
|
|
164
|
+
if (
|
|
165
|
+
typeof entry.sessionKey === 'string' &&
|
|
166
|
+
entry.sessionKey.trim() !== ''
|
|
167
|
+
) {
|
|
168
|
+
trackTokenNames([entry.sessionKey.trim()]);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
const cleanup = () => {
|
|
173
|
+
active = false;
|
|
174
|
+
stopCollecting();
|
|
175
|
+
stopTrackingRefreshes();
|
|
176
|
+
placeholder?.remove();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const instance = await setup({ ...ctx, options, services });
|
|
181
|
+
if (!instance) {
|
|
182
|
+
cleanup();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const destroy = instance.destroy;
|
|
187
|
+
return {
|
|
188
|
+
...instance,
|
|
189
|
+
destroy: async () => {
|
|
190
|
+
try {
|
|
191
|
+
await destroy();
|
|
192
|
+
} finally {
|
|
193
|
+
cleanup();
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
cleanup();
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
128
201
|
},
|
|
129
202
|
};
|
|
130
203
|
}
|
|
@@ -237,12 +310,14 @@ export function createManagedCaptchaModule<
|
|
|
237
310
|
TProvider extends Record<string, unknown>,
|
|
238
311
|
TApi,
|
|
239
312
|
TWidget,
|
|
240
|
-
>(
|
|
313
|
+
>(
|
|
314
|
+
adapter: ManagedCaptchaModuleAdapter<TProvider, TApi, TWidget>,
|
|
315
|
+
): BrowserModuleDefinition {
|
|
241
316
|
return createCaptchaModule<TProvider>({
|
|
242
317
|
moduleId: adapter.moduleId,
|
|
243
318
|
defaultPlaceholderSelector: adapter.defaultPlaceholderSelector,
|
|
244
319
|
defaultTokenFieldNames: adapter.defaultTokenFieldNames,
|
|
245
|
-
setup: async(ctx) => {
|
|
320
|
+
setup: async (ctx) => {
|
|
246
321
|
// Managed providers are the widget/SDK-backed family. The factory
|
|
247
322
|
// owns only the generic plumbing:
|
|
248
323
|
// - load the provider API once
|
|
@@ -254,13 +329,15 @@ export function createManagedCaptchaModule<
|
|
|
254
329
|
const cleanups: Cleanup[] = [];
|
|
255
330
|
const mountedWidgets = new Map<HTMLElement, TWidget>();
|
|
256
331
|
const mountPromises = new Map<HTMLElement, Promise<void>>();
|
|
332
|
+
const unmountPromises = new Map<HTMLElement, Promise<void>>();
|
|
333
|
+
let destructionPromise: Promise<void> | null = null;
|
|
257
334
|
let activePlaceholder = ctx.services.placeholder.getPrimary();
|
|
258
335
|
let destroyed = false;
|
|
259
336
|
let initializationError: unknown;
|
|
260
337
|
|
|
261
338
|
let apiPromise: Promise<TApi> | null = null;
|
|
262
339
|
|
|
263
|
-
const getApi = async(): Promise<TApi> => {
|
|
340
|
+
const getApi = async (): Promise<TApi> => {
|
|
264
341
|
if (!apiPromise) {
|
|
265
342
|
debug.log('Loading captcha provider API.', {
|
|
266
343
|
moduleId: adapter.moduleId,
|
|
@@ -271,55 +348,67 @@ export function createManagedCaptchaModule<
|
|
|
271
348
|
return apiPromise;
|
|
272
349
|
};
|
|
273
350
|
|
|
274
|
-
const unmountPlaceholder =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (!widget) {
|
|
280
|
-
placeholder.innerHTML = '';
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
351
|
+
const unmountPlaceholder = (
|
|
352
|
+
placeholder: HTMLElement,
|
|
353
|
+
): Promise<void> => {
|
|
354
|
+
const pending = unmountPromises.get(placeholder);
|
|
355
|
+
if (pending) return pending;
|
|
283
356
|
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
await
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
357
|
+
const promise = (async () => {
|
|
358
|
+
// A widget can finish mounting after its placeholder hides or
|
|
359
|
+
// teardown starts. Wait so its provider resources are released too.
|
|
360
|
+
await mountPromises.get(placeholder)?.catch(() => {});
|
|
361
|
+
ctx.services.errors.clear(placeholder);
|
|
362
|
+
const hasWidget = mountedWidgets.has(placeholder);
|
|
363
|
+
const widget = mountedWidgets.get(placeholder) as TWidget;
|
|
364
|
+
mountedWidgets.delete(placeholder);
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
if (hasWidget && adapter.unmount) {
|
|
368
|
+
const api = await getApi();
|
|
369
|
+
await adapter.unmount({
|
|
370
|
+
api,
|
|
371
|
+
widget,
|
|
372
|
+
placeholder,
|
|
373
|
+
services: ctx.services,
|
|
374
|
+
options: ctx.options,
|
|
375
|
+
provider: ctx.options.provider,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
} finally {
|
|
379
|
+
// Provider failures must not leave transport values or DOM
|
|
380
|
+
// behind, or prevent another widget from being cleaned up.
|
|
381
|
+
placeholder.innerHTML = '';
|
|
382
|
+
ctx.services.tokens.clear();
|
|
383
|
+
if (activePlaceholder === placeholder) {
|
|
384
|
+
activePlaceholder =
|
|
385
|
+
ctx.services.placeholder.getPrimary();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
})().finally(() => unmountPromises.delete(placeholder));
|
|
303
389
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
390
|
+
unmountPromises.set(placeholder, promise);
|
|
391
|
+
return promise;
|
|
307
392
|
};
|
|
308
393
|
|
|
309
|
-
const mountPlaceholder = async(placeholder: HTMLElement) => {
|
|
394
|
+
const mountPlaceholder = async (placeholder: HTMLElement) => {
|
|
395
|
+
const unmounting = unmountPromises.get(placeholder);
|
|
396
|
+
if (unmounting) await unmounting;
|
|
310
397
|
if (destroyed || mountedWidgets.has(placeholder)) return;
|
|
311
|
-
if (mountPromises.has(placeholder))
|
|
398
|
+
if (mountPromises.has(placeholder))
|
|
399
|
+
return mountPromises.get(placeholder);
|
|
312
400
|
|
|
313
401
|
// Guard against repeated visibility events or rapid DOM churn
|
|
314
402
|
// by tracking an in-flight mount promise per placeholder.
|
|
315
|
-
const promise = (async() => {
|
|
403
|
+
const promise = (async () => {
|
|
316
404
|
const api = await getApi();
|
|
317
405
|
|
|
318
406
|
if (destroyed || mountedWidgets.has(placeholder)) {
|
|
319
407
|
return;
|
|
320
408
|
}
|
|
321
409
|
|
|
322
|
-
const container =
|
|
410
|
+
const container =
|
|
411
|
+
ctx.services.placeholder.createContainer(placeholder);
|
|
323
412
|
// Providers always render into a fresh container so the
|
|
324
413
|
// shared services can safely tear down and rebuild
|
|
325
414
|
// placeholder DOM.
|
|
@@ -349,16 +438,25 @@ export function createManagedCaptchaModule<
|
|
|
349
438
|
const visibility = ctx.services.placeholder.observe(
|
|
350
439
|
(placeholder) => {
|
|
351
440
|
activePlaceholder = placeholder;
|
|
352
|
-
void mountPlaceholder(placeholder).catch((error) => {
|
|
441
|
+
void mountPlaceholder(placeholder).catch((error) => {
|
|
442
|
+
initializationError = error;
|
|
443
|
+
});
|
|
353
444
|
},
|
|
354
445
|
(placeholder) => {
|
|
355
|
-
|
|
446
|
+
if (!destroyed) {
|
|
447
|
+
void unmountPlaceholder(placeholder).catch((error) => {
|
|
448
|
+
initializationError = error;
|
|
449
|
+
});
|
|
450
|
+
}
|
|
356
451
|
},
|
|
357
452
|
);
|
|
358
453
|
|
|
359
454
|
cleanups.push(visibility.cleanup);
|
|
360
455
|
|
|
361
|
-
const rerenderVisiblePlaceholders = async(
|
|
456
|
+
const rerenderVisiblePlaceholders = async (
|
|
457
|
+
reason: 'submit-result' | 'reset-state',
|
|
458
|
+
) => {
|
|
459
|
+
if (destroyed) return;
|
|
362
460
|
// Some providers can cheaply reset in-place, while others are
|
|
363
461
|
// safer to unmount and mount again. The provider advertises
|
|
364
462
|
// that through the optional `reset()` hook.
|
|
@@ -369,9 +467,9 @@ export function createManagedCaptchaModule<
|
|
|
369
467
|
const api = await getApi();
|
|
370
468
|
|
|
371
469
|
for (const placeholder of placeholdersToMount) {
|
|
372
|
-
const widget = mountedWidgets.get(placeholder);
|
|
470
|
+
const widget = mountedWidgets.get(placeholder) as TWidget;
|
|
373
471
|
|
|
374
|
-
if (!
|
|
472
|
+
if (!mountedWidgets.has(placeholder)) {
|
|
375
473
|
await mountPlaceholder(placeholder);
|
|
376
474
|
continue;
|
|
377
475
|
}
|
|
@@ -405,57 +503,94 @@ export function createManagedCaptchaModule<
|
|
|
405
503
|
visibility.reconcile();
|
|
406
504
|
};
|
|
407
505
|
|
|
408
|
-
cleanups.push(
|
|
409
|
-
|
|
506
|
+
cleanups.push(
|
|
507
|
+
ctx.services.events.onRoot('formie:submit:result', (event) => {
|
|
508
|
+
const detail =
|
|
509
|
+
event instanceof CustomEvent
|
|
510
|
+
? (event.detail as FormSubmitResult)
|
|
511
|
+
: null;
|
|
410
512
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
513
|
+
if (detail?.stage === 'validate') {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
414
516
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
517
|
+
if (detail?.ok === false && detail?.stage === 'challenge') {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
418
520
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
521
|
+
if (detail?.ok === true) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
422
524
|
|
|
423
|
-
|
|
424
|
-
|
|
525
|
+
void rerenderVisiblePlaceholders('submit-result');
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
425
528
|
|
|
426
529
|
if (ctx.form) {
|
|
427
|
-
cleanups.push(
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
530
|
+
cleanups.push(
|
|
531
|
+
ctx.services.events.onForm(
|
|
532
|
+
getFormStateEventName('reset'),
|
|
533
|
+
() => {
|
|
534
|
+
activePlaceholder =
|
|
535
|
+
ctx.services.placeholder.getPrimary() ||
|
|
536
|
+
activePlaceholder;
|
|
537
|
+
|
|
538
|
+
window.setTimeout(() => {
|
|
539
|
+
void rerenderVisiblePlaceholders('reset-state');
|
|
540
|
+
}, 0);
|
|
541
|
+
},
|
|
542
|
+
),
|
|
543
|
+
);
|
|
434
544
|
}
|
|
435
545
|
|
|
546
|
+
const destroy = (): Promise<void> => {
|
|
547
|
+
if (destructionPromise) return destructionPromise;
|
|
548
|
+
destroyed = true;
|
|
549
|
+
destructionPromise = (async () => {
|
|
550
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
551
|
+
await Promise.allSettled([...mountPromises.values()]);
|
|
552
|
+
const results = await Promise.allSettled(
|
|
553
|
+
new Set([
|
|
554
|
+
...unmountPromises.values(),
|
|
555
|
+
...Array.from(
|
|
556
|
+
mountedWidgets.keys(),
|
|
557
|
+
unmountPlaceholder,
|
|
558
|
+
),
|
|
559
|
+
]),
|
|
560
|
+
);
|
|
561
|
+
const errors = results.flatMap((result) =>
|
|
562
|
+
result.status === 'rejected' ? [result.reason] : [],
|
|
563
|
+
);
|
|
564
|
+
if (errors.length)
|
|
565
|
+
throw new AggregateError(
|
|
566
|
+
errors,
|
|
567
|
+
'CAPTCHA widgets could not clean up.',
|
|
568
|
+
);
|
|
569
|
+
})();
|
|
570
|
+
return destructionPromise;
|
|
571
|
+
};
|
|
572
|
+
|
|
436
573
|
visibility.reconcileImmediate();
|
|
437
574
|
try {
|
|
438
575
|
await Promise.all([...mountPromises.values()]);
|
|
439
576
|
if (initializationError) throw initializationError;
|
|
440
577
|
} catch (error) {
|
|
441
|
-
|
|
442
|
-
|
|
578
|
+
// Setup can fail after another placeholder has already mounted.
|
|
579
|
+
// Preserve the setup error while still releasing every widget.
|
|
580
|
+
await destroy().catch((cleanupError) => {
|
|
581
|
+
debug.warn('Captcha cleanup after failed setup.', {
|
|
582
|
+
error: cleanupError,
|
|
583
|
+
});
|
|
584
|
+
});
|
|
443
585
|
throw error;
|
|
444
586
|
}
|
|
445
587
|
|
|
446
588
|
return {
|
|
447
|
-
assertReady: () => {
|
|
448
|
-
|
|
449
|
-
destroyed = true;
|
|
450
|
-
cleanups.forEach((cleanup) => {
|
|
451
|
-
cleanup();
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
for (const placeholder of Array.from(mountedWidgets.keys())) {
|
|
455
|
-
await unmountPlaceholder(placeholder);
|
|
456
|
-
}
|
|
589
|
+
assertReady: () => {
|
|
590
|
+
if (initializationError) throw initializationError;
|
|
457
591
|
},
|
|
458
|
-
|
|
592
|
+
destroy,
|
|
593
|
+
beforeSubmit: async (stageCtx) => {
|
|
459
594
|
if (stageCtx.action !== 'submit') {
|
|
460
595
|
return;
|
|
461
596
|
}
|
|
@@ -475,22 +610,27 @@ export function createManagedCaptchaModule<
|
|
|
475
610
|
return;
|
|
476
611
|
}
|
|
477
612
|
|
|
478
|
-
let placeholder =
|
|
479
|
-
|
|
613
|
+
let placeholder =
|
|
614
|
+
visiblePlaceholders.find(
|
|
615
|
+
(candidate) => candidate === activePlaceholder,
|
|
616
|
+
) || visiblePlaceholders[0];
|
|
480
617
|
|
|
481
618
|
await mountPlaceholder(placeholder);
|
|
482
619
|
placeholder = activePlaceholder || placeholder;
|
|
483
620
|
|
|
484
621
|
ctx.services.errors.clear(placeholder);
|
|
485
622
|
|
|
486
|
-
const widget = mountedWidgets.get(placeholder);
|
|
623
|
+
const widget = mountedWidgets.get(placeholder) as TWidget;
|
|
487
624
|
|
|
488
|
-
if (!
|
|
625
|
+
if (!mountedWidgets.has(placeholder)) {
|
|
489
626
|
const message = ctx.services.errors.getDefaultMessage();
|
|
490
627
|
ctx.services.errors.show(message, placeholder);
|
|
491
|
-
debug.warn(
|
|
492
|
-
|
|
493
|
-
|
|
628
|
+
debug.warn(
|
|
629
|
+
'Captcha widget unavailable at challenge stage.',
|
|
630
|
+
{
|
|
631
|
+
moduleId: adapter.moduleId,
|
|
632
|
+
},
|
|
633
|
+
);
|
|
494
634
|
stageCtx.abort(message);
|
|
495
635
|
return;
|
|
496
636
|
}
|
|
@@ -237,8 +237,11 @@ function observeVisiblePlaceholders(
|
|
|
237
237
|
onHide: (placeholder: HTMLElement) => void,
|
|
238
238
|
): ObserveVisiblePlaceholdersResult {
|
|
239
239
|
const visible = new Set<HTMLElement>();
|
|
240
|
+
let active = true;
|
|
240
241
|
|
|
241
242
|
const reconcileNow = () => {
|
|
243
|
+
// A debounced observer callback may already be queued when cleanup runs.
|
|
244
|
+
if (!active) return;
|
|
242
245
|
const placeholders = queryCaptchaPlaceholders(root, selector);
|
|
243
246
|
const nextVisible = new Set(placeholders.filter((placeholder) => isElementVisible(placeholder)));
|
|
244
247
|
|
|
@@ -291,6 +294,8 @@ function observeVisiblePlaceholders(
|
|
|
291
294
|
|
|
292
295
|
return {
|
|
293
296
|
cleanup: () => {
|
|
297
|
+
if (!active) return;
|
|
298
|
+
active = false;
|
|
294
299
|
mutationObserver.disconnect();
|
|
295
300
|
cleanups.forEach((cleanup) => {
|
|
296
301
|
cleanup();
|
|
@@ -371,6 +376,10 @@ export function normalizeCaptchaModuleOptions<TProvider extends Record<string, u
|
|
|
371
376
|
export function createCaptchaHostServices<TProvider extends Record<string, unknown>>(
|
|
372
377
|
ctx: ModuleSetupContext,
|
|
373
378
|
options: NormalizedCaptchaModuleOptions<TProvider>,
|
|
379
|
+
lifecycle: {
|
|
380
|
+
onTokenFieldNames?: (names: string[]) => void;
|
|
381
|
+
isActive?: () => boolean;
|
|
382
|
+
} = {},
|
|
374
383
|
): CaptchaHostServices {
|
|
375
384
|
const tokenRoot = ctx.form || ctx.root;
|
|
376
385
|
const placeholderSelector = options.ui.placeholderSelector;
|
|
@@ -396,9 +405,11 @@ export function createCaptchaHostServices<TProvider extends Record<string, unkno
|
|
|
396
405
|
errors: {
|
|
397
406
|
getDefaultMessage: () => options.ui.errorMessage,
|
|
398
407
|
show: (message, placeholder) => {
|
|
408
|
+
if (lifecycle.isActive?.() === false) return;
|
|
399
409
|
showCaptchaError(placeholder || getPrimaryPlaceholder(ctx.root, placeholderSelector), message || options.ui.errorMessage, ctx.form || ctx.root);
|
|
400
410
|
},
|
|
401
411
|
clear: (placeholder) => {
|
|
412
|
+
if (lifecycle.isActive?.() === false) return;
|
|
402
413
|
clearCaptchaError(placeholder || getPrimaryPlaceholder(ctx.root, placeholderSelector));
|
|
403
414
|
},
|
|
404
415
|
},
|
|
@@ -411,6 +422,10 @@ export function createCaptchaHostServices<TProvider extends Record<string, unkno
|
|
|
411
422
|
root = tokenRoot,
|
|
412
423
|
container = ctx.form,
|
|
413
424
|
} = {}) => {
|
|
425
|
+
// Late SDK callbacks must not mutate an unmounted form or a new
|
|
426
|
+
// instance that now owns the same transport names.
|
|
427
|
+
if (lifecycle.isActive?.() === false) return;
|
|
428
|
+
lifecycle.onTokenFieldNames?.(names);
|
|
414
429
|
names.forEach((name) => {
|
|
415
430
|
ensureCaptchaValueInput(root, name, {
|
|
416
431
|
value,
|
|
@@ -419,6 +434,7 @@ export function createCaptchaHostServices<TProvider extends Record<string, unkno
|
|
|
419
434
|
});
|
|
420
435
|
},
|
|
421
436
|
clear: (names = options.transport.tokenFieldNames, root = tokenRoot) => {
|
|
437
|
+
if (lifecycle.isActive?.() === false) return;
|
|
422
438
|
clearCaptchaValues(root, names);
|
|
423
439
|
},
|
|
424
440
|
wait: (timeoutMs = options.transport.waitForValueMs, names = options.transport.tokenFieldNames, root = tokenRoot) => {
|
|
@@ -109,7 +109,7 @@ export const recaptchaEnterpriseModule = defineCaptchaModule<RecaptchaEnterprise
|
|
|
109
109
|
// Fallback branch for any future Enterprise widget-like mode.
|
|
110
110
|
enterpriseApi.execute(widget);
|
|
111
111
|
}
|
|
112
|
-
});
|
|
112
|
+
}, true);
|
|
113
113
|
|
|
114
114
|
const hasToken = await services.tokens.wait(CAPTCHA_EXECUTE_WAIT_FOR_VALUE_MS);
|
|
115
115
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadExternalScript } from '#utils/scripts';
|
|
2
|
+
import { waitFor } from '#utils/async';
|
|
2
3
|
import { CAPTCHA_PROVIDER_LOAD_TIMEOUT_MS } from '#modules/captchas/constants';
|
|
3
4
|
import { getScriptAttributes } from '#modules/captchas/utils';
|
|
4
5
|
|
|
@@ -59,20 +60,37 @@ export async function loadRecaptchaGlobal(
|
|
|
59
60
|
src.searchParams.set('badge', options.badge.trim());
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
// Both SDKs share `grecaptcha`, but loading one does not load the other.
|
|
64
|
+
// A ready() stub can also exist before either SDK has attached its methods.
|
|
65
|
+
const getLoadedApi = (): RecaptchaGlobal | null => {
|
|
66
|
+
const api = (window as Window & { grecaptcha?: RecaptchaGlobal }).grecaptcha;
|
|
67
|
+
const target = enterprise ? api?.enterprise : api;
|
|
68
|
+
|
|
69
|
+
return api && target && typeof target.ready === 'function' && typeof target.execute === 'function'
|
|
70
|
+
&& typeof target.render === 'function' ? api : null;
|
|
71
|
+
};
|
|
72
|
+
const existing = getLoadedApi();
|
|
73
|
+
|
|
74
|
+
if (existing) {
|
|
75
|
+
return existing;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await loadExternalScript({
|
|
63
79
|
id: enterprise ? 'FORMIE_RECAPTCHA_ENTERPRISE_SCRIPT' : 'FORMIE_RECAPTCHA_SCRIPT',
|
|
64
80
|
src: src.toString(),
|
|
65
81
|
async,
|
|
66
82
|
defer,
|
|
67
|
-
timeoutMs: CAPTCHA_PROVIDER_LOAD_TIMEOUT_MS,
|
|
68
83
|
});
|
|
84
|
+
|
|
85
|
+
return waitFor(getLoadedApi, { timeoutMs: CAPTCHA_PROVIDER_LOAD_TIMEOUT_MS });
|
|
69
86
|
}
|
|
70
87
|
|
|
71
|
-
export function whenRecaptchaReady(api: RecaptchaGlobal, callback: () => void | Promise<void
|
|
72
|
-
const readyApi = api.enterprise
|
|
88
|
+
export function whenRecaptchaReady(api: RecaptchaGlobal, callback: () => void | Promise<void>, enterprise = false): Promise<void> {
|
|
89
|
+
const readyApi = enterprise ? api.enterprise : api;
|
|
73
90
|
|
|
74
91
|
return new Promise((resolve, reject) => {
|
|
75
92
|
try {
|
|
93
|
+
if (!readyApi) throw new Error('The reCAPTCHA API is unavailable.');
|
|
76
94
|
readyApi.ready(() => {
|
|
77
95
|
Promise.resolve(callback()).then(resolve).catch(reject);
|
|
78
96
|
});
|
|
@@ -17,21 +17,25 @@ export const recaptchaV3Module = defineCaptchaModule<RecaptchaProviderOptions, R
|
|
|
17
17
|
defaultPlaceholderSelector: '[data-recaptcha-placeholder]',
|
|
18
18
|
defaultTokenFieldNames: ['g-recaptcha-response'],
|
|
19
19
|
load: ({ options }) => {
|
|
20
|
-
|
|
21
|
-
// explicit widget render, so the loader needs the site key up front.
|
|
22
|
-
return loadRecaptchaGlobal(options.provider, false, options.provider.siteKey || undefined);
|
|
20
|
+
return loadRecaptchaGlobal(options.provider);
|
|
23
21
|
},
|
|
24
|
-
mount: ({ api, provider }) => {
|
|
25
|
-
return new Promise((resolve) => {
|
|
26
|
-
api.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
mount: ({ api, container, provider, services }) => {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
// An existing api.js may have loaded without render=<our site key>.
|
|
25
|
+
// Register this form explicitly, then execute its returned client ID.
|
|
26
|
+
void whenRecaptchaReady(api, async() => {
|
|
27
|
+
resolve(api.render(container, {
|
|
28
|
+
sitekey: provider.siteKey || '',
|
|
29
|
+
size: 'invisible',
|
|
30
|
+
badge: provider.badge || 'bottomright',
|
|
31
|
+
theme: provider.theme || 'light',
|
|
32
|
+
'expired-callback': () => services.tokens.clear(),
|
|
33
|
+
'error-callback': () => services.tokens.clear(),
|
|
34
|
+
}));
|
|
35
|
+
}).catch(reject);
|
|
32
36
|
});
|
|
33
37
|
},
|
|
34
|
-
challenge: async({ api, provider, placeholder, services, stageCtx }) => {
|
|
38
|
+
challenge: async({ api, widget, provider, placeholder, services, stageCtx }) => {
|
|
35
39
|
// If a token already exists we can skip execution, which matters for
|
|
36
40
|
// re-renders or multi-step flows where the same challenge was already
|
|
37
41
|
// completed very recently.
|
|
@@ -42,7 +46,7 @@ export const recaptchaV3Module = defineCaptchaModule<RecaptchaProviderOptions, R
|
|
|
42
46
|
// v3 returns its token directly from `execute()`, unlike widget-based
|
|
43
47
|
// captchas that signal completion later via callbacks.
|
|
44
48
|
await whenRecaptchaReady(api, async() => {
|
|
45
|
-
const token = await api.execute(
|
|
49
|
+
const token = await api.execute(widget, { action: provider.action || 'submit' });
|
|
46
50
|
|
|
47
51
|
if (typeof token === 'string' && token.trim() !== '') {
|
|
48
52
|
services.tokens.write(token.trim());
|
|
@@ -59,7 +63,12 @@ export const recaptchaV3Module = defineCaptchaModule<RecaptchaProviderOptions, R
|
|
|
59
63
|
stageCtx.abort(message);
|
|
60
64
|
}
|
|
61
65
|
},
|
|
62
|
-
|
|
66
|
+
reset: ({ api, widget, services }) => {
|
|
67
|
+
api.reset(widget);
|
|
68
|
+
services.tokens.clear();
|
|
69
|
+
},
|
|
70
|
+
unmount: ({ api, widget, services }) => {
|
|
71
|
+
api.reset(widget);
|
|
63
72
|
services.tokens.clear();
|
|
64
73
|
},
|
|
65
74
|
});
|