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 +62 -42 3.1.53.2.1 View file →
@@ -35,51 +35,79 @@
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 82
71 -const createAccount = async ({
72 - slug,
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,
73 102 email,
74 103 marketingConsent,
75 104 termsAgreed,
76 105 signal,
77 106 scriptData,
78 -}) => {
79 - await apiFetch({
80 - path: `extendify/v1/${slug}/create-account`,
81 - method: 'POST',
107 +}) =>
108 + post({
109 + path: endpoint,
82 110 data: {
83 111 email,
84 112 marketingConsent,
85 113 termsAgreed,
@@ -86,21 +114,20 @@
86 114 ...scriptData,
87 115 },
88 116 signal,
89 117 });
90 -};
91 118
92 119 /*
93 120 * Plugin entries shape:
94 - * createAccountCallback: (data) => Promise<void> — performs the account creation request
95 - * idempotent: boolean (default true) — false skips retries; an aborted fetch does not stop the PHP call, so a retry creates a second account
96 - * 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
97 124 */
98 125 export const pluginsActivation = {
99 126 simplybook: {
100 - idempotent: false,
101 127 createAccountCallback: async ({
102 128 scriptData,
129 + endpoint,
103 130 email,
104 131 marketingConsent,
105 132 termsAgreed,
106 133 signal,
@@ -114,14 +141,13 @@
114 141
115 142 // Hit the endpoint via ?rest_route= so the request URL contains "simplybook" —
116 143 // SimplyBook only registers its onboarding routes when it does, else they 404.
117 144 const url = addQueryArgs(`${window.extSharedData.homeUrl}/`, {
118 - rest_route: '/extendify/v1/simplybook/create-account',
145 + rest_route: `/${endpoint}`,
119 146 });
120 147
121 - await apiFetch({
148 + return post({
122 149 url,
123 - method: 'POST',
124 150 data: {
125 151 email,
126 152 marketingConsent,
127 153 termsAgreed,
@@ -131,21 +157,17 @@
131 157 });
132 158 },
133 159 },
134 160 'translatepress-multilingual': {
135 - idempotent: false,
136 - createAccountCallback: (data) =>
137 - createAccount({ slug: 'translatepress-multilingual', ...data }),
161 + createAccountCallback: createAccount,
138 162 },
139 163 imagify: {
140 - idempotent: false,
141 - createAccountCallback: (data) =>
142 - createAccount({ slug: 'imagify', ...data }),
164 + createAccountCallback: createAccount,
143 165 },
144 166 metricool: {
145 - idempotent: false,
146 167 createAccountCallback: async ({
147 168 scriptData,
169 + endpoint,
148 170 email,
149 171 marketingConsent,
150 172 termsAgreed,
151 173 signal,
@@ -156,12 +178,10 @@
156 178 scriptData?.recaptchaSiteKey,
157 179 timings,
158 180 );
159 181
160 - // The "/v1" segment is what makes Metricool register its logout route.
161 - await apiFetch({
162 - path: 'extendify/v1/metricool/v1/create-account',
163 - method: 'POST',
182 + return post({
183 + path: endpoint,
164 184 data: {
165 185 email,
166 186 marketingConsent,
167 187 termsAgreed,