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 +62 -42 3.1.5 → trunk 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,