@omnizap-system/omnizap 2.6.2 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +24 -0
- package/app/config/index.js +4 -0
- package/app/configParts/adminIdentity.js +29 -0
- package/app/configParts/baileysConfig.js +116 -0
- package/app/configParts/groupUtils.js +221 -0
- package/app/configParts/loggerConfig.js +185 -0
- package/app/configParts/messagePersistenceService.js +169 -7
- package/app/configParts/sessionConfig.js +85 -0
- package/app/connection/baileysCompatibility.test.js +9 -0
- package/app/connection/baileysDbAuthState.js +205 -9
- package/app/connection/baileysLibsignalPatch.js +210 -0
- package/app/connection/groupOwnerWriteStateResolver.js +53 -21
- package/app/connection/socketController.js +95 -25
- package/app/connection/socketController.multiSession.test.js +20 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +17 -3
- package/app/controllers/messageProcessingPipeline.js +2 -0
- package/app/controllers/messageProcessingPipeline.test.js +15 -13
- package/app/services/multiSession/assignmentBalancerService.js +1 -6
- package/app/services/multiSession/groupOwnershipRepository.js +9 -44
- package/app/services/multiSession/groupOwnershipService.js +9 -90
- package/app/services/multiSession/groupOwnershipService.test.js +12 -4
- package/app/services/multiSession/sessionRegistryService.js +6 -60
- package/app/utils/antiLink/antiLinkModule.js +54 -24
- package/docs/security/omnizap-static-security-headers.conf +3 -3
- package/package.json +3 -2
- package/public/comandos/commands-catalog.json +1 -1
- package/public/css/payments-react.css +478 -0
- package/public/js/apps/homeReactApp.js +2 -2
- package/public/js/apps/paymentsCancelReactApp.js +45 -0
- package/public/js/apps/paymentsReactApp.js +399 -0
- package/public/js/apps/paymentsSuccessReactApp.js +148 -0
- package/public/pages/pagamentos-cancelado.html +21 -0
- package/public/pages/pagamentos-sucesso.html +21 -0
- package/public/pages/pagamentos.html +30 -0
- package/scripts/deploy.sh +3 -0
- package/scripts/new-whatsapp-session.sh +247 -0
- package/server/controllers/admin/systemAdminController.js +4 -17
- package/server/controllers/payments/paymentsController.js +731 -0
- package/server/controllers/system/systemController.js +4 -30
- package/server/email/emailAutomationRuntime.js +36 -1
- package/server/email/emailAutomationService.js +42 -1
- package/server/email/emailTemplateService.js +137 -31
- package/server/http/httpRequestUtils.js +18 -14
- package/server/middleware/securityHeaders.js +15 -2
- package/server/routes/indexRouter.js +27 -7
- package/server/routes/payments/paymentsRouter.js +47 -0
- package/server/routes/static/staticPageRouter.js +3 -0
- package/vite.config.mjs +3 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import htm from 'htm';
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(React.createElement);
|
|
6
|
+
|
|
7
|
+
const GOOGLE_AUTH_CACHE_KEY = 'omnizap_google_web_auth_cache_v1';
|
|
8
|
+
const GOOGLE_AUTH_CACHE_MAX_STALE_MS = 8 * 24 * 60 * 60 * 1000;
|
|
9
|
+
const DEFAULT_HOME_BOOTSTRAP_ENDPOINT = '/api/home-bootstrap';
|
|
10
|
+
const DEFAULT_LOGIN_PATH = '/login/';
|
|
11
|
+
const DEFAULT_PAYMENTS_API_BASE_PATH = '/api/payments';
|
|
12
|
+
const DEFAULT_PLAN_NAME = 'Plano Premium';
|
|
13
|
+
const DEFAULT_PLAN_PRICE = 'Assinatura recorrente';
|
|
14
|
+
|
|
15
|
+
const normalizeDigits = (value) =>
|
|
16
|
+
String(value || '')
|
|
17
|
+
.replace(/\D+/g, '')
|
|
18
|
+
.slice(0, 20);
|
|
19
|
+
|
|
20
|
+
const normalizeGoogleAuthState = (value) => {
|
|
21
|
+
const user = value?.user && typeof value.user === 'object' ? value.user : null;
|
|
22
|
+
const sub = String(user?.sub || '').trim();
|
|
23
|
+
if (!sub) return null;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
user: {
|
|
27
|
+
sub,
|
|
28
|
+
email: String(user?.email || '').trim(),
|
|
29
|
+
name: String(user?.name || '').trim(),
|
|
30
|
+
picture: String(user?.picture || '').trim(),
|
|
31
|
+
},
|
|
32
|
+
ownerPhone: normalizeDigits(value?.ownerPhone || ''),
|
|
33
|
+
ownerJid: String(value?.ownerJid || '').trim(),
|
|
34
|
+
expiresAt: String(value?.expiresAt || '').trim(),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const readGoogleAuthCache = () => {
|
|
39
|
+
try {
|
|
40
|
+
const raw = localStorage.getItem(GOOGLE_AUTH_CACHE_KEY);
|
|
41
|
+
if (!raw) return null;
|
|
42
|
+
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
const savedAt = Number(parsed?.savedAt || 0);
|
|
45
|
+
if (savedAt && Date.now() - savedAt > GOOGLE_AUTH_CACHE_MAX_STALE_MS) {
|
|
46
|
+
localStorage.removeItem(GOOGLE_AUTH_CACHE_KEY);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const normalized = normalizeGoogleAuthState(parsed?.auth || null);
|
|
51
|
+
if (!normalized?.user?.sub) {
|
|
52
|
+
localStorage.removeItem(GOOGLE_AUTH_CACHE_KEY);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (normalized.expiresAt) {
|
|
57
|
+
const expiresAt = Number(new Date(normalized.expiresAt));
|
|
58
|
+
if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
|
|
59
|
+
localStorage.removeItem(GOOGLE_AUTH_CACHE_KEY);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return normalized;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const writeGoogleAuthCache = (authState) => {
|
|
71
|
+
try {
|
|
72
|
+
const normalized = normalizeGoogleAuthState(authState);
|
|
73
|
+
if (!normalized?.user?.sub) return;
|
|
74
|
+
|
|
75
|
+
localStorage.setItem(
|
|
76
|
+
GOOGLE_AUTH_CACHE_KEY,
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
auth: normalized,
|
|
79
|
+
savedAt: Date.now(),
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore storage errors
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const clearGoogleAuthCache = () => {
|
|
88
|
+
try {
|
|
89
|
+
localStorage.removeItem(GOOGLE_AUTH_CACHE_KEY);
|
|
90
|
+
} catch {
|
|
91
|
+
// ignore storage errors
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const resolveOwnerPhone = ({ ownerPhone = '', ownerJid = '' } = {}) => {
|
|
96
|
+
const fromPhone = normalizeDigits(ownerPhone);
|
|
97
|
+
if (fromPhone.length >= 10) return fromPhone;
|
|
98
|
+
|
|
99
|
+
return normalizeDigits(String(ownerJid || '').split('@')[0]);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const buildLoginRedirectUrl = (loginPath) => {
|
|
103
|
+
const nextPath = `${window.location.pathname || '/pagamentos/'}${window.location.search || ''}`;
|
|
104
|
+
const loginUrl = new URL(loginPath || DEFAULT_LOGIN_PATH, window.location.origin);
|
|
105
|
+
loginUrl.searchParams.set('next', nextPath);
|
|
106
|
+
return `${loginUrl.pathname}${loginUrl.search}`;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const resolveConfig = (rootElement) => {
|
|
110
|
+
const dataset = rootElement?.dataset || {};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
loginPath: String(dataset.loginPath || DEFAULT_LOGIN_PATH).trim() || DEFAULT_LOGIN_PATH,
|
|
114
|
+
homeBootstrapPath: String(dataset.homeBootstrapPath || DEFAULT_HOME_BOOTSTRAP_ENDPOINT).trim() || DEFAULT_HOME_BOOTSTRAP_ENDPOINT,
|
|
115
|
+
paymentsApiBasePath: String(dataset.paymentsApiBasePath || DEFAULT_PAYMENTS_API_BASE_PATH).trim() || DEFAULT_PAYMENTS_API_BASE_PATH,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const PaymentsReactApp = ({ config }) => {
|
|
120
|
+
const [statusMessage, setStatusMessage] = useState('');
|
|
121
|
+
const [statusType, setStatusType] = useState('');
|
|
122
|
+
const [planName, setPlanName] = useState(DEFAULT_PLAN_NAME);
|
|
123
|
+
const [planPriceLabel, setPlanPriceLabel] = useState(DEFAULT_PLAN_PRICE);
|
|
124
|
+
const [paymentsEnabled, setPaymentsEnabled] = useState(true);
|
|
125
|
+
const [authenticated, setAuthenticated] = useState(false);
|
|
126
|
+
const [loadingCheckout, setLoadingCheckout] = useState(false);
|
|
127
|
+
|
|
128
|
+
const [formValues, setFormValues] = useState({
|
|
129
|
+
name: '',
|
|
130
|
+
email: '',
|
|
131
|
+
whatsapp: '',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const [lockedFields, setLockedFields] = useState({
|
|
135
|
+
name: false,
|
|
136
|
+
email: false,
|
|
137
|
+
whatsapp: false,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const setStatus = useCallback((message, type = '') => {
|
|
141
|
+
setStatusMessage(String(message || ''));
|
|
142
|
+
setStatusType(String(type || ''));
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
const fillFormWithUserData = useCallback(({ name = '', email = '', ownerPhone = '', ownerJid = '' } = {}) => {
|
|
146
|
+
const normalizedName = String(name || '').trim();
|
|
147
|
+
const normalizedEmail = String(email || '')
|
|
148
|
+
.trim()
|
|
149
|
+
.toLowerCase();
|
|
150
|
+
const normalizedPhone = resolveOwnerPhone({ ownerPhone, ownerJid });
|
|
151
|
+
|
|
152
|
+
setFormValues((previous) => ({
|
|
153
|
+
...previous,
|
|
154
|
+
name: normalizedName || previous.name,
|
|
155
|
+
email: normalizedEmail || previous.email,
|
|
156
|
+
whatsapp: normalizedPhone || previous.whatsapp,
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
setLockedFields({
|
|
160
|
+
name: Boolean(normalizedName),
|
|
161
|
+
email: Boolean(normalizedEmail),
|
|
162
|
+
whatsapp: Boolean(normalizedPhone),
|
|
163
|
+
});
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const redirectToLogin = useCallback(() => {
|
|
167
|
+
window.location.replace(buildLoginRedirectUrl(config.loginPath));
|
|
168
|
+
}, [config.loginPath]);
|
|
169
|
+
|
|
170
|
+
const loadAuthenticatedUser = useCallback(async () => {
|
|
171
|
+
const cachedAuth = readGoogleAuthCache();
|
|
172
|
+
if (cachedAuth?.user?.sub) {
|
|
173
|
+
fillFormWithUserData({
|
|
174
|
+
name: cachedAuth.user?.name,
|
|
175
|
+
email: cachedAuth.user?.email,
|
|
176
|
+
ownerPhone: cachedAuth.ownerPhone,
|
|
177
|
+
ownerJid: cachedAuth.ownerJid,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let response = null;
|
|
182
|
+
try {
|
|
183
|
+
response = await fetch(config.homeBootstrapPath, {
|
|
184
|
+
method: 'GET',
|
|
185
|
+
headers: { Accept: 'application/json' },
|
|
186
|
+
credentials: 'include',
|
|
187
|
+
});
|
|
188
|
+
} catch {
|
|
189
|
+
if (cachedAuth?.user?.sub) {
|
|
190
|
+
setAuthenticated(true);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
redirectToLogin();
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const payload = await response.json().catch(() => ({}));
|
|
198
|
+
const session = payload?.data?.session || null;
|
|
199
|
+
const isAuthenticated = Boolean(response.ok && session?.authenticated && session?.user?.sub);
|
|
200
|
+
|
|
201
|
+
if (!isAuthenticated) {
|
|
202
|
+
clearGoogleAuthCache();
|
|
203
|
+
redirectToLogin();
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const authState = {
|
|
208
|
+
user: {
|
|
209
|
+
sub: String(session?.user?.sub || ''),
|
|
210
|
+
email: String(session?.user?.email || ''),
|
|
211
|
+
name: String(session?.user?.name || ''),
|
|
212
|
+
picture: String(session?.user?.picture || ''),
|
|
213
|
+
},
|
|
214
|
+
ownerPhone: String(session?.owner_phone || ''),
|
|
215
|
+
ownerJid: String(session?.owner_jid || ''),
|
|
216
|
+
expiresAt: String(session?.expires_at || ''),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
writeGoogleAuthCache(authState);
|
|
220
|
+
fillFormWithUserData(authState);
|
|
221
|
+
setAuthenticated(true);
|
|
222
|
+
return true;
|
|
223
|
+
}, [config.homeBootstrapPath, fillFormWithUserData, redirectToLogin]);
|
|
224
|
+
|
|
225
|
+
const loadPublicConfig = useCallback(async () => {
|
|
226
|
+
try {
|
|
227
|
+
const response = await fetch(`${config.paymentsApiBasePath}/config`, {
|
|
228
|
+
method: 'GET',
|
|
229
|
+
headers: { Accept: 'application/json' },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const payload = await response.json().catch(() => ({}));
|
|
233
|
+
if (!response.ok || !payload || payload.ok !== true) return true;
|
|
234
|
+
|
|
235
|
+
if (payload.plan_name) setPlanName(String(payload.plan_name));
|
|
236
|
+
if (payload.plan_price_label) setPlanPriceLabel(String(payload.plan_price_label));
|
|
237
|
+
|
|
238
|
+
if (payload.enabled === false) {
|
|
239
|
+
setPaymentsEnabled(false);
|
|
240
|
+
setStatus('Pagamentos estao temporariamente indisponiveis.', 'error');
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return true;
|
|
245
|
+
} catch {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}, [config.paymentsApiBasePath, setStatus]);
|
|
249
|
+
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
let active = true;
|
|
252
|
+
|
|
253
|
+
const bootstrap = async () => {
|
|
254
|
+
setStatus('Validando sua sessao...', 'success');
|
|
255
|
+
const [configEnabled] = await Promise.all([loadPublicConfig(), loadAuthenticatedUser()]);
|
|
256
|
+
if (!active) return;
|
|
257
|
+
if (configEnabled) {
|
|
258
|
+
setStatus('', '');
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
void bootstrap();
|
|
263
|
+
|
|
264
|
+
return () => {
|
|
265
|
+
active = false;
|
|
266
|
+
};
|
|
267
|
+
}, [loadAuthenticatedUser, loadPublicConfig, setStatus]);
|
|
268
|
+
|
|
269
|
+
const onInputChange = useCallback(
|
|
270
|
+
(field) => (event) => {
|
|
271
|
+
const value = String(event?.target?.value || '');
|
|
272
|
+
setFormValues((previous) => ({ ...previous, [field]: value }));
|
|
273
|
+
},
|
|
274
|
+
[],
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const onSubmit = useCallback(
|
|
278
|
+
async (event) => {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
setStatus('', '');
|
|
281
|
+
|
|
282
|
+
const whatsapp = String(formValues.whatsapp || '').trim();
|
|
283
|
+
const email = String(formValues.email || '').trim();
|
|
284
|
+
const name = String(formValues.name || '').trim();
|
|
285
|
+
|
|
286
|
+
if (!whatsapp) {
|
|
287
|
+
setStatus('Informe seu WhatsApp para continuar.', 'error');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
setLoadingCheckout(true);
|
|
292
|
+
setStatus('Preparando checkout seguro...', 'success');
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetch(`${config.paymentsApiBasePath}/checkout-session`, {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
headers: {
|
|
298
|
+
'Content-Type': 'application/json',
|
|
299
|
+
Accept: 'application/json',
|
|
300
|
+
},
|
|
301
|
+
credentials: 'include',
|
|
302
|
+
body: JSON.stringify({ whatsapp, email, name }),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const payload = await response.json().catch(() => ({}));
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
throw new Error(payload?.error || 'Nao foi possivel iniciar o checkout.');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!payload?.checkout_url) {
|
|
311
|
+
throw new Error('Checkout criado sem URL de redirecionamento.');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
window.location.href = payload.checkout_url;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
setStatus(error?.message || 'Falha ao iniciar checkout.', 'error');
|
|
317
|
+
setLoadingCheckout(false);
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
[config.paymentsApiBasePath, formValues.email, formValues.name, formValues.whatsapp, setStatus],
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const submitDisabled = useMemo(() => loadingCheckout || !authenticated || !paymentsEnabled, [authenticated, loadingCheckout, paymentsEnabled]);
|
|
324
|
+
|
|
325
|
+
const statusClassName = useMemo(() => {
|
|
326
|
+
const list = ['payments-status'];
|
|
327
|
+
if (statusType) list.push(statusType);
|
|
328
|
+
return list.join(' ');
|
|
329
|
+
}, [statusType]);
|
|
330
|
+
|
|
331
|
+
return html`
|
|
332
|
+
<main className="payments-page">
|
|
333
|
+
<section className="payments-hero">
|
|
334
|
+
<span className="payments-eyebrow">Pagamento Automatico Stripe</span>
|
|
335
|
+
<h1>Ative o Premium em minutos</h1>
|
|
336
|
+
<p>Preencha seu WhatsApp e siga para o checkout seguro. Assim que o pagamento confirmar no webhook, seu acesso Premium e liberado automaticamente.</p>
|
|
337
|
+
</section>
|
|
338
|
+
|
|
339
|
+
<section className="payments-layout">
|
|
340
|
+
<article className="payments-card payments-checkout">
|
|
341
|
+
<form onSubmit=${onSubmit} novalidate=${true}>
|
|
342
|
+
<div className="payments-row">
|
|
343
|
+
<label htmlFor="checkout-name">Nome</label>
|
|
344
|
+
<input id="checkout-name" name="name" type="text" maxlength="120" autocomplete="name" placeholder="Seu nome" value=${formValues.name} onChange=${onInputChange('name')} readonly=${lockedFields.name} title=${lockedFields.name ? 'Campo preenchido automaticamente pela sua sessao.' : undefined} />
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div className="payments-row">
|
|
348
|
+
<label htmlFor="checkout-email">E-mail</label>
|
|
349
|
+
<input id="checkout-email" name="email" type="email" maxlength="255" autocomplete="email" placeholder="voce@empresa.com" value=${formValues.email} onChange=${onInputChange('email')} readonly=${lockedFields.email} title=${lockedFields.email ? 'Campo preenchido automaticamente pela sua sessao.' : undefined} />
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div className="payments-row">
|
|
353
|
+
<label htmlFor="checkout-whatsapp">WhatsApp para liberar Premium</label>
|
|
354
|
+
<input id="checkout-whatsapp" name="whatsapp" type="text" required=${true} autocomplete="tel" placeholder="5511999999999" value=${formValues.whatsapp} onChange=${onInputChange('whatsapp')} readonly=${lockedFields.whatsapp} title=${lockedFields.whatsapp ? 'Campo preenchido automaticamente pela sua sessao.' : undefined} />
|
|
355
|
+
<p className="payments-hint">
|
|
356
|
+
Use com DDI e DDD. Exemplo:
|
|
357
|
+
<code>5511999999999</code>
|
|
358
|
+
ou
|
|
359
|
+
<code>5511999999999@s.whatsapp.net</code>
|
|
360
|
+
</p>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<div className="payments-actions">
|
|
364
|
+
<button className="payments-button payments-button-primary" type="submit" disabled=${submitDisabled}>${loadingCheckout ? 'Criando checkout...' : 'Ir para checkout'}</button>
|
|
365
|
+
<a className="payments-button payments-button-secondary" href="/">Voltar para home</a>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<p className=${statusClassName} aria-live="polite">${statusMessage}</p>
|
|
369
|
+
</form>
|
|
370
|
+
</article>
|
|
371
|
+
|
|
372
|
+
<aside className="payments-card payments-aside">
|
|
373
|
+
<p className="payments-plan-badge">Plano ativo no checkout</p>
|
|
374
|
+
<h2 className="payments-plan-title">${planName}</h2>
|
|
375
|
+
<p className="payments-plan-price">${planPriceLabel}</p>
|
|
376
|
+
|
|
377
|
+
<ul className="payments-feature-list">
|
|
378
|
+
<li>Ativacao automatica apos pagamento confirmado no Stripe.</li>
|
|
379
|
+
<li>Fluxo seguro com assinatura de webhook validada no backend.</li>
|
|
380
|
+
<li>Checkout hospedado no Stripe para reduzir risco e fraude.</li>
|
|
381
|
+
</ul>
|
|
382
|
+
|
|
383
|
+
<p className="payments-small">
|
|
384
|
+
Ao continuar, voce concorda com os
|
|
385
|
+
<a href="/termos-de-uso/">Termos de Uso</a>
|
|
386
|
+
e
|
|
387
|
+
<a href="/politica-de-privacidade/">Politica de Privacidade</a>.
|
|
388
|
+
</p>
|
|
389
|
+
</aside>
|
|
390
|
+
</section>
|
|
391
|
+
</main>
|
|
392
|
+
`;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const rootElement = document.getElementById('payments-react-root');
|
|
396
|
+
if (rootElement) {
|
|
397
|
+
const config = resolveConfig(rootElement);
|
|
398
|
+
createRoot(rootElement).render(html`<${PaymentsReactApp} config=${config} />`);
|
|
399
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import htm from 'htm';
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(React.createElement);
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PAYMENTS_API_BASE_PATH = '/api/payments';
|
|
8
|
+
const DEFAULT_PANEL_PATH = '/user/';
|
|
9
|
+
const DEFAULT_PAYMENTS_PATH = '/pagamentos/';
|
|
10
|
+
|
|
11
|
+
const normalizeRoutePath = (value, fallback) => {
|
|
12
|
+
const raw = String(value || '').trim();
|
|
13
|
+
if (!raw) return fallback;
|
|
14
|
+
if (!raw.startsWith('/')) return fallback;
|
|
15
|
+
if (/^\/\//.test(raw)) return fallback;
|
|
16
|
+
return raw;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const normalizeSessionId = (value) =>
|
|
20
|
+
String(value || '')
|
|
21
|
+
.trim()
|
|
22
|
+
.slice(0, 255);
|
|
23
|
+
|
|
24
|
+
const resolveConfig = (rootElement) => {
|
|
25
|
+
const dataset = rootElement?.dataset || {};
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
paymentsApiBasePath: String(dataset.paymentsApiBasePath || DEFAULT_PAYMENTS_API_BASE_PATH).trim() || DEFAULT_PAYMENTS_API_BASE_PATH,
|
|
29
|
+
panelPath: normalizeRoutePath(dataset.panelPath, DEFAULT_PANEL_PATH),
|
|
30
|
+
paymentsPath: normalizeRoutePath(dataset.paymentsPath, DEFAULT_PAYMENTS_PATH),
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const PaymentsSuccessReactApp = ({ config }) => {
|
|
35
|
+
const sessionId = useMemo(() => {
|
|
36
|
+
const params = new URLSearchParams(window.location.search);
|
|
37
|
+
return normalizeSessionId(params.get('session_id'));
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const [statusMessage, setStatusMessage] = useState('Consultando o status no Stripe...');
|
|
41
|
+
const [statusType, setStatusType] = useState('');
|
|
42
|
+
const [metaMessage, setMetaMessage] = useState('');
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let active = true;
|
|
46
|
+
|
|
47
|
+
const setStatus = (message, type = '') => {
|
|
48
|
+
if (!active) return;
|
|
49
|
+
setStatusMessage(String(message || ''));
|
|
50
|
+
setStatusType(String(type || ''));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const setMeta = (message) => {
|
|
54
|
+
if (!active) return;
|
|
55
|
+
setMetaMessage(String(message || ''));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (!sessionId) {
|
|
59
|
+
setStatus('Pagamento concluido. Se o Premium ainda nao apareceu, aguarde alguns segundos e atualize o painel.', 'pending');
|
|
60
|
+
setMeta('Dica: volte ao painel e confirme se o plano Premium ja foi liberado.');
|
|
61
|
+
return () => {
|
|
62
|
+
active = false;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const loadStatus = async () => {
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(`${config.paymentsApiBasePath}/finalize-session`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
Accept: 'application/json',
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
session_id: sessionId,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const payload = await response.json().catch(() => ({}));
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(payload?.error || 'Nao foi possivel consultar o status da sessao.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const status = String(payload?.session?.status || '').toLowerCase();
|
|
85
|
+
const paymentStatus = String(payload?.session?.payment_status || '').toLowerCase();
|
|
86
|
+
const customerEmail = String(payload?.session?.customer_email || '').trim();
|
|
87
|
+
const ownerJid = String(payload?.owner_jid || payload?.session?.owner_jid || '').trim();
|
|
88
|
+
const action = String(payload?.action || '').toLowerCase();
|
|
89
|
+
const reason = String(payload?.reason || '').toLowerCase();
|
|
90
|
+
|
|
91
|
+
if (action === 'premium_activated') {
|
|
92
|
+
setStatus('Pagamento confirmado e Premium ativado com sucesso.');
|
|
93
|
+
} else if (status === 'complete' && (paymentStatus === 'paid' || paymentStatus === 'no_payment_required') && reason === 'owner_jid_missing') {
|
|
94
|
+
setStatus('Pagamento confirmado, mas faltou o WhatsApp para liberar Premium. Fale com o suporte.', 'error');
|
|
95
|
+
} else if (status === 'complete' && (paymentStatus === 'paid' || paymentStatus === 'no_payment_required')) {
|
|
96
|
+
setStatus('Pagamento confirmado. Estamos finalizando a liberacao do Premium.', 'pending');
|
|
97
|
+
} else {
|
|
98
|
+
setStatus('Sessao concluida, aguardando confirmacao final de pagamento no Stripe.', 'pending');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const metaParts = [`Sessao: ${sessionId}`];
|
|
102
|
+
if (customerEmail) metaParts.push(`Cliente: ${customerEmail}`);
|
|
103
|
+
if (ownerJid) metaParts.push(`WhatsApp: ${ownerJid}`);
|
|
104
|
+
if (status) metaParts.push(`Status checkout: ${status}`);
|
|
105
|
+
if (paymentStatus) metaParts.push(`Status pagamento: ${paymentStatus}`);
|
|
106
|
+
if (action) metaParts.push(`Acao: ${action}`);
|
|
107
|
+
if (reason && reason !== 'ok') metaParts.push(`Motivo: ${reason}`);
|
|
108
|
+
setMeta(metaParts.join(' | '));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
setStatus(error?.message || 'Falha ao consultar status do pagamento.', 'error');
|
|
111
|
+
setMeta(`Sessao: ${sessionId}`);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
void loadStatus();
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
active = false;
|
|
119
|
+
};
|
|
120
|
+
}, [config.paymentsApiBasePath, sessionId]);
|
|
121
|
+
|
|
122
|
+
const statusClassName = useMemo(() => {
|
|
123
|
+
const list = ['payments-result-status'];
|
|
124
|
+
if (statusType) list.push(statusType);
|
|
125
|
+
return list.join(' ');
|
|
126
|
+
}, [statusType]);
|
|
127
|
+
|
|
128
|
+
return html`
|
|
129
|
+
<main className="payments-result-card">
|
|
130
|
+
<h1 className="payments-result-title">Pagamento recebido</h1>
|
|
131
|
+
<p className="payments-result-subtitle">Estamos finalizando a ativacao do seu Premium.</p>
|
|
132
|
+
|
|
133
|
+
<p className=${statusClassName} aria-live="polite">${statusMessage}</p>
|
|
134
|
+
<p className="payments-result-meta">${metaMessage}</p>
|
|
135
|
+
|
|
136
|
+
<div className="payments-result-actions">
|
|
137
|
+
<a className="payments-result-button primary" href=${config.panelPath}>Ir para painel</a>
|
|
138
|
+
<a className="payments-result-button" href=${config.paymentsPath}>Fazer outro pagamento</a>
|
|
139
|
+
</div>
|
|
140
|
+
</main>
|
|
141
|
+
`;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const rootElement = document.getElementById('payments-success-react-root');
|
|
145
|
+
if (rootElement) {
|
|
146
|
+
const config = resolveConfig(rootElement);
|
|
147
|
+
createRoot(rootElement).render(html`<${PaymentsSuccessReactApp} config=${config} />`);
|
|
148
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="pt-BR" data-theme="night">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Pagamento cancelado | OmniZap</title>
|
|
7
|
+
<meta name="description" content="Seu pagamento foi cancelado. Voce pode tentar novamente quando quiser." />
|
|
8
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
9
|
+
<meta name="theme-color" content="#2a1318" />
|
|
10
|
+
<link rel="icon" href="/favicon.ico" sizes="any" />
|
|
11
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
12
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
13
|
+
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&family=Manrope:wght@400;600&display=swap" rel="stylesheet" />
|
|
14
|
+
<link rel="stylesheet" href="/assets/css/payments-react.css" />
|
|
15
|
+
</head>
|
|
16
|
+
<body class="payments-body payments-body-cancel">
|
|
17
|
+
<div id="payments-cancel-react-root" data-payments-path="/pagamentos/" data-home-path="/"></div>
|
|
18
|
+
|
|
19
|
+
<script type="module" src="/assets/js/payments-cancel-react.bundle.js"></script>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="pt-BR" data-theme="night">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Pagamento concluido | OmniZap</title>
|
|
7
|
+
<meta name="description" content="Status do seu pagamento OmniZap Premium." />
|
|
8
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
9
|
+
<meta name="theme-color" content="#0e1f1f" />
|
|
10
|
+
<link rel="icon" href="/favicon.ico" sizes="any" />
|
|
11
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
12
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
13
|
+
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@500;700;800&family=Manrope:wght@400;600;700&display=swap" rel="stylesheet" />
|
|
14
|
+
<link rel="stylesheet" href="/assets/css/payments-react.css" />
|
|
15
|
+
</head>
|
|
16
|
+
<body class="payments-body payments-body-success">
|
|
17
|
+
<div id="payments-success-react-root" data-payments-api-base-path="/api/payments" data-panel-path="/user/" data-payments-path="/pagamentos/"></div>
|
|
18
|
+
|
|
19
|
+
<script type="module" src="/assets/js/payments-success-react.bundle.js"></script>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="pt-BR" data-theme="night">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Pagamentos | OmniZap Premium</title>
|
|
7
|
+
<meta name="description" content="Ative seu plano Premium do OmniZap com pagamento automatico via Stripe Checkout." />
|
|
8
|
+
<meta name="robots" content="index, follow" />
|
|
9
|
+
<meta name="theme-color" content="#0f172a" />
|
|
10
|
+
<link rel="canonical" href="https://omnizap.shop/pagamentos/" />
|
|
11
|
+
<link rel="icon" href="/favicon.ico" sizes="any" />
|
|
12
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
13
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
14
|
+
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700;800&family=Manrope:wght@400;600;700&display=swap" rel="stylesheet" />
|
|
15
|
+
<link rel="stylesheet" href="/assets/css/payments-react.css" />
|
|
16
|
+
</head>
|
|
17
|
+
<body class="payments-body payments-body-main">
|
|
18
|
+
<div id="payments-react-root" data-login-path="/login/" data-home-bootstrap-path="/api/home-bootstrap" data-payments-api-base-path="/api/payments"></div>
|
|
19
|
+
|
|
20
|
+
<noscript>
|
|
21
|
+
<main style="max-width: 680px; margin: 100px auto; padding: 20px; text-align: center; color: #e7f0ff">
|
|
22
|
+
<h1>Pagamento OmniZap Premium</h1>
|
|
23
|
+
<p>Ative o JavaScript para continuar com o checkout seguro.</p>
|
|
24
|
+
<a href="/login/" style="color: #4ade80">Ir para login</a>
|
|
25
|
+
</main>
|
|
26
|
+
</noscript>
|
|
27
|
+
|
|
28
|
+
<script type="module" src="/assets/js/payments-react.bundle.js"></script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
package/scripts/deploy.sh
CHANGED
|
@@ -732,6 +732,9 @@ prepare_legacy_static_entrypoints() {
|
|
|
732
732
|
copy_page_alias "dpa.html" "dpa/index.html"
|
|
733
733
|
copy_page_alias "licenca.html" "licenca/index.html"
|
|
734
734
|
copy_page_alias "notice-and-takedown.html" "notice-and-takedown/index.html"
|
|
735
|
+
copy_page_alias "pagamentos.html" "pagamentos/index.html"
|
|
736
|
+
copy_page_alias "pagamentos-sucesso.html" "pagamentos/sucesso/index.html"
|
|
737
|
+
copy_page_alias "pagamentos-cancelado.html" "pagamentos/cancelado/index.html"
|
|
735
738
|
copy_page_alias "politica-de-privacidade.html" "politica-de-privacidade/index.html"
|
|
736
739
|
copy_page_alias "suboperadores.html" "suboperadores/index.html"
|
|
737
740
|
copy_page_alias "termos-de-uso.html" "termos-de-uso/index.html"
|