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 +48 -36 3.1.6 → trunk 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,