PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | src/Shared/api/pluginsActivation.js +128 -34 3.1.33.2.1 View file →
@@ -1,46 +1,113 @@
1 1 import apiFetch from '@wordpress/api-fetch';
2 2 import { addQueryArgs } from '@wordpress/url';
3 3
4 -const getRecaptchaToken = (action, siteKey) =>
5 - new Promise((resolve, reject) => {
4 +let recaptchaReady;
5 +const loadRecaptcha = () => {
6 + recaptchaReady ??= new Promise((resolve, reject) => {
7 + const ready = () => window.grecaptcha.enterprise.ready(resolve);
8 + if (window.grecaptcha?.enterprise) {
9 + ready();
10 + return;
11 + }
12 +
6 13 const existing = document.querySelector(
7 - `script[src*="recaptcha/enterprise"]`,
14 + 'script[src*="recaptcha/enterprise"]',
8 15 );
9 - const load = () =>
10 - window.grecaptcha.enterprise.ready(async () => {
11 - try {
12 - resolve(
13 - await window.grecaptcha.enterprise.execute(siteKey, { action }),
14 - );
15 - } catch (error) {
16 - reject(error);
17 - }
18 - });
19 -
20 16 if (existing) {
21 - load();
17 + existing.addEventListener('load', ready);
22 18 return;
23 19 }
24 20
25 21 const script = document.createElement('script');
26 - script.src = `https://www.google.com/recaptcha/enterprise.js?render=${siteKey}`;
22 + script.src =
23 + 'https://www.google.com/recaptcha/enterprise.js?render=explicit';
27 24 script.async = true;
28 - script.onload = load;
25 + script.onload = ready;
26 + script.onerror = () => {
27 + // A cached rejection would block every retry.
28 + recaptchaReady = undefined;
29 + reject(new Error('Failed to load the reCAPTCHA script'));
30 + };
29 31 document.head.appendChild(script);
30 32 });
33 + return recaptchaReady;
34 +};
31 35
32 -const createAccount = async ({
33 - slug,
36 +// enterprise.js can't load twice, and execute() needs a rendered site key —
37 +// one widget per key.
38 +const recaptchaWidgets = new Map();
39 +const renderedKeys = new Set();
40 +
41 +const renderWidget = async (siteKey) => {
42 + await loadRecaptcha();
43 +
44 + const container = document.createElement('div');
45 + document.body.appendChild(container);
46 + const widget = window.grecaptcha.enterprise.render(container, {
47 + sitekey: siteKey,
48 + size: 'invisible',
49 + });
50 + renderedKeys.add(siteKey);
51 +
52 + return widget;
53 +};
54 +
55 +// Keeps the script load and the widget render off the click's deadline.
56 +export const prewarmRecaptcha = (siteKey) => {
57 + if (!recaptchaWidgets.has(siteKey)) {
58 + const widget = renderWidget(siteKey);
59 + widget.catch(() => recaptchaWidgets.delete(siteKey));
60 + recaptchaWidgets.set(siteKey, widget);
61 + }
62 + return recaptchaWidgets.get(siteKey);
63 +};
64 +
65 +const getRecaptchaToken = async (action, siteKey, timings = {}) => {
66 + if (!siteKey) {
67 + throw new Error(`No reCAPTCHA site key for the ${action} action`);
68 + }
69 +
70 + timings.captchaWasWarm = renderedKeys.has(siteKey);
71 + const start = Date.now();
72 +
73 + try {
74 + const widget = await prewarmRecaptcha(siteKey);
75 +
76 + // Without await, finally runs before execute settles and records ~0ms.
77 + return await window.grecaptcha.enterprise.execute(widget, { action });
78 + } finally {
79 + timings.captchaTimeInMs = Date.now() - start;
80 + }
81 +};
82 +
83 +// api-fetch throws the parsed body and drops the Response, so parse:false is the only way to keep the status.
84 +const post = async (options) => {
85 + try {
86 + const response = await apiFetch({
87 + ...options,
88 + method: 'POST',
89 + parse: false,
90 + });
91 + return await response.json().catch(() => undefined);
92 + } catch (error) {
93 + if (typeof error?.json !== 'function') throw error;
94 +
95 + const body = await error.json().catch(() => ({ code: 'invalid_json' }));
96 + throw { ...body, httpStatus: error.status };
97 + }
98 +};
99 +
100 +const createAccount = ({
101 + endpoint,
34 102 email,
35 103 marketingConsent,
36 104 termsAgreed,
37 105 signal,
38 106 scriptData,
39 -}) => {
40 - await apiFetch({
41 - path: `extendify/v1/${slug}/create-account`,
42 - method: 'POST',
107 +}) =>
108 + post({
109 + path: endpoint,
43 110 data: {
44 111 email,
45 112 marketingConsent,
46 113 termsAgreed,
@@ -47,39 +114,40 @@
47 114 ...scriptData,
48 115 },
49 116 signal,
50 117 });
51 -};
52 118
53 119 /*
54 120 * Plugin entries shape:
55 - * createAccountCallback: (data) => Promise<void> — performs the account creation request
56 - * idempotent: boolean (default true) — false skips retries; use when re-sending the same request could cause errors
121 + * createAccountCallback: (data) => Promise<body> — performs the account creation request
122 + * data.endpoint: the route PHP registered — requesting and recording must not drift
123 + * data.timings: out-param — write the captcha timings here; they survive a throw
57 124 */
58 125 export const pluginsActivation = {
59 126 simplybook: {
60 - idempotent: false,
61 127 createAccountCallback: async ({
62 128 scriptData,
129 + endpoint,
63 130 email,
64 131 marketingConsent,
65 132 termsAgreed,
66 133 signal,
134 + timings,
67 135 }) => {
68 136 const captchaToken = await getRecaptchaToken(
69 137 scriptData?.recaptchaAction,
70 138 scriptData?.recaptchaSiteKey,
139 + timings,
71 140 );
72 141
73 142 // Hit the endpoint via ?rest_route= so the request URL contains "simplybook" —
74 143 // SimplyBook only registers its onboarding routes when it does, else they 404.
75 144 const url = addQueryArgs(`${window.extSharedData.homeUrl}/`, {
76 - rest_route: '/extendify/v1/simplybook/create-account',
145 + rest_route: `/${endpoint}`,
77 146 });
78 147
79 - await apiFetch({
148 + return post({
80 149 url,
81 - method: 'POST',
82 150 data: {
83 151 email,
84 152 marketingConsent,
85 153 termsAgreed,
@@ -89,12 +157,38 @@
89 157 });
90 158 },
91 159 },
92 160 'translatepress-multilingual': {
93 - createAccountCallback: (data) =>
94 - createAccount({ slug: 'translatepress-multilingual', ...data }),
161 + createAccountCallback: createAccount,
95 162 },
96 163 imagify: {
97 - createAccountCallback: (data) =>
98 - createAccount({ slug: 'imagify', ...data }),
164 + createAccountCallback: createAccount,
165 + },
166 + metricool: {
167 + createAccountCallback: async ({
168 + scriptData,
169 + endpoint,
170 + email,
171 + marketingConsent,
172 + termsAgreed,
173 + signal,
174 + timings,
175 + }) => {
176 + const captchaToken = await getRecaptchaToken(
177 + scriptData?.recaptchaAction,
178 + scriptData?.recaptchaSiteKey,
179 + timings,
180 + );
181 +
182 + return post({
183 + path: endpoint,
184 + data: {
185 + email,
186 + marketingConsent,
187 + termsAgreed,
188 + captcha_token: captchaToken,
189 + },
190 + signal,
191 + });
192 + },
99 193 },
100 194 };