PluginProbe
Extendify / 3.2.3
Extendify v3.2.3
3.2.3 3.2.2 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 All 129 releases
← All changes | src/Shared/components/ProductAccountActivation/ProductAccountActivation.jsx +97 -161 3.1.2 → 3.2.3 View file →
@@ -1,119 +1,34 @@
1 1 import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react';
2 +import { prewarmRecaptcha } from '@shared/api/pluginsActivation';
2 3 import { useEffect, useState } from '@wordpress/element';
3 4 import { isEmail } from '@wordpress/url';
4 -import { pluginsActivation } from '../../api/pluginsActivation';
5 -import { Loading } from './Loading';
5 +import { accountContext } from './accountContext';
6 +import { createAccount } from './createAccount';
7 +import { partitionPlugins } from './partitionPlugins';
6 8 import { SetupComplete } from './SetupComplete';
7 9 import { SetupPlugins } from './SetupPlugins';
8 10 import {
11 + ACCOUNT_STATUS,
9 12 ACTIVATION_STATUS,
10 13 usePluginsActivation,
11 14 } from './usePluginsActivation';
12 15
13 -async function createAccount(plugin, data) {
14 - if (!plugin?.idempotent) {
15 - const signal = AbortSignal.timeout(10000);
16 - const attemptStart = Date.now();
17 -
18 - try {
19 - await plugin.createAccountCallback({ ...data, signal });
20 -
21 - return {
22 - requestTimeInMs: [Date.now() - attemptStart],
23 - retries: 0,
24 - errors: [],
25 - };
26 - } catch (error) {
27 - const err = new Error('Single attempt failed');
28 -
29 - err.requestTimeInMs = [Date.now() - attemptStart];
30 - err.retries = 0;
31 - err.errors = error?.message ? [error.message] : [];
32 -
33 - throw err;
34 - }
35 - }
36 -
37 - return createAccountWithRetry(plugin, data);
38 -}
39 -
40 -/**
41 - * Attempts account creation with retry logic within a 10s window.
42 - * Retries immediately on timeout (5s), or after 2.5s on other failures.
43 - */
44 -async function createAccountWithRetry(
45 - plugin,
46 - { email, marketingConsent, termsAgreed, scriptData },
47 -) {
48 - const windowMs = 10000;
49 - const perAttemptMs = 5000;
50 - const backoffMs = 2500;
51 - const maxRetries = 5;
52 -
53 - const windowStart = Date.now();
54 - const requestTimeInMs = [];
55 - const errors = [];
56 - let retries = 0;
57 -
58 - while (Date.now() - windowStart < windowMs && retries < maxRetries) {
59 - const attemptStart = Date.now();
60 - const signal = AbortSignal.timeout(perAttemptMs);
61 -
62 - try {
63 - await plugin.createAccountCallback({
64 - email,
65 - marketingConsent,
66 - termsAgreed,
67 - scriptData,
68 - signal,
69 - });
70 - requestTimeInMs.push(Date.now() - attemptStart);
71 - return { requestTimeInMs, retries, errors };
72 - } catch (error) {
73 - requestTimeInMs.push(Date.now() - attemptStart);
74 - if (error?.message) errors.push(error.message);
75 -
76 - const isTimeout = signal.aborted;
77 - const remainingMs = windowMs - (Date.now() - windowStart);
78 -
79 - if (remainingMs <= 0) break;
80 -
81 - retries++;
82 -
83 - if (!isTimeout && remainingMs >= backoffMs) {
84 - await new Promise((resolve) => setTimeout(resolve, backoffMs));
85 - }
86 - }
87 - }
88 -
89 - const err = new Error(`Retry window of ${windowMs}ms exceeded`);
90 - err.requestTimeInMs = requestTimeInMs;
91 - err.retries = retries;
92 - err.errors = errors;
93 - throw err;
94 -}
95 -
96 16 export const ProductAccountActivation = () => {
97 17 const [isOpen, setIsOpen] = useState(true);
98 - const [isLoading, setIsLoading] = useState(false);
99 - const [isFinished, setIsFinished] = useState(false);
100 - const [plugins, setPlugins] = useState(
101 - (window.extSharedData?.showProductActivation ?? [])
102 - .map((pluginData) => ({
103 - ...pluginData,
104 - selected: true,
105 - createAccountCallback:
106 - pluginsActivation[pluginData.slug]?.createAccountCallback ?? null,
107 - idempotent: pluginsActivation[pluginData.slug]?.idempotent ?? true,
108 - }))
109 - .filter((plugin) => plugin.createAccountCallback),
18 + const [submitted, setSubmitted] = useState(null);
19 + const { offered, ineligible } = partitionPlugins(
20 + window.extSharedData?.showProductActivation,
110 21 );
22 + const [plugins, setPlugins] = useState(offered);
111 23
112 24 const [email, setEmail] = useState(window.extSharedData?.userEmail ?? '');
113 25 const [marketingConsent, setMarketingConsent] = useState(false);
114 26 const [termsAgreed, setTermsAgreed] = useState(false);
115 - const { scriptData, activatePlugins } = usePluginsActivation(plugins);
27 + const { scriptData, activatePlugins } = usePluginsActivation(
28 + plugins,
29 + ineligible,
30 + );
116 31
117 32 useEffect(() => {
118 33 const style = document.createElement('style');
119 34 style.textContent = '.grecaptcha-badge { visibility: hidden; }';
@@ -119,8 +34,16 @@
119 34 style.textContent = '.grecaptcha-badge { visibility: hidden; }';
120 35 document.head.appendChild(style);
121 36 }, []);
122 37
38 + useEffect(() => {
39 + // Site keys come from PHP, so the prewarm needs no SWR wait.
40 + for (const plugin of offered) {
41 + const siteKey = plugin.scriptData?.recaptchaSiteKey;
42 + if (siteKey) prewarmRecaptcha(siteKey).catch(() => {});
43 + }
44 + }, []);
45 +
123 46 const handleClose = () => {
124 47 activatePlugins({
125 48 status: ACTIVATION_STATUS.skipped,
126 49 });
@@ -126,89 +49,102 @@
126 49 });
127 50 setIsOpen(false);
128 51 };
129 52
53 + const trackStatus = (slug, request) => {
54 + const record = (status) =>
55 + setSubmitted((current) =>
56 + current.map((plugin) =>
57 + plugin.slug === slug ? { ...plugin, status } : plugin,
58 + ),
59 + );
60 +
61 + request.then(
62 + () => record(ACCOUNT_STATUS.success),
63 + () => record(ACCOUNT_STATUS.error),
64 + );
65 +
66 + return request;
67 + };
68 +
130 69 const handleCreateAccounts = async () => {
131 70 if (!isEmail(email)) return;
132 71
133 - setIsLoading(true);
72 + const selectedPlugins = plugins.filter((plugin) => plugin.selected);
134 73
135 - const selectedPlugins = plugins?.filter((plugin) => plugin.selected) ?? [];
74 + setSubmitted(
75 + selectedPlugins.map((plugin) => ({
76 + ...plugin,
77 + status: ACCOUNT_STATUS.pending,
78 + })),
79 + );
136 80
137 - const results = await Promise.allSettled(
81 + const settling = Promise.allSettled(
138 82 selectedPlugins.map((plugin) =>
139 - createAccount(plugin, {
140 - email,
141 - marketingConsent,
142 - termsAgreed,
143 - scriptData: scriptData?.[plugin.slug],
144 - }),
83 + trackStatus(
84 + plugin.slug,
85 + createAccount(plugin, {
86 + email,
87 + marketingConsent,
88 + termsAgreed,
89 + scriptData: { ...scriptData?.[plugin.slug], ...plugin.scriptData },
90 + }),
91 + ),
145 92 ),
146 93 );
147 94
148 - const context = Object.fromEntries(
149 - selectedPlugins.map((plugin, index) => {
150 - const result = results[index];
151 - const { requestTimeInMs, retries, errors } =
152 - result.status === 'fulfilled' ? result.value : result.reason;
153 - const entry = {
154 - status: result.status === 'fulfilled' ? 'success' : 'error',
155 - requestTimeInMs,
156 - endpoint: `extendify/v1/${plugin.slug}/create-account`,
157 - retries,
158 - ...(errors.length > 0 && { errors }),
159 - };
160 - return [plugin.slug, entry];
161 - }),
162 - );
95 + // A record stuck on pending is a user who left before the answer.
96 + await activatePlugins({
97 + status: ACTIVATION_STATUS.completed,
98 + context: accountContext(selectedPlugins),
99 + });
163 100
101 + const results = await settling;
102 +
164 103 await activatePlugins({
165 104 status: ACTIVATION_STATUS.completed,
166 - context,
105 + context: accountContext(selectedPlugins, results),
167 106 });
168 -
169 - setIsFinished(true);
170 - setIsLoading(false);
171 107 };
172 108
173 109 return (
174 - plugins?.length && (
175 - <Dialog
176 - open={isOpen}
177 - onClose={() => {}}
178 - className="relative z-high extendify-shared"
179 - >
180 - <DialogBackdrop
181 - transition
182 - className="fixed inset-0 bg-black/30 transition-opacity data-closed:opacity-0"
183 - />
184 -
185 - <div className="z-10 fixed inset-0 flex w-screen items-center justify-center p-4 [body:has(#extendify-agent-chat)_&]:ml-96 [body:has(#extendify-agent-chat)_&]:w-[calc(100%-24rem)]">
186 - <DialogPanel
110 + plugins.length > 0 && (
111 + <Dialog open={isOpen} onClose={() => {}} className="extendify-shared">
112 + {/* Utilities on the scope-class element itself never match the prefixed CSS. */}
113 + <div className="relative z-high">
114 + <DialogBackdrop
187 115 transition
188 - className="relative w-full max-w-208 bg-white rounded-lg shadow-xl transition-all data-closed:opacity-0 data-closed:scale-95"
189 - >
190 - {!isFinished && !isLoading && (
191 - <SetupPlugins
192 - plugins={plugins}
193 - setPlugins={setPlugins}
194 - handleCreateAccounts={handleCreateAccounts}
195 - email={email}
196 - setEmail={setEmail}
197 - handleClose={handleClose}
198 - marketingConsent={marketingConsent}
199 - setMarketingConsent={setMarketingConsent}
200 - termsAgreed={termsAgreed}
201 - setTermsAgreed={setTermsAgreed}
202 - />
203 - )}
116 + className="fixed inset-0 bg-black/30 transition-opacity data-closed:opacity-0"
117 + />
204 118
205 - {!isFinished && isLoading && <Loading />}
206 -
207 - {isFinished && (
208 - <SetupComplete handleClose={() => setIsOpen(false)} />
209 - )}
210 - </DialogPanel>
119 + <div className="z-10 fixed top-0 left-0 right-0 bottom-(--extendify-notification-bar-height,0px) flex items-center justify-center p-4 [body:has(#extendify-agent-chat)_&]:ml-96 [body:has(#extendify-agent-chat)_&]:w-[calc(100%-24rem)]">
120 + <DialogPanel
121 + transition
122 + className="relative w-full max-w-208 max-h-full overflow-hidden flex flex-col bg-white rounded-lg shadow-xl transition-all data-closed:opacity-0 data-closed:scale-95"
123 + >
124 + <div className="overflow-y-auto">
125 + {submitted ? (
126 + <SetupComplete
127 + plugins={submitted}
128 + handleClose={() => setIsOpen(false)}
129 + />
130 + ) : (
131 + <SetupPlugins
132 + plugins={plugins}
133 + setPlugins={setPlugins}
134 + handleCreateAccounts={handleCreateAccounts}
135 + email={email}
136 + setEmail={setEmail}
137 + handleClose={handleClose}
138 + marketingConsent={marketingConsent}
139 + setMarketingConsent={setMarketingConsent}
140 + termsAgreed={termsAgreed}
141 + setTermsAgreed={setTermsAgreed}
142 + />
143 + )}
144 + </div>
145 + </DialogPanel>
146 + </div>
211 147 </div>
212 148 </Dialog>
213 149 )
214 150 );