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
extendify / src / Shared / api / pluginsActivation.js

pluginsActivation.js in Extendify 3.2.1, at src/Shared/api/pluginsActivation.js

195 lines 4.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import apiFetch from '@wordpress/api-fetch';
2 import { addQueryArgs } from '@wordpress/url';
3
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
13 const existing = document.querySelector(
14 'script[src*="recaptcha/enterprise"]',
15 );
16 if (existing) {
17 existing.addEventListener('load', ready);
18 return;
19 }
20
21 const script = document.createElement('script');
22 script.src =
23 'https://www.google.com/recaptcha/enterprise.js?render=explicit';
24 script.async = true;
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 };
31 document.head.appendChild(script);
32 });
33 return recaptchaReady;
34 };
35
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,
102 email,
103 marketingConsent,
104 termsAgreed,
105 signal,
106 scriptData,
107 }) =>
108 post({
109 path: endpoint,
110 data: {
111 email,
112 marketingConsent,
113 termsAgreed,
114 ...scriptData,
115 },
116 signal,
117 });
118
119 /*
120 * Plugin entries shape:
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
124 */
125 export const pluginsActivation = {
126 simplybook: {
127 createAccountCallback: async ({
128 scriptData,
129 endpoint,
130 email,
131 marketingConsent,
132 termsAgreed,
133 signal,
134 timings,
135 }) => {
136 const captchaToken = await getRecaptchaToken(
137 scriptData?.recaptchaAction,
138 scriptData?.recaptchaSiteKey,
139 timings,
140 );
141
142 // Hit the endpoint via ?rest_route= so the request URL contains "simplybook" —
143 // SimplyBook only registers its onboarding routes when it does, else they 404.
144 const url = addQueryArgs(`${window.extSharedData.homeUrl}/`, {
145 rest_route: `/${endpoint}`,
146 });
147
148 return post({
149 url,
150 data: {
151 email,
152 marketingConsent,
153 termsAgreed,
154 captcha_token: captchaToken,
155 },
156 signal,
157 });
158 },
159 },
160 'translatepress-multilingual': {
161 createAccountCallback: createAccount,
162 },
163 imagify: {
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 },
193 },
194 };
195