@slyxup/ui 0.2.10 → 0.2.11
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/.turbo/turbo-build.log +4 -0
- package/LICENSE +21 -0
- package/README.md +23 -3
- package/dist/components/SignIn/SignIn.d.ts.map +1 -1
- package/dist/components/SignIn/SignIn.js +29 -3
- package/dist/components/SignIn/SignIn.js.map +1 -1
- package/dist/components/SignUp/SignUp.d.ts.map +1 -1
- package/dist/components/SignUp/SignUp.js +10 -2
- package/dist/components/SignUp/SignUp.js.map +1 -1
- package/dist/components/UserProfile/UserProfile.d.ts.map +1 -1
- package/dist/components/UserProfile/UserProfile.js +275 -65
- package/dist/components/UserProfile/UserProfile.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/paddle.d.ts +39 -0
- package/dist/lib/paddle.d.ts.map +1 -0
- package/dist/lib/paddle.js +94 -0
- package/dist/lib/paddle.js.map +1 -0
- package/dist/styles.d.ts +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +10 -0
- package/dist/styles.js.map +1 -1
- package/package.json +10 -10
- package/src/components/SignIn/SignIn.tsx +112 -47
- package/src/components/SignUp/SignUp.tsx +38 -1
- package/src/components/UserProfile/UserProfile.tsx +590 -96
- package/src/index.ts +1 -0
- package/src/lib/paddle.ts +131 -0
- package/src/styles.ts +10 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useAuth, useUser } from '@slyxup/react';
|
|
3
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { initPaddle, openPaddleCheckout } from '../../lib/paddle';
|
|
4
5
|
import { injectStyles } from '../../styles';
|
|
5
6
|
function initials(user) {
|
|
6
7
|
const f = user.firstName?.trim();
|
|
@@ -27,20 +28,33 @@ function deviceLabel(ua) {
|
|
|
27
28
|
? 'macOS'
|
|
28
29
|
: /Android/i.test(ua)
|
|
29
30
|
? 'Android'
|
|
30
|
-
: /iPhone|iPad|
|
|
31
|
+
: /iPhone|iPad|iPod/i.test(ua)
|
|
31
32
|
? 'iOS'
|
|
32
|
-
: /
|
|
33
|
-
? '
|
|
34
|
-
:
|
|
33
|
+
: /CrOS/i.test(ua)
|
|
34
|
+
? 'Chrome OS'
|
|
35
|
+
: /Linux/i.test(ua)
|
|
36
|
+
? 'Linux'
|
|
37
|
+
: 'Unknown OS';
|
|
38
|
+
// Order matters: check Edge before Chrome (Edg/ appears in Chrome UA)
|
|
35
39
|
const browser = /Edg\//i.test(ua)
|
|
36
40
|
? 'Edge'
|
|
37
|
-
: /
|
|
38
|
-
? '
|
|
39
|
-
: /
|
|
40
|
-
? '
|
|
41
|
-
: /
|
|
42
|
-
? '
|
|
43
|
-
:
|
|
41
|
+
: /OPR|Opera/i.test(ua)
|
|
42
|
+
? 'Opera'
|
|
43
|
+
: /Vivaldi/i.test(ua)
|
|
44
|
+
? 'Vivaldi'
|
|
45
|
+
: /Brave/i.test(ua)
|
|
46
|
+
? 'Brave'
|
|
47
|
+
: /Chrome\//i.test(ua) && !/CriOS/i.test(ua)
|
|
48
|
+
? 'Chrome'
|
|
49
|
+
: /Safari\//i.test(ua) && !/Chrome\//i.test(ua)
|
|
50
|
+
? 'Safari'
|
|
51
|
+
: /Firefox\//i.test(ua) || /FxiOS/i.test(ua)
|
|
52
|
+
? 'Firefox'
|
|
53
|
+
: /SamsungBrowser/i.test(ua)
|
|
54
|
+
? 'Samsung Browser'
|
|
55
|
+
: /Mobile/i.test(ua) || /Android/i.test(ua)
|
|
56
|
+
? 'Mobile Browser'
|
|
57
|
+
: 'Browser';
|
|
44
58
|
return `${browser} · ${os}`;
|
|
45
59
|
}
|
|
46
60
|
function formatDate(iso) {
|
|
@@ -72,6 +86,7 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
72
86
|
// ── Profile form state ──
|
|
73
87
|
const [firstName, setFirstName] = useState('');
|
|
74
88
|
const [lastName, setLastName] = useState('');
|
|
89
|
+
const [username, setUsername] = useState('');
|
|
75
90
|
const [avatarUrl, setAvatarUrl] = useState('');
|
|
76
91
|
const [busy, setBusy] = useState(false);
|
|
77
92
|
const [saved, setSaved] = useState(false);
|
|
@@ -89,26 +104,61 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
89
104
|
const [sessionsLoading, setSessionsLoading] = useState(true);
|
|
90
105
|
const [revokingId, setRevokingId] = useState(null);
|
|
91
106
|
const [othersRevoking, setOthersRevoking] = useState(false);
|
|
107
|
+
const [sessionsTotal, setSessionsTotal] = useState(0);
|
|
108
|
+
const [sessionsPage, setSessionsPage] = useState(0);
|
|
109
|
+
const SESSIONS_PER_PAGE = 10;
|
|
92
110
|
// ── Billing state ──
|
|
93
111
|
const [billingLoading, setBillingLoading] = useState(true);
|
|
94
112
|
const [subscription, setSubscription] = useState(null);
|
|
95
113
|
const [invoices, setInvoices] = useState([]);
|
|
96
114
|
const [plans, setPlans] = useState([]);
|
|
97
115
|
const [plansLoading, setPlansLoading] = useState(false);
|
|
116
|
+
// Project whose plans/subscription are shown — used to attribute checkout customData.
|
|
117
|
+
const [projectId, setProjectId] = useState(null);
|
|
98
118
|
const [checkoutId, setCheckoutId] = useState(null);
|
|
119
|
+
const [checkoutDone, setCheckoutDone] = useState(false);
|
|
120
|
+
const [checkoutSuccess, setCheckoutSuccess] = useState(false);
|
|
121
|
+
// Keep the latest subscription available to the async polling loop (avoids
|
|
122
|
+
// stale-closure reads of state inside setTimeout callbacks).
|
|
123
|
+
const subscriptionRef = useRef(subscription);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
subscriptionRef.current = subscription;
|
|
126
|
+
}, [subscription]);
|
|
99
127
|
// ── Danger zone state ──
|
|
100
128
|
const [confirmText, setConfirmText] = useState('');
|
|
101
129
|
const [deleteBusy, setDeleteBusy] = useState(false);
|
|
102
130
|
const [deleteError, setDeleteError] = useState(null);
|
|
103
|
-
//
|
|
104
|
-
|
|
131
|
+
// ── Two-factor (TOTP) state ──
|
|
132
|
+
const [tfaStage, setTfaStage] = useState('idle');
|
|
133
|
+
const [tfaSetup, setTfaSetup] = useState(null);
|
|
134
|
+
const [tfaCode, setTfaCode] = useState('');
|
|
135
|
+
const [tfaRecoveryCodes, setTfaRecoveryCodes] = useState([]);
|
|
136
|
+
const [tfaBusy, setTfaBusy] = useState(false);
|
|
137
|
+
const [tfaError, setTfaError] = useState(null);
|
|
138
|
+
const [tfaVerifyCode, setTfaVerifyCode] = useState('');
|
|
139
|
+
// ── Connected accounts state ──
|
|
140
|
+
const [accounts, setAccounts] = useState([]);
|
|
141
|
+
const [accountsLoading, setAccountsLoading] = useState(false);
|
|
142
|
+
const [accountsError, setAccountsError] = useState(null);
|
|
143
|
+
const [unlinkingId, setUnlinkingId] = useState(null);
|
|
144
|
+
// Sync form fields when user loads/updates. Use individual primitives as
|
|
145
|
+
// deps so the effect fires even when the user object reference stays the
|
|
146
|
+
// same (e.g. after a silent reload that returns identical data).
|
|
147
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional — see comment above
|
|
105
148
|
useEffect(() => {
|
|
106
149
|
if (user) {
|
|
107
150
|
setFirstName(user.firstName ?? '');
|
|
108
151
|
setLastName(user.lastName ?? '');
|
|
152
|
+
setUsername(user.username ?? '');
|
|
109
153
|
setAvatarUrl(user.avatarUrl ?? '');
|
|
110
154
|
}
|
|
111
|
-
}, [
|
|
155
|
+
}, [
|
|
156
|
+
user?.firstName,
|
|
157
|
+
user?.lastName,
|
|
158
|
+
user?.username,
|
|
159
|
+
user?.avatarUrl,
|
|
160
|
+
user?.id,
|
|
161
|
+
]);
|
|
112
162
|
useEffect(() => {
|
|
113
163
|
function onKey(e) {
|
|
114
164
|
if (e.key === 'Escape' && modal)
|
|
@@ -117,14 +167,20 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
117
167
|
document.addEventListener('keydown', onKey);
|
|
118
168
|
return () => document.removeEventListener('keydown', onKey);
|
|
119
169
|
}, [modal, onClose]);
|
|
120
|
-
const loadSessions = useCallback(async () => {
|
|
170
|
+
const loadSessions = useCallback(async (page = 0) => {
|
|
121
171
|
setSessionsLoading(true);
|
|
122
172
|
try {
|
|
123
|
-
const res = await client.sessions.list(
|
|
173
|
+
const res = await client.sessions.list({
|
|
174
|
+
limit: SESSIONS_PER_PAGE,
|
|
175
|
+
offset: page * SESSIONS_PER_PAGE,
|
|
176
|
+
});
|
|
124
177
|
setSessions(res.sessions);
|
|
178
|
+
setSessionsTotal(res.total);
|
|
179
|
+
setSessionsPage(page);
|
|
125
180
|
}
|
|
126
181
|
catch {
|
|
127
182
|
setSessions([]);
|
|
183
|
+
setSessionsTotal(0);
|
|
128
184
|
}
|
|
129
185
|
finally {
|
|
130
186
|
setSessionsLoading(false);
|
|
@@ -136,9 +192,15 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
136
192
|
try {
|
|
137
193
|
const rawApiUrl = client.apiUrl ??
|
|
138
194
|
'https://auth.slyxup.online';
|
|
139
|
-
const billingUrl =
|
|
140
|
-
|
|
141
|
-
|
|
195
|
+
const billingUrl = (() => {
|
|
196
|
+
// Localhost: swap port 8787 → 8788 (auth → billing)
|
|
197
|
+
if (/^https?:\/\/localhost(:\d+)?$/.test(rawApiUrl)) {
|
|
198
|
+
return rawApiUrl.replace(/:(\d+)$/, ':8788');
|
|
199
|
+
}
|
|
200
|
+
return rawApiUrl.replace('auth.slyxup.online', 'billing.slyxup.online');
|
|
201
|
+
})();
|
|
202
|
+
const token = client.getToken?.() ??
|
|
203
|
+
client?._token;
|
|
142
204
|
const headers = {
|
|
143
205
|
'Content-Type': 'application/json',
|
|
144
206
|
};
|
|
@@ -149,8 +211,10 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
149
211
|
?.publishableKey;
|
|
150
212
|
if (pubKey && pubKey !== 'pk_test_missing')
|
|
151
213
|
headers['X-Publishable-Key'] = pubKey;
|
|
152
|
-
// Derive projectId for plans: prefer user.projectId, then try
|
|
214
|
+
// Derive projectId for plans: prefer user.projectId, then try to resolve from publishableKey's project (for examples)
|
|
215
|
+
// For the example apps, the publishableKey is for the example project, not the user's projectId
|
|
153
216
|
const projectId = user?.projectId ?? null;
|
|
217
|
+
setProjectId(projectId);
|
|
154
218
|
// Fetch subscription + invoices (new /v1/billing/* with fallback to legacy /v1/*)
|
|
155
219
|
async function fetchJson(url) {
|
|
156
220
|
const res = await fetch(url, { headers, credentials: 'include' });
|
|
@@ -231,14 +295,22 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
231
295
|
// try next
|
|
232
296
|
}
|
|
233
297
|
}
|
|
234
|
-
// Plans (needs projectId — if missing, try
|
|
298
|
+
// Plans (needs projectId — if missing, try with publishableKey header instead of empty projectId)
|
|
299
|
+
let gotPlans = false;
|
|
235
300
|
const planPaths = [];
|
|
236
|
-
if (projectId)
|
|
301
|
+
if (projectId) {
|
|
237
302
|
planPaths.push(`${billingUrl}/v1/billing/plans?projectId=${projectId}`);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
303
|
+
}
|
|
304
|
+
else if (pubKey && pubKey !== 'pk_test_missing') {
|
|
305
|
+
// No projectId but have publishableKey — let billing resolve project via X-Publishable-Key header.
|
|
306
|
+
// Billing plans route returns [] in test/localhost when projectId is missing, which is fine.
|
|
307
|
+
planPaths.push(`${billingUrl}/v1/billing/plans`);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
// No projectId and no publishableKey — don't make the request, just show empty
|
|
311
|
+
setPlans([]);
|
|
312
|
+
gotPlans = true;
|
|
313
|
+
}
|
|
242
314
|
for (const p of planPaths) {
|
|
243
315
|
try {
|
|
244
316
|
const pj = (await fetchJson(p));
|
|
@@ -266,16 +338,27 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
266
338
|
}, [client, user]);
|
|
267
339
|
useEffect(() => {
|
|
268
340
|
if (tab === 'security')
|
|
269
|
-
void loadSessions();
|
|
341
|
+
void loadSessions(0);
|
|
270
342
|
if (tab === 'billing')
|
|
271
343
|
void loadBilling();
|
|
272
344
|
}, [tab, loadSessions, loadBilling]);
|
|
345
|
+
// React to a completed Paddle checkout: reload billing immediately and show
|
|
346
|
+
// a success confirmation (fallback for when the webhook is fast / polling lags).
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
function onCheckoutCompleted() {
|
|
349
|
+
setCheckoutSuccess(true);
|
|
350
|
+
void loadBilling();
|
|
351
|
+
setTimeout(() => setCheckoutSuccess(false), 6000);
|
|
352
|
+
}
|
|
353
|
+
window.addEventListener('slyxup:checkout-completed', onCheckoutCompleted);
|
|
354
|
+
return () => window.removeEventListener('slyxup:checkout-completed', onCheckoutCompleted);
|
|
355
|
+
}, [loadBilling]);
|
|
273
356
|
async function onProfileSubmit(e) {
|
|
274
357
|
e.preventDefault();
|
|
275
358
|
setBusy(true);
|
|
276
359
|
setSaved(false);
|
|
277
360
|
try {
|
|
278
|
-
await client.users.update({ firstName, lastName, avatarUrl });
|
|
361
|
+
await client.users.update({ firstName, lastName, username, avatarUrl });
|
|
279
362
|
await reload();
|
|
280
363
|
setSaved(true);
|
|
281
364
|
setTimeout(() => setSaved(false), 2500);
|
|
@@ -297,6 +380,95 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
297
380
|
setResending(false);
|
|
298
381
|
}
|
|
299
382
|
}
|
|
383
|
+
const loadAccounts = useCallback(async () => {
|
|
384
|
+
setAccountsLoading(true);
|
|
385
|
+
setAccountsError(null);
|
|
386
|
+
try {
|
|
387
|
+
const res = await client.accounts.list();
|
|
388
|
+
setAccounts(res.accounts);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
setAccounts([]);
|
|
392
|
+
setAccountsError('Could not load connected accounts.');
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
setAccountsLoading(false);
|
|
396
|
+
}
|
|
397
|
+
}, [client]);
|
|
398
|
+
useEffect(() => {
|
|
399
|
+
if (tab === 'security')
|
|
400
|
+
void loadAccounts();
|
|
401
|
+
}, [tab, loadAccounts]);
|
|
402
|
+
async function startTfaSetup() {
|
|
403
|
+
setTfaError(null);
|
|
404
|
+
setTfaBusy(true);
|
|
405
|
+
try {
|
|
406
|
+
const res = await client.twoFactor.setup();
|
|
407
|
+
setTfaSetup(res);
|
|
408
|
+
setTfaStage('setup');
|
|
409
|
+
setTfaCode('');
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
setTfaError(err instanceof Error ? err.message : 'Failed to start setup');
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
setTfaBusy(false);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async function submitTfa(e) {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
if (!tfaSetup)
|
|
421
|
+
return;
|
|
422
|
+
setTfaError(null);
|
|
423
|
+
setTfaBusy(true);
|
|
424
|
+
try {
|
|
425
|
+
const res = await client.twoFactor.enable(tfaSetup.secret, tfaCode.trim());
|
|
426
|
+
setTfaRecoveryCodes(res.recoveryCodes);
|
|
427
|
+
setTfaStage('codes');
|
|
428
|
+
await reload();
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
setTfaError(err instanceof Error ? err.message : 'Invalid code — try again.');
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
setTfaBusy(false);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function submitTfaDisable(e) {
|
|
438
|
+
e.preventDefault();
|
|
439
|
+
setTfaError(null);
|
|
440
|
+
setTfaBusy(true);
|
|
441
|
+
try {
|
|
442
|
+
await client.twoFactor.disable(tfaVerifyCode.trim());
|
|
443
|
+
setTfaVerifyCode('');
|
|
444
|
+
setTfaStage('idle');
|
|
445
|
+
setTfaSetup(null);
|
|
446
|
+
setTfaRecoveryCodes([]);
|
|
447
|
+
await reload();
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
setTfaError(err instanceof Error
|
|
451
|
+
? err.message
|
|
452
|
+
: 'Invalid code — could not disable 2FA.');
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
setTfaBusy(false);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async function onUnlink(accountId, provider) {
|
|
459
|
+
setUnlinkingId(accountId);
|
|
460
|
+
setAccountsError(null);
|
|
461
|
+
try {
|
|
462
|
+
await client.accounts.unlink(accountId, provider);
|
|
463
|
+
setAccounts((prev) => prev.filter((a) => a.id !== accountId));
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
setAccountsError(err instanceof Error ? err.message : 'Could not unlink account.');
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
setUnlinkingId(null);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
300
472
|
async function onPasswordSubmit(e) {
|
|
301
473
|
e.preventDefault();
|
|
302
474
|
setPwError(null);
|
|
@@ -329,7 +501,7 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
329
501
|
setRevokingId(id);
|
|
330
502
|
try {
|
|
331
503
|
await client.sessions.revoke(id);
|
|
332
|
-
await loadSessions();
|
|
504
|
+
await loadSessions(sessionsPage);
|
|
333
505
|
}
|
|
334
506
|
finally {
|
|
335
507
|
setRevokingId(null);
|
|
@@ -339,7 +511,7 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
339
511
|
setOthersRevoking(true);
|
|
340
512
|
try {
|
|
341
513
|
await client.sessions.revokeOthers();
|
|
342
|
-
await loadSessions();
|
|
514
|
+
await loadSessions(0);
|
|
343
515
|
}
|
|
344
516
|
finally {
|
|
345
517
|
setOthersRevoking(false);
|
|
@@ -360,41 +532,51 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
360
532
|
setDeleteBusy(false);
|
|
361
533
|
}
|
|
362
534
|
}
|
|
363
|
-
async function handleCheckout(
|
|
364
|
-
setCheckoutId(
|
|
535
|
+
async function handleCheckout(plan) {
|
|
536
|
+
setCheckoutId(plan.id);
|
|
537
|
+
setCheckoutDone(false);
|
|
365
538
|
try {
|
|
539
|
+
// Always use Paddle.js overlay checkout
|
|
540
|
+
// authApiUrl is available via client apiUrl
|
|
366
541
|
const rawApiUrl = client.apiUrl ??
|
|
367
542
|
'https://auth.slyxup.online';
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
543
|
+
await initPaddle(rawApiUrl);
|
|
544
|
+
// Pass custom data so the billing webhook can attribute the created
|
|
545
|
+
// subscription to this user + project + plan (Paddle copies custom_data
|
|
546
|
+
// from the transaction to the subscription for recurring items).
|
|
547
|
+
const customData = {
|
|
548
|
+
userId: user?.id ?? '',
|
|
549
|
+
planId: plan.id,
|
|
373
550
|
};
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
551
|
+
if (projectId)
|
|
552
|
+
customData.projectId = projectId;
|
|
553
|
+
openPaddleCheckout(plan.paddlePriceId, user?.email, customData);
|
|
554
|
+
setCheckoutDone(true);
|
|
555
|
+
// Poll for subscription updates after checkout (webhook may take a few seconds)
|
|
556
|
+
let attempts = 0;
|
|
557
|
+
const maxAttempts = 10;
|
|
558
|
+
const pollInterval = 3000; // 3 seconds
|
|
559
|
+
const poll = async () => {
|
|
560
|
+
attempts++;
|
|
561
|
+
try {
|
|
562
|
+
await loadBilling();
|
|
563
|
+
if (subscriptionRef.current && attempts < maxAttempts) {
|
|
564
|
+
// Found subscription, stop polling
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
// Ignore polling errors
|
|
570
|
+
}
|
|
571
|
+
if (attempts < maxAttempts) {
|
|
572
|
+
setTimeout(poll, pollInterval);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
// Start polling after a short delay to allow webhook to process
|
|
576
|
+
setTimeout(poll, 2000);
|
|
577
|
+
return;
|
|
395
578
|
}
|
|
396
579
|
catch (err) {
|
|
397
|
-
// Fallback: if billing checkout isn't configured, just log. Parent can handle via window redirect.
|
|
398
580
|
console.error('[SlyxUp] checkout failed', err);
|
|
399
581
|
}
|
|
400
582
|
finally {
|
|
@@ -415,7 +597,7 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
415
597
|
const bodyInner = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "slx-profile-head", children: [_jsx("h2", { className: "slx-profile-title", children: "Account settings" }), modal && (_jsx("button", { type: "button", className: "slx-profile-close", onClick: (e) => {
|
|
416
598
|
e.stopPropagation();
|
|
417
599
|
onClose?.();
|
|
418
|
-
}, "aria-label": "Close account settings", children: "\u2715" }))] }), _jsxs("div", { className: "slx-profile-body", children: [_jsxs("nav", { className: "slx-profile-nav", "aria-label": "Settings sections", children: [_jsxs("button", { type: "button", className: `slx-profile-nav-btn${tab === 'profile' ? ' on' : ''}`, onClick: () => setTab('profile'), "aria-current": tab === 'profile' ? 'page' : undefined, children: [_jsx(ProfileIcon, {}), " Profile"] }), _jsxs("button", { type: "button", className: `slx-profile-nav-btn${tab === 'security' ? ' on' : ''}`, onClick: () => setTab('security'), "aria-current": tab === 'security' ? 'page' : undefined, children: [_jsx(ShieldIcon, {}), " Security"] }), _jsxs("button", { type: "button", className: `slx-profile-nav-btn${tab === 'billing' ? ' on' : ''}`, onClick: () => setTab('billing'), "aria-current": tab === 'billing' ? 'page' : undefined, children: [_jsx(CreditCardIcon, {}), " Billing"] })] }), _jsxs("div", { className: "slx-profile-content", children: [tab === 'profile' && (_jsxs(_Fragment, { children: [_jsxs("section", { className: "slx-profile-sec", children: [_jsxs("div", { className: "slx-avatar-row", children: [_jsx("div", { className: "slx-avatar-lg", "aria-hidden": "true", children: user.avatarUrl ? (_jsx("img", { src: user.avatarUrl, alt: "" })) : (initials(user)) }), _jsxs("div", { style: { minWidth: 0 }, children: [_jsx("p", { className: "slx-row-value", style: { margin: 0, wordBreak: 'break-word' }, children: name }), _jsx("p", { className: "slx-row-label", style: { wordBreak: 'break-all' }, children: user.email })] })] }), saved && (_jsx("p", { className: "slx-error-text", style: { color: 'var(--slx-success)' }, children: "Profile saved." })), _jsxs("form", { onSubmit: onProfileSubmit, children: [_jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-up-first", children: "First name" }), _jsx("input", { id: "slx-up-first", className: "slx-input", type: "text", autoComplete: "given-name", value: firstName, onChange: (e) => setFirstName(e.target.value) })] }), _jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-up-last", children: "Last name" }), _jsx("input", { id: "slx-up-last", className: "slx-input", type: "text", autoComplete: "family-name", value: lastName, onChange: (e) => setLastName(e.target.value) })] }), _jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-up-avatar", children: "Avatar URL" }), _jsx("input", { id: "slx-up-avatar", className: "slx-input", type: "url", placeholder: "https://\u2026", value: avatarUrl, onChange: (e) => setAvatarUrl(e.target.value) }), _jsx("p", { className: "slx-hint", children: "Paste a public image URL for your avatar." })] }), _jsx("button", { className: "slx-btn", type: "submit", disabled: busy, children: busy ? 'Saving…' : 'Save changes' })] })] }), _jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Email" }), _jsxs("div", { className: "slx-row", style: { flexWrap: 'wrap', gap: 8 }, children: [_jsxs("div", { style: { minWidth: 0 }, children: [_jsx("p", { className: "slx-row-value", style: { wordBreak: 'break-all' }, children: user.email }), _jsx("p", { className: "slx-row-label", children: "Primary email" })] }), emailVerified ? (_jsx("span", { className: "slx-badge slx-badge-ok", children: "Verified" })) : (_jsxs("span", { style: {
|
|
600
|
+
}, "aria-label": "Close account settings", children: "\u2715" }))] }), _jsxs("div", { className: "slx-profile-body", children: [_jsxs("nav", { className: "slx-profile-nav", "aria-label": "Settings sections", children: [_jsxs("button", { type: "button", className: `slx-profile-nav-btn${tab === 'profile' ? ' on' : ''}`, onClick: () => setTab('profile'), "aria-current": tab === 'profile' ? 'page' : undefined, children: [_jsx(ProfileIcon, {}), " Profile"] }), _jsxs("button", { type: "button", className: `slx-profile-nav-btn${tab === 'security' ? ' on' : ''}`, onClick: () => setTab('security'), "aria-current": tab === 'security' ? 'page' : undefined, children: [_jsx(ShieldIcon, {}), " Security"] }), _jsxs("button", { type: "button", className: `slx-profile-nav-btn${tab === 'billing' ? ' on' : ''}`, onClick: () => setTab('billing'), "aria-current": tab === 'billing' ? 'page' : undefined, children: [_jsx(CreditCardIcon, {}), " Billing"] })] }), _jsxs("div", { className: "slx-profile-content", children: [tab === 'profile' && (_jsxs(_Fragment, { children: [_jsxs("section", { className: "slx-profile-sec", children: [_jsxs("div", { className: "slx-avatar-row", children: [_jsx("div", { className: "slx-avatar-lg", "aria-hidden": "true", children: user.avatarUrl ? (_jsx("img", { src: user.avatarUrl, alt: "" })) : (initials(user)) }), _jsxs("div", { style: { minWidth: 0 }, children: [_jsx("p", { className: "slx-row-value", style: { margin: 0, wordBreak: 'break-word' }, children: name }), _jsx("p", { className: "slx-row-label", style: { wordBreak: 'break-all' }, children: user.email })] })] }), saved && (_jsx("p", { className: "slx-error-text", style: { color: 'var(--slx-success)' }, children: "Profile saved." })), _jsxs("form", { onSubmit: onProfileSubmit, children: [_jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-up-first", children: "First name" }), _jsx("input", { id: "slx-up-first", className: "slx-input", type: "text", autoComplete: "given-name", value: firstName, onChange: (e) => setFirstName(e.target.value) })] }), _jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-up-last", children: "Last name" }), _jsx("input", { id: "slx-up-last", className: "slx-input", type: "text", autoComplete: "family-name", value: lastName, onChange: (e) => setLastName(e.target.value) })] }), _jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-up-avatar", children: "Avatar URL" }), _jsx("input", { id: "slx-up-avatar", className: "slx-input", type: "url", placeholder: "https://\u2026", value: avatarUrl, onChange: (e) => setAvatarUrl(e.target.value) }), _jsx("p", { className: "slx-hint", children: "Paste a public image URL for your avatar." })] }), _jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-up-username", children: "Username" }), _jsx("input", { id: "slx-up-username", className: "slx-input", type: "text", autoComplete: "username", placeholder: "yourname", value: username, onChange: (e) => setUsername(e.target.value) }), _jsx("p", { className: "slx-hint", children: "Used for password sign-in as an alternative to email. Must be unique within your project." })] }), _jsx("button", { className: "slx-btn", type: "submit", disabled: busy, children: busy ? 'Saving…' : 'Save changes' })] })] }), _jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Email" }), _jsxs("div", { className: "slx-row", style: { flexWrap: 'wrap', gap: 8 }, children: [_jsxs("div", { style: { minWidth: 0 }, children: [_jsx("p", { className: "slx-row-value", style: { wordBreak: 'break-all' }, children: user.email }), _jsx("p", { className: "slx-row-label", children: "Primary email" })] }), emailVerified ? (_jsx("span", { className: "slx-badge slx-badge-ok", children: "Verified" })) : (_jsxs("span", { style: {
|
|
419
601
|
display: 'inline-flex',
|
|
420
602
|
alignItems: 'center',
|
|
421
603
|
gap: 8,
|
|
@@ -428,14 +610,42 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
428
610
|
.filter(Boolean)
|
|
429
611
|
.join(' · ') })] }), !s.isCurrent && (_jsx("button", { type: "button", className: "slx-btn-danger-outline", onClick: () => onRevoke(s.id), disabled: revokingId === s.id, children: revokingId === s.id ? '…' : 'Revoke' }))] }, s.id))), sessions.some((s) => !s.isCurrent) && (_jsx("button", { type: "button", className: "slx-btn-danger-outline", style: { width: '100%', marginTop: 4 }, onClick: onRevokeOthers, disabled: othersRevoking, children: othersRevoking
|
|
430
612
|
? 'Signing out…'
|
|
431
|
-
: 'Sign out other devices' }))
|
|
613
|
+
: 'Sign out other devices' })), sessionsTotal > SESSIONS_PER_PAGE && (_jsxs("div", { className: "slx-pagination", children: [_jsx("button", { type: "button", className: "slx-btn-secondary", disabled: sessionsPage === 0 || sessionsLoading, onClick: () => void loadSessions(sessionsPage - 1), children: "Previous" }), _jsxs("span", { className: "slx-pagination-info", children: [sessionsPage * SESSIONS_PER_PAGE + 1, "\u2013", Math.min((sessionsPage + 1) * SESSIONS_PER_PAGE, sessionsTotal), ' ', "of ", sessionsTotal] }), _jsx("button", { type: "button", className: "slx-btn-secondary", disabled: (sessionsPage + 1) * SESSIONS_PER_PAGE >=
|
|
614
|
+
sessionsTotal || sessionsLoading, onClick: () => void loadSessions(sessionsPage + 1), children: "Next" })] }))] }))] }), _jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Two-factor authentication" }), tfaError && _jsx("p", { className: "slx-error-text", children: tfaError }), !user.twoFactorEnabled && tfaStage === 'idle' && (_jsxs(_Fragment, { children: [_jsx("p", { className: "slx-hint", children: "Add an authenticator app (Google Authenticator, Authy, 1Password, etc.) to protect your account with a time-based one-time password." }), _jsx("button", { type: "button", className: "slx-btn", onClick: () => void startTfaSetup(), disabled: tfaBusy, children: tfaBusy ? 'Starting…' : 'Set up 2FA' })] })), !user.twoFactorEnabled && tfaStage === 'setup' && (_jsxs("form", { onSubmit: submitTfa, children: [tfaSetup && (_jsxs(_Fragment, { children: [_jsx("p", { className: "slx-hint", children: "Scan this QR code with your authenticator app:" }), _jsxs("div", { style: {
|
|
615
|
+
display: 'flex',
|
|
616
|
+
alignItems: 'center',
|
|
617
|
+
gap: 14,
|
|
618
|
+
margin: '8px 0',
|
|
619
|
+
flexWrap: 'wrap',
|
|
620
|
+
}, children: [_jsx("img", { src: `https://api.qrserver.com/v1/create-qr-code/?size=132x132&data=${encodeURIComponent(tfaSetup.provisioningUri)}`, alt: "QR code to scan with your authenticator app", width: 132, height: 132, style: { borderRadius: 6 } }), _jsxs("div", { style: { minWidth: 0 }, children: [_jsx("p", { className: "slx-row-label", children: "Or enter this code manually:" }), _jsx("p", { className: "slx-row-value", style: {
|
|
621
|
+
userSelect: 'all',
|
|
622
|
+
letterSpacing: 2,
|
|
623
|
+
fontFamily: 'monospace',
|
|
624
|
+
}, children: tfaSetup.secret.replace(/(.{4})/g, '$1 ').trim() }), _jsxs("p", { className: "slx-hint", children: ["Account: ", tfaSetup.accountName] })] })] })] })), _jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-tfa-code", children: "Enter the 6-digit code" }), _jsx("input", { id: "slx-tfa-code", className: "slx-input", type: "text", inputMode: "numeric", autoComplete: "one-time-code", maxLength: 6, pattern: "[0-9]*", placeholder: "000000", value: tfaCode, onChange: (e) => setTfaCode(e.target.value.replace(/\D/g, '')), required: true })] }), _jsx("button", { className: "slx-btn", type: "submit", disabled: tfaBusy, children: tfaBusy ? 'Verifying…' : 'Enable 2FA' })] })), !user.twoFactorEnabled && tfaStage === 'codes' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "slx-billing-card", style: { margin: '4px 0 8px' }, children: [_jsx("p", { className: "slx-billing-plan", children: "2FA enabled \u2014 save recovery codes" }), _jsx("p", { className: "slx-billing-detail", children: "Each code can be used once to sign in if you lose access to your authenticator. Store them somewhere safe." }), _jsx("ul", { style: {
|
|
625
|
+
display: 'grid',
|
|
626
|
+
gridTemplateColumns: 'repeat(2, auto)',
|
|
627
|
+
gap: '4px 24px',
|
|
628
|
+
justifyContent: 'start',
|
|
629
|
+
margin: '10px 0',
|
|
630
|
+
paddingLeft: 0,
|
|
631
|
+
listStyle: 'none',
|
|
632
|
+
fontFamily: 'monospace',
|
|
633
|
+
}, children: tfaRecoveryCodes.map((c) => (_jsx("li", { children: c }, c))) }), _jsx("button", { type: "button", className: "slx-btn-secondary", onClick: () => void navigator.clipboard
|
|
634
|
+
?.writeText(tfaRecoveryCodes.join('\n'))
|
|
635
|
+
.catch(() => undefined), children: "Copy codes" })] }), _jsx("button", { type: "button", className: "slx-btn-secondary", onClick: () => void reload(), children: "Done" })] })), user.twoFactorEnabled && (_jsxs(_Fragment, { children: [_jsx("p", { className: "slx-hint", children: "2FA is enabled for this account." }), _jsxs("form", { onSubmit: submitTfaDisable, style: {
|
|
636
|
+
display: 'flex',
|
|
637
|
+
gap: 10,
|
|
638
|
+
alignItems: 'flex-end',
|
|
639
|
+
flexWrap: 'wrap',
|
|
640
|
+
marginTop: 8,
|
|
641
|
+
}, children: [_jsxs("div", { className: "slx-field", style: { flex: '1 1 160px' }, children: [_jsx("label", { className: "slx-label", htmlFor: "slx-tfa-disable", children: "Authenticator code" }), _jsx("input", { id: "slx-tfa-disable", className: "slx-input", type: "text", inputMode: "numeric", autoComplete: "one-time-code", maxLength: 6, pattern: "[0-9]*", placeholder: "000000", value: tfaVerifyCode, onChange: (e) => setTfaVerifyCode(e.target.value.replace(/\D/g, '')), required: true })] }), _jsx("button", { type: "submit", className: "slx-btn-danger-outline", disabled: tfaBusy, children: tfaBusy ? 'Disabling…' : 'Disable 2FA' })] })] }))] }), _jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Connected accounts" }), accountsError && (_jsx("p", { className: "slx-error-text", children: accountsError })), accountsLoading ? (_jsx("p", { className: "slx-hint", children: "Loading\u2026" })) : accounts.length === 0 ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "slx-hint", children: "No social accounts connected. You can sign in with Google or GitHub and link them here later." }), _jsx("button", { type: "button", className: "slx-btn-secondary", onClick: () => void loadAccounts(), children: "Refresh" })] })) : (_jsx(_Fragment, { children: _jsx("ul", { style: { listStyle: 'none', margin: 0, padding: 0 }, children: accounts.map((acc) => (_jsxs("li", { className: "slx-session", style: { alignItems: 'center' }, children: [_jsxs("div", { className: "slx-session-meta", children: [_jsx("p", { className: "slx-session-device", children: acc.provider === 'google' ? 'Google' : 'GitHub' }), _jsxs("p", { className: "slx-session-sub", children: ["Connected ", formatDate(acc.createdAt)] })] }), _jsx("button", { type: "button", className: "slx-btn-danger-outline", onClick: () => void onUnlink(acc.id, acc.provider), disabled: unlinkingId === acc.id, children: unlinkingId === acc.id ? '…' : 'Unlink' })] }, acc.id))) }) }))] }), _jsxs("section", { className: "slx-danger-zone", children: [_jsx("p", { className: "slx-danger-title", children: "Danger zone" }), _jsx("p", { className: "slx-danger-desc", children: "Permanently deletes your account and all associated data. Active sessions are revoked immediately. This cannot be undone." }), deleteError && _jsx("p", { className: "slx-error-text", children: deleteError }), _jsxs("div", { className: "slx-field", children: [_jsxs("label", { className: "slx-label", htmlFor: "slx-del-confirm", children: ["Type ", _jsx("strong", { children: "DELETE" }), " to confirm"] }), _jsx("input", { id: "slx-del-confirm", className: "slx-input", type: "text", value: confirmText, onChange: (e) => setConfirmText(e.target.value), placeholder: "DELETE" })] }), _jsx("button", { type: "button", className: "slx-btn", style: {
|
|
432
642
|
background: 'var(--slx-danger)',
|
|
433
643
|
borderColor: 'var(--slx-danger)',
|
|
434
644
|
}, onClick: onDeleteAccount, disabled: confirmText !== 'DELETE' || deleteBusy, children: deleteBusy ? 'Deleting…' : 'Delete my account forever' })] })] })), tab === 'billing' &&
|
|
435
|
-
(billingLoading ? (_jsx("section", { className: "slx-profile-sec", children: _jsx("p", { className: "slx-hint", children: "Loading billing information\u2026" }) })) : !subscription ? (_jsxs(_Fragment, { children: [_jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Subscription" }), _jsxs("div", { className: "slx-billing-card", children: [_jsx("p", { className: "slx-billing-plan", children: "No active subscription" }), _jsx("p", { className: "slx-billing-detail", children: "You don't have a subscription yet. Choose a plan to get started." })] })] }), _jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Available plans" }), plansLoading ? (_jsx("p", { className: "slx-hint", children: "Loading plans\u2026" })) : plans.length === 0 ? (_jsxs("div", { className: "slx-billing-card", style: { textAlign: 'center' }, children: [_jsx("p", { className: "slx-billing-detail", children: "No plans configured for this project yet." }), _jsx("p", { className: "slx-hint", style: { marginTop: 6 }, children: "Ask your admin to create a plan in billing." })] })) : (_jsx("div", { className: "slx-billing-plans", children: plans.map((plan) => (_jsxs("div", { className: `slx-plan-card${plan.isPopular ? ' popular' : ''}`, children: [plan.isPopular && (_jsx("span", { className: "slx-plan-badge", children: "POPULAR" })), _jsx("p", { className: "slx-plan-name", children: plan.name }), _jsxs("p", { style: { margin: '2px 0 0' }, children: [_jsx("span", { className: "slx-plan-price", children: formatCurrency(plan.amount, plan.currency) }), _jsxs("span", { className: "slx-plan-interval", children: ["/", plan.interval] })] }), plan.trialDays ? (_jsxs("p", { className: "slx-billing-detail", style: {
|
|
645
|
+
(billingLoading ? (_jsx("section", { className: "slx-profile-sec", children: _jsx("p", { className: "slx-hint", children: "Loading billing information\u2026" }) })) : !subscription ? (_jsxs(_Fragment, { children: [_jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Subscription" }), checkoutDone && (_jsx("p", { className: "slx-error-text", style: { color: 'var(--slx-success)', marginBottom: 8 }, children: "Checkout opened \u2014 complete your payment in the overlay." })), checkoutSuccess && (_jsx("p", { className: "slx-error-text", style: { color: 'var(--slx-success)', marginBottom: 8 }, children: "Payment successful \u2014 your subscription is being set up." })), _jsxs("div", { className: "slx-billing-card", children: [_jsx("p", { className: "slx-billing-plan", children: "No active subscription" }), _jsx("p", { className: "slx-billing-detail", children: "You don't have a subscription yet. Choose a plan to get started." })] })] }), _jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Available plans" }), plansLoading ? (_jsx("p", { className: "slx-hint", children: "Loading plans\u2026" })) : plans.length === 0 ? (_jsxs("div", { className: "slx-billing-card", style: { textAlign: 'center' }, children: [_jsx("p", { className: "slx-billing-detail", children: "No plans configured for this project yet." }), _jsx("p", { className: "slx-hint", style: { marginTop: 6 }, children: "Ask your admin to create a plan in billing." })] })) : (_jsx("div", { className: "slx-billing-plans", children: plans.map((plan) => (_jsxs("div", { className: `slx-plan-card${plan.isPopular ? ' popular' : ''}`, children: [plan.isPopular && (_jsx("span", { className: "slx-plan-badge", children: "POPULAR" })), _jsx("p", { className: "slx-plan-name", children: plan.name }), _jsxs("p", { style: { margin: '2px 0 0' }, children: [_jsx("span", { className: "slx-plan-price", children: formatCurrency(plan.amount, plan.currency) }), _jsxs("span", { className: "slx-plan-interval", children: ["/", plan.interval] })] }), plan.trialDays ? (_jsxs("p", { className: "slx-billing-detail", style: {
|
|
436
646
|
color: 'var(--slx-accent)',
|
|
437
647
|
marginTop: 4,
|
|
438
|
-
}, children: [plan.trialDays, " day free trial"] })) : (_jsx("p", { className: "slx-billing-detail", style: { visibility: 'hidden', marginTop: 4 }, children: "\u00A0" })), _jsx("ul", { className: "slx-plan-features", children: (plan.features ?? []).map((f) => (_jsx("li", { children: f }, f))) }), _jsx("button", { type: "button", className: "slx-btn slx-plan-cta", onClick: () => handleCheckout(plan
|
|
648
|
+
}, children: [plan.trialDays, " day free trial"] })) : (_jsx("p", { className: "slx-billing-detail", style: { visibility: 'hidden', marginTop: 4 }, children: "\u00A0" })), _jsx("ul", { className: "slx-plan-features", children: (plan.features ?? []).map((f) => (_jsx("li", { children: f }, f))) }), _jsx("button", { type: "button", className: "slx-btn slx-plan-cta", onClick: () => handleCheckout(plan), disabled: checkoutId === plan.id, children: checkoutId === plan.id
|
|
439
649
|
? 'Redirecting…'
|
|
440
650
|
: 'Choose plan' })] }, plan.id))) }))] }), invoices.length > 0 && (_jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Invoices" }), invoices.map((inv) => (_jsxs("div", { className: "slx-invoice-row", children: [_jsx("span", { className: "slx-invoice-date", children: inv.billedAt ? formatDate(inv.billedAt) : '—' }), _jsx("span", { className: "slx-invoice-amount", children: formatCurrency(inv.amount, inv.currency) }), _jsx("span", { className: `slx-badge ${inv.status === 'paid'
|
|
441
651
|
? 'slx-badge-ok'
|
|
@@ -465,7 +675,7 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
|
|
|
465
675
|
marginTop: 4,
|
|
466
676
|
}, children: [plan.trialDays, " day trial"] })) : (_jsx("p", { className: "slx-billing-detail", style: { visibility: 'hidden', marginTop: 4 }, children: "\u00A0" })), _jsx("ul", { className: "slx-plan-features", children: (plan.features ?? []).map((f) => (_jsx("li", { children: f }, f))) }), _jsx("button", { type: "button", className: isCurrent
|
|
467
677
|
? 'slx-btn-secondary slx-plan-cta'
|
|
468
|
-
: 'slx-btn slx-plan-cta', disabled: isCurrent || checkoutId === plan.id, onClick: () => handleCheckout(plan
|
|
678
|
+
: 'slx-btn slx-plan-cta', disabled: isCurrent || checkoutId === plan.id, onClick: () => handleCheckout(plan), children: isCurrent
|
|
469
679
|
? 'Current plan'
|
|
470
680
|
: checkoutId === plan.id
|
|
471
681
|
? 'Redirecting…'
|