PluginProbe
Extendify / trunk
Extendify vtrunk
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.3 → trunk 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 };