PluginProbe
Extendify / 3.1.6
Extendify v3.1.6
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.1.6, at src/Shared/api/pluginsActivation.js

183 lines 4.4 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 getRecaptchaToken = async (action, siteKey, timings = {}) => {
40 if (!siteKey) {
41 throw new Error(`No reCAPTCHA site key for the ${action} action`);
42 }
43
44 const start = Date.now();
45
46 try {
47 await loadRecaptcha();
48
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 // Without await, finally runs before execute settles and records ~0ms.
62 return await window.grecaptcha.enterprise.execute(
63 recaptchaWidgets.get(siteKey),
64 { action },
65 );
66 } finally {
67 timings.captchaTimeInMs = Date.now() - start;
68 }
69 };
70
71 // api-fetch throws the parsed body and drops the Response, so parse:false is the only way to keep the status.
72 const post = async (options) => {
73 try {
74 await apiFetch({ ...options, method: 'POST', parse: false });
75 } catch (error) {
76 if (typeof error?.json !== 'function') throw error;
77
78 const body = await error.json().catch(() => ({ code: 'invalid_json' }));
79 throw { ...body, httpStatus: error.status };
80 }
81 };
82
83 const createAccount = ({
84 slug,
85 email,
86 marketingConsent,
87 termsAgreed,
88 signal,
89 scriptData,
90 }) =>
91 post({
92 path: `extendify/v1/${slug}/create-account`,
93 data: {
94 email,
95 marketingConsent,
96 termsAgreed,
97 ...scriptData,
98 },
99 signal,
100 });
101
102 /*
103 * 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
107 */
108 export const pluginsActivation = {
109 simplybook: {
110 idempotent: false,
111 createAccountCallback: async ({
112 scriptData,
113 email,
114 marketingConsent,
115 termsAgreed,
116 signal,
117 timings,
118 }) => {
119 const captchaToken = await getRecaptchaToken(
120 scriptData?.recaptchaAction,
121 scriptData?.recaptchaSiteKey,
122 timings,
123 );
124
125 // Hit the endpoint via ?rest_route= so the request URL contains "simplybook" —
126 // SimplyBook only registers its onboarding routes when it does, else they 404.
127 const url = addQueryArgs(`${window.extSharedData.homeUrl}/`, {
128 rest_route: '/extendify/v1/simplybook/create-account',
129 });
130
131 await post({
132 url,
133 data: {
134 email,
135 marketingConsent,
136 termsAgreed,
137 captcha_token: captchaToken,
138 },
139 signal,
140 });
141 },
142 },
143 'translatepress-multilingual': {
144 idempotent: false,
145 createAccountCallback: (data) =>
146 createAccount({ slug: 'translatepress-multilingual', ...data }),
147 },
148 imagify: {
149 idempotent: false,
150 createAccountCallback: (data) =>
151 createAccount({ slug: 'imagify', ...data }),
152 },
153 metricool: {
154 idempotent: false,
155 createAccountCallback: async ({
156 scriptData,
157 email,
158 marketingConsent,
159 termsAgreed,
160 signal,
161 timings,
162 }) => {
163 const captchaToken = await getRecaptchaToken(
164 scriptData?.recaptchaAction,
165 scriptData?.recaptchaSiteKey,
166 timings,
167 );
168
169 // The "/v1" segment is what makes Metricool register its logout route.
170 await post({
171 path: 'extendify/v1/metricool/v1/create-account',
172 data: {
173 email,
174 marketingConsent,
175 termsAgreed,
176 captcha_token: captchaToken,
177 },
178 signal,
179 });
180 },
181 },
182 };
183