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 +48 -36 3.1.63.2.1 View file →
@@ -35,35 +35,47 @@
35 35
36 36 // enterprise.js can't load twice, and execute() needs a rendered site key —
37 37 // one widget per key.
38 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 +
39 65 const getRecaptchaToken = async (action, siteKey, timings = {}) => {
40 66 if (!siteKey) {
41 67 throw new Error(`No reCAPTCHA site key for the ${action} action`);
42 68 }
43 69
70 + timings.captchaWasWarm = renderedKeys.has(siteKey);
44 71 const start = Date.now();
45 72
46 73 try {
47 - await loadRecaptcha();
74 + const widget = await prewarmRecaptcha(siteKey);
48 75
49 - if (!recaptchaWidgets.has(siteKey)) {
50 - const container = document.createElement('div');
51 - document.body.appendChild(container);
52 - recaptchaWidgets.set(
53 - siteKey,
54 - window.grecaptcha.enterprise.render(container, {
55 - sitekey: siteKey,
56 - size: 'invisible',
57 - }),
58 - );
59 - }
60 -
61 76 // Without await, finally runs before execute settles and records ~0ms.
62 - return await window.grecaptcha.enterprise.execute(
63 - recaptchaWidgets.get(siteKey),
64 - { action },
65 - );
77 + return await window.grecaptcha.enterprise.execute(widget, { action });
66 78 } finally {
67 79 timings.captchaTimeInMs = Date.now() - start;
68 80 }
69 81 };
@@ -70,9 +82,14 @@
70 82
71 83 // api-fetch throws the parsed body and drops the Response, so parse:false is the only way to keep the status.
72 84 const post = async (options) => {
73 85 try {
74 - await apiFetch({ ...options, method: 'POST', parse: false });
86 + const response = await apiFetch({
87 + ...options,
88 + method: 'POST',
89 + parse: false,
90 + });
91 + return await response.json().catch(() => undefined);
75 92 } catch (error) {
76 93 if (typeof error?.json !== 'function') throw error;
77 94
78 95 const body = await error.json().catch(() => ({ code: 'invalid_json' }));
@@ -80,9 +97,9 @@
80 97 }
81 98 };
82 99
83 100 const createAccount = ({
84 - slug,
101 + endpoint,
85 102 email,
86 103 marketingConsent,
87 104 termsAgreed,
88 105 signal,
@@ -88,9 +105,9 @@
88 105 signal,
89 106 scriptData,
90 107 }) =>
91 108 post({
92 - path: `extendify/v1/${slug}/create-account`,
109 + path: endpoint,
93 110 data: {
94 111 email,
95 112 marketingConsent,
96 113 termsAgreed,
@@ -100,17 +117,17 @@
100 117 });
101 118
102 119 /*
103 120 * Plugin entries shape:
104 - * createAccountCallback: (data) => Promise<void> — performs the account creation request
105 - * idempotent: boolean (default true) — false skips retries; an aborted fetch does not stop the PHP call, so a retry creates a second account
106 - * data.timings: out-param — write captchaTimeInMs here; it survives a throw
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
107 124 */
108 125 export const pluginsActivation = {
109 126 simplybook: {
110 - idempotent: false,
111 127 createAccountCallback: async ({
112 128 scriptData,
129 + endpoint,
113 130 email,
114 131 marketingConsent,
115 132 termsAgreed,
116 133 signal,
@@ -124,12 +141,12 @@
124 141
125 142 // Hit the endpoint via ?rest_route= so the request URL contains "simplybook" —
126 143 // SimplyBook only registers its onboarding routes when it does, else they 404.
127 144 const url = addQueryArgs(`${window.extSharedData.homeUrl}/`, {
128 - rest_route: '/extendify/v1/simplybook/create-account',
145 + rest_route: `/${endpoint}`,
129 146 });
130 147
131 - await post({
148 + return post({
132 149 url,
133 150 data: {
134 151 email,
135 152 marketingConsent,
@@ -140,21 +157,17 @@
140 157 });
141 158 },
142 159 },
143 160 'translatepress-multilingual': {
144 - idempotent: false,
145 - createAccountCallback: (data) =>
146 - createAccount({ slug: 'translatepress-multilingual', ...data }),
161 + createAccountCallback: createAccount,
147 162 },
148 163 imagify: {
149 - idempotent: false,
150 - createAccountCallback: (data) =>
151 - createAccount({ slug: 'imagify', ...data }),
164 + createAccountCallback: createAccount,
152 165 },
153 166 metricool: {
154 - idempotent: false,
155 167 createAccountCallback: async ({
156 168 scriptData,
169 + endpoint,
157 170 email,
158 171 marketingConsent,
159 172 termsAgreed,
160 173 signal,
@@ -165,11 +178,10 @@
165 178 scriptData?.recaptchaSiteKey,
166 179 timings,
167 180 );
168 181
169 - // The "/v1" segment is what makes Metricool register its logout route.
170 - await post({
171 - path: 'extendify/v1/metricool/v1/create-account',
182 + return post({
183 + path: endpoint,
172 184 data: {
173 185 email,
174 186 marketingConsent,
175 187 termsAgreed,