@slyxup/ui 0.2.9 → 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.
@@ -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|iOS/i.test(ua)
31
+ : /iPhone|iPad|iPod/i.test(ua)
31
32
  ? 'iOS'
32
- : /Linux/i.test(ua)
33
- ? 'Linux'
34
- : 'Unknown OS';
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
- : /Chrome\//i.test(ua)
38
- ? 'Chrome'
39
- : /Safari\//i.test(ua)
40
- ? 'Safari'
41
- : /Firefox\//i.test(ua)
42
- ? 'Firefox'
43
- : 'Browser';
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
- // Sync form fields when user loads/updates (ensures firstName/lastName show correctly
104
- // after SlyxUpProvider fetches client.users.me()). Guard against stale overwrites.
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
- }, [user?.firstName, user?.lastName, user?.avatarUrl]);
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);
@@ -134,21 +190,31 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
134
190
  setBillingLoading(true);
135
191
  setPlansLoading(true);
136
192
  try {
137
- const rawApiUrl = client.apiUrl ?? 'https://auth.slyxup.online';
138
- const billingUrl = rawApiUrl.replace('auth.slyxup.online', 'billing.slyxup.online');
139
- const token = client?._token
140
- ?? client?.getToken?.();
193
+ const rawApiUrl = client.apiUrl ??
194
+ 'https://auth.slyxup.online';
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;
141
204
  const headers = {
142
205
  'Content-Type': 'application/json',
143
206
  };
144
207
  if (token)
145
208
  headers.Authorization = `Bearer ${token}`;
146
209
  // Also forward publishable key if available (helps billing resolve project)
147
- const pubKey = client?.publishableKey;
210
+ const pubKey = client
211
+ ?.publishableKey;
148
212
  if (pubKey && pubKey !== 'pk_test_missing')
149
213
  headers['X-Publishable-Key'] = pubKey;
150
- // Derive projectId for plans: prefer user.projectId, then try subscription's projectId, fallback none
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
151
216
  const projectId = user?.projectId ?? null;
217
+ setProjectId(projectId);
152
218
  // Fetch subscription + invoices (new /v1/billing/* with fallback to legacy /v1/*)
153
219
  async function fetchJson(url) {
154
220
  const res = await fetch(url, { headers, credentials: 'include' });
@@ -163,7 +229,7 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
163
229
  `${billingUrl}/v1/subscription`,
164
230
  ]) {
165
231
  try {
166
- const j = await fetchJson(path);
232
+ const j = (await fetchJson(path));
167
233
  if (j && j.ok !== false) {
168
234
  subData = j;
169
235
  break;
@@ -184,9 +250,17 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
184
250
  setSubscription({
185
251
  id: String(sub.id ?? ''),
186
252
  status: String(sub.status ?? 'active'),
187
- planId: sub.planId ?? sub.plan_id ?? null,
188
- planName: sub.planName ?? sub.plan_name ?? sub.name ?? null,
189
- currentPeriodEnd: sub.currentPeriodEnd ?? sub.current_period_end ?? sub.currentPeriod_end ?? null,
253
+ planId: sub.planId ??
254
+ sub.plan_id ??
255
+ null,
256
+ planName: sub.planName ??
257
+ sub.plan_name ??
258
+ sub.name ??
259
+ null,
260
+ currentPeriodEnd: sub.currentPeriodEnd ??
261
+ sub.current_period_end ??
262
+ sub.currentPeriod_end ??
263
+ null,
190
264
  cancelAtPeriodEnd: Boolean(sub.cancelAtPeriodEnd ?? sub.cancel_at_period_end ?? false),
191
265
  });
192
266
  }
@@ -198,7 +272,10 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
198
272
  setSubscription(null);
199
273
  }
200
274
  // Invoices
201
- for (const path of [`${billingUrl}/v1/billing/invoices`, `${billingUrl}/v1/invoices`]) {
275
+ for (const path of [
276
+ `${billingUrl}/v1/billing/invoices`,
277
+ `${billingUrl}/v1/invoices`,
278
+ ]) {
202
279
  try {
203
280
  const invJ = (await fetchJson(path));
204
281
  if (invJ?.ok !== false && Array.isArray(invJ.invoices)) {
@@ -207,7 +284,9 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
207
284
  amount: Number(inv.amount ?? 0),
208
285
  currency: String(inv.currency ?? 'USD'),
209
286
  status: String(inv.status ?? 'pending'),
210
- billedAt: inv.billedAt ?? inv.billed_at ?? null,
287
+ billedAt: inv.billedAt ??
288
+ inv.billed_at ??
289
+ null,
211
290
  })));
212
291
  break;
213
292
  }
@@ -216,14 +295,22 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
216
295
  // try next
217
296
  }
218
297
  }
219
- // Plans (needs projectId — if missing, try without and degrade gracefully)
298
+ // Plans (needs projectId — if missing, try with publishableKey header instead of empty projectId)
299
+ let gotPlans = false;
220
300
  const planPaths = [];
221
- if (projectId)
301
+ if (projectId) {
222
302
  planPaths.push(`${billingUrl}/v1/billing/plans?projectId=${projectId}`);
223
- // Also try without projectId as last resort (will 400 but we catch)
224
- planPaths.push(`${billingUrl}/v1/billing/plans?projectId=${projectId ?? ''}`);
225
- planPaths.push(`${billingUrl}/v1/plans`);
226
- let gotPlans = false;
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
+ }
227
314
  for (const p of planPaths) {
228
315
  try {
229
316
  const pj = (await fetchJson(p));
@@ -251,16 +338,27 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
251
338
  }, [client, user]);
252
339
  useEffect(() => {
253
340
  if (tab === 'security')
254
- void loadSessions();
341
+ void loadSessions(0);
255
342
  if (tab === 'billing')
256
343
  void loadBilling();
257
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]);
258
356
  async function onProfileSubmit(e) {
259
357
  e.preventDefault();
260
358
  setBusy(true);
261
359
  setSaved(false);
262
360
  try {
263
- await client.users.update({ firstName, lastName, avatarUrl });
361
+ await client.users.update({ firstName, lastName, username, avatarUrl });
264
362
  await reload();
265
363
  setSaved(true);
266
364
  setTimeout(() => setSaved(false), 2500);
@@ -282,6 +380,95 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
282
380
  setResending(false);
283
381
  }
284
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
+ }
285
472
  async function onPasswordSubmit(e) {
286
473
  e.preventDefault();
287
474
  setPwError(null);
@@ -314,7 +501,7 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
314
501
  setRevokingId(id);
315
502
  try {
316
503
  await client.sessions.revoke(id);
317
- await loadSessions();
504
+ await loadSessions(sessionsPage);
318
505
  }
319
506
  finally {
320
507
  setRevokingId(null);
@@ -324,7 +511,7 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
324
511
  setOthersRevoking(true);
325
512
  try {
326
513
  await client.sessions.revokeOthers();
327
- await loadSessions();
514
+ await loadSessions(0);
328
515
  }
329
516
  finally {
330
517
  setOthersRevoking(false);
@@ -345,35 +532,51 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
345
532
  setDeleteBusy(false);
346
533
  }
347
534
  }
348
- async function handleCheckout(planId) {
349
- setCheckoutId(planId);
535
+ async function handleCheckout(plan) {
536
+ setCheckoutId(plan.id);
537
+ setCheckoutDone(false);
350
538
  try {
351
- const rawApiUrl = client.apiUrl ?? 'https://auth.slyxup.online';
352
- const billingUrl = rawApiUrl.replace('auth.slyxup.online', 'billing.slyxup.online');
353
- const token = client?._token
354
- ?? client?.getToken?.();
355
- const headers = { 'Content-Type': 'application/json' };
356
- if (token)
357
- headers.Authorization = `Bearer ${token}`;
358
- const pubKey = client?.publishableKey;
359
- if (pubKey && pubKey !== 'pk_test_missing')
360
- headers['X-Publishable-Key'] = pubKey;
361
- const res = await fetch(`${billingUrl}/v1/billing/checkout`, {
362
- method: 'POST',
363
- headers,
364
- credentials: 'include',
365
- body: JSON.stringify({ planId }),
366
- });
367
- const data = (await res.json().catch(() => ({})));
368
- if (res.ok && typeof data.checkoutUrl === 'string' && data.checkoutUrl) {
369
- window.location.href = data.checkoutUrl;
370
- return;
371
- }
372
- if (!res.ok)
373
- throw new Error(typeof data.error === 'string' ? data.error : `Checkout failed (${res.status})`);
539
+ // Always use Paddle.js overlay checkout
540
+ // authApiUrl is available via client apiUrl
541
+ const rawApiUrl = client.apiUrl ??
542
+ 'https://auth.slyxup.online';
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,
550
+ };
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;
374
578
  }
375
579
  catch (err) {
376
- // Fallback: if billing checkout isn't configured, just log. Parent can handle via window redirect.
377
580
  console.error('[SlyxUp] checkout failed', err);
378
581
  }
379
582
  finally {
@@ -388,24 +591,67 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
388
591
  const name = displayName(user);
389
592
  // Resolve current plan name via plans lookup if subscription has planId but no planName
390
593
  const currentPlan = subscription
391
- ? plans.find((p) => p.id === subscription.planId) ?? null
594
+ ? (plans.find((p) => p.id === subscription.planId) ?? null)
392
595
  : null;
393
596
  const resolvedPlanName = subscription?.planName ?? currentPlan?.name ?? null;
394
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) => {
395
598
  e.stopPropagation();
396
599
  onClose?.();
397
- }, "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: {
398
601
  display: 'inline-flex',
399
602
  alignItems: 'center',
400
603
  gap: 8,
401
604
  flexWrap: 'wrap',
402
- }, children: [_jsx("span", { className: "slx-badge slx-badge-warn", children: "Unverified" }), _jsx("button", { type: "button", className: "slx-link", onClick: onResendVerification, disabled: resending, children: resending ? 'Sending…' : resent ? 'Sent ✓' : 'Resend' })] }))] })] })] })), tab === 'security' && (_jsxs(_Fragment, { children: [_jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Change password" }), pwSaved && (_jsx("p", { className: "slx-error-text", style: { color: 'var(--slx-success)' }, children: "Password updated." })), pwError && _jsx("p", { className: "slx-error-text", children: pwError }), _jsxs("form", { onSubmit: onPasswordSubmit, children: [_jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-pw-current", children: "Current password" }), _jsx("input", { id: "slx-pw-current", className: "slx-input", type: "password", autoComplete: "current-password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), required: true })] }), _jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-pw-new", children: "New password" }), _jsx("input", { id: "slx-pw-new", className: "slx-input", type: "password", autoComplete: "new-password", minLength: 8, value: newPassword, onChange: (e) => setNewPassword(e.target.value), required: true }), _jsx("p", { className: "slx-hint", children: "At least 8 characters." })] }), _jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-pw-confirm", children: "Confirm new password" }), _jsx("input", { id: "slx-pw-confirm", className: "slx-input", type: "password", autoComplete: "new-password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), required: true })] }), _jsx("button", { className: "slx-btn", type: "submit", disabled: pwBusy, children: pwBusy ? 'Updating…' : 'Update password' })] })] }), _jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Active sessions" }), sessionsLoading ? (_jsx("p", { className: "slx-hint", children: "Loading sessions\u2026" })) : sessions.length === 0 ? (_jsx("p", { className: "slx-hint", children: "No active sessions." })) : (_jsxs(_Fragment, { children: [sessions.map((s) => (_jsxs("div", { className: "slx-session", children: [_jsxs("div", { className: "slx-session-meta", children: [_jsxs("p", { className: "slx-session-device", children: [deviceLabel(s.userAgent), s.isCurrent && _jsx("span", { className: "slx-badge slx-badge-accent", children: "This device" })] }), _jsx("p", { className: "slx-session-sub", children: [s.ipAddress, `created ${formatDate(s.createdAt)}`, `expires ${formatDate(s.expiresAt)}`]
605
+ }, children: [_jsx("span", { className: "slx-badge slx-badge-warn", children: "Unverified" }), _jsx("button", { type: "button", className: "slx-link", onClick: onResendVerification, disabled: resending, children: resending ? 'Sending…' : resent ? 'Sent ✓' : 'Resend' })] }))] })] })] })), tab === 'security' && (_jsxs(_Fragment, { children: [_jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Change password" }), pwSaved && (_jsx("p", { className: "slx-error-text", style: { color: 'var(--slx-success)' }, children: "Password updated." })), pwError && _jsx("p", { className: "slx-error-text", children: pwError }), _jsxs("form", { onSubmit: onPasswordSubmit, children: [_jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-pw-current", children: "Current password" }), _jsx("input", { id: "slx-pw-current", className: "slx-input", type: "password", autoComplete: "current-password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), required: true })] }), _jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-pw-new", children: "New password" }), _jsx("input", { id: "slx-pw-new", className: "slx-input", type: "password", autoComplete: "new-password", minLength: 8, value: newPassword, onChange: (e) => setNewPassword(e.target.value), required: true }), _jsx("p", { className: "slx-hint", children: "At least 8 characters." })] }), _jsxs("div", { className: "slx-field", children: [_jsx("label", { className: "slx-label", htmlFor: "slx-pw-confirm", children: "Confirm new password" }), _jsx("input", { id: "slx-pw-confirm", className: "slx-input", type: "password", autoComplete: "new-password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), required: true })] }), _jsx("button", { className: "slx-btn", type: "submit", disabled: pwBusy, children: pwBusy ? 'Updating…' : 'Update password' })] })] }), _jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Active sessions" }), sessionsLoading ? (_jsx("p", { className: "slx-hint", children: "Loading sessions\u2026" })) : sessions.length === 0 ? (_jsx("p", { className: "slx-hint", children: "No active sessions." })) : (_jsxs(_Fragment, { children: [sessions.map((s) => (_jsxs("div", { className: "slx-session", children: [_jsxs("div", { className: "slx-session-meta", children: [_jsxs("p", { className: "slx-session-device", children: [deviceLabel(s.userAgent), s.isCurrent && (_jsx("span", { className: "slx-badge slx-badge-accent", children: "This device" }))] }), _jsx("p", { className: "slx-session-sub", children: [
606
+ s.ipAddress,
607
+ `created ${formatDate(s.createdAt)}`,
608
+ `expires ${formatDate(s.expiresAt)}`,
609
+ ]
403
610
  .filter(Boolean)
404
- .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 ? 'Signing out…' : 'Sign out other devices' }))] }))] }), _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: {
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
612
+ ? 'Signing out…'
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: {
405
642
  background: 'var(--slx-danger)',
406
643
  borderColor: 'var(--slx-danger)',
407
644
  }, onClick: onDeleteAccount, disabled: confirmText !== 'DELETE' || deleteBusy, children: deleteBusy ? 'Deleting…' : 'Delete my account forever' })] })] })), tab === 'billing' &&
408
- (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: { color: 'var(--slx-accent)', marginTop: 4 }, 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.id), disabled: checkoutId === plan.id, children: checkoutId === plan.id ? 'Redirecting…' : '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' ? 'slx-badge-ok' : inv.status === 'overdue' ? 'slx-badge-warn' : 'slx-badge-accent'}`, children: inv.status })] }, inv.id)))] }))] })) : (_jsxs(_Fragment, { children: [_jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Current plan" }), _jsxs("div", { className: "slx-billing-card", children: [_jsxs("div", { 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: {
646
+ color: 'var(--slx-accent)',
647
+ marginTop: 4,
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
649
+ ? 'Redirecting…'
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'
651
+ ? 'slx-badge-ok'
652
+ : inv.status === 'overdue'
653
+ ? 'slx-badge-warn'
654
+ : 'slx-badge-accent'}`, children: inv.status })] }, inv.id)))] }))] })) : (_jsxs(_Fragment, { children: [_jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Current plan" }), _jsxs("div", { className: "slx-billing-card", children: [_jsxs("div", { style: {
409
655
  display: 'flex',
410
656
  justifyContent: 'space-between',
411
657
  alignItems: 'flex-start',
@@ -413,14 +659,23 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
413
659
  flexWrap: 'wrap',
414
660
  }, children: [_jsxs("div", { style: { minWidth: 0 }, children: [_jsx("p", { className: "slx-billing-plan", children: resolvedPlanName ?? 'Subscription' }), _jsxs("p", { className: "slx-billing-detail", children: ["Status:", ' ', _jsx("span", { className: `slx-billing-status slx-billing-status-${subscription.status}`, children: subscription.status })] }), subscription.currentPeriodEnd && (_jsx("p", { className: "slx-billing-detail", children: subscription.cancelAtPeriodEnd
415
661
  ? `Cancels ${formatDate(subscription.currentPeriodEnd)}`
416
- : `Renews ${formatDate(subscription.currentPeriodEnd)}` })), subscription.cancelAtPeriodEnd && (_jsx("p", { className: "slx-billing-detail", style: { color: 'var(--slx-danger)', fontWeight: 600 }, children: "Scheduled to cancel at period end" }))] }), currentPlan && (_jsxs("span", { className: "slx-badge slx-badge-accent", style: { flexShrink: 0 }, children: [formatCurrency(currentPlan.amount, currentPlan.currency), "/", currentPlan.interval] }))] }), currentPlan?.features && currentPlan.features.length > 0 && (_jsx("ul", { className: "slx-plan-features", style: { margin: '12px 0 0' }, children: currentPlan.features.map((f) => (_jsx("li", { children: f }, f))) })), _jsx("div", { className: "slx-billing-actions", children: _jsx("button", { type: "button", className: "slx-btn-secondary", onClick: () => void loadBilling(), children: "Refresh" }) })] })] }), plansLoading ? (_jsx("section", { className: "slx-profile-sec", children: _jsx("p", { className: "slx-hint", children: "Loading available plans\u2026" }) })) : plans.length > 0 ? (_jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Available plans" }), _jsx("p", { className: "slx-hint", style: { marginBottom: 8 }, children: "Switch plans anytime. Changes apply at the next billing cycle." }), _jsx("div", { className: "slx-billing-plans", children: plans.map((plan) => {
662
+ : `Renews ${formatDate(subscription.currentPeriodEnd)}` })), subscription.cancelAtPeriodEnd && (_jsx("p", { className: "slx-billing-detail", style: {
663
+ color: 'var(--slx-danger)',
664
+ fontWeight: 600,
665
+ }, children: "Scheduled to cancel at period end" }))] }), currentPlan && (_jsxs("span", { className: "slx-badge slx-badge-accent", style: { flexShrink: 0 }, children: [formatCurrency(currentPlan.amount, currentPlan.currency), "/", currentPlan.interval] }))] }), currentPlan?.features &&
666
+ currentPlan.features.length > 0 && (_jsx("ul", { className: "slx-plan-features", style: { margin: '12px 0 0' }, children: currentPlan.features.map((f) => (_jsx("li", { children: f }, f))) })), _jsx("div", { className: "slx-billing-actions", children: _jsx("button", { type: "button", className: "slx-btn-secondary", onClick: () => void loadBilling(), children: "Refresh" }) })] })] }), plansLoading ? (_jsx("section", { className: "slx-profile-sec", children: _jsx("p", { className: "slx-hint", children: "Loading available plans\u2026" }) })) : plans.length > 0 ? (_jsxs("section", { className: "slx-profile-sec", children: [_jsx("h3", { className: "slx-sec-title", children: "Available plans" }), _jsx("p", { className: "slx-hint", style: { marginBottom: 8 }, children: "Switch plans anytime. Changes apply at the next billing cycle." }), _jsx("div", { className: "slx-billing-plans", children: plans.map((plan) => {
417
667
  const isCurrent = subscription.planId
418
668
  ? subscription.planId === plan.id
419
669
  : resolvedPlanName === plan.name;
420
670
  const currentAmount = currentPlan?.amount ?? 0;
421
671
  const isUpgrade = plan.amount > currentAmount;
422
672
  const isDowngrade = plan.amount < currentAmount && !isCurrent;
423
- return (_jsxs("div", { className: `slx-plan-card${plan.isPopular ? ' popular' : ''}`, style: isCurrent ? { opacity: 0.92 } : undefined, children: [plan.isPopular && !isCurrent && _jsx("span", { className: "slx-plan-badge", children: "POPULAR" }), isCurrent && _jsx("span", { className: "slx-plan-badge", style: { background: 'var(--slx-success)' }, children: "CURRENT" }), _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: { color: 'var(--slx-accent)', marginTop: 4 }, 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 ? 'slx-btn-secondary slx-plan-cta' : 'slx-btn slx-plan-cta', disabled: isCurrent || checkoutId === plan.id, onClick: () => handleCheckout(plan.id), children: isCurrent
673
+ return (_jsxs("div", { className: `slx-plan-card${plan.isPopular ? ' popular' : ''}`, style: isCurrent ? { opacity: 0.92 } : undefined, children: [plan.isPopular && !isCurrent && (_jsx("span", { className: "slx-plan-badge", children: "POPULAR" })), isCurrent && (_jsx("span", { className: "slx-plan-badge", style: { background: 'var(--slx-success)' }, children: "CURRENT" })), _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: {
674
+ color: 'var(--slx-accent)',
675
+ marginTop: 4,
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
677
+ ? 'slx-btn-secondary slx-plan-cta'
678
+ : 'slx-btn slx-plan-cta', disabled: isCurrent || checkoutId === plan.id, onClick: () => handleCheckout(plan), children: isCurrent
424
679
  ? 'Current plan'
425
680
  : checkoutId === plan.id
426
681
  ? 'Redirecting…'
@@ -429,13 +684,19 @@ export function UserProfile({ modal = true, onClose, onDeleted, }) {
429
684
  : isDowngrade
430
685
  ? 'Downgrade'
431
686
  : 'Switch plan' })] }, plan.id));
432
- }) })] })) : null, 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' ? 'slx-badge-ok' : inv.status === 'overdue' ? 'slx-badge-warn' : 'slx-badge-accent'}`, children: inv.status })] }, inv.id)))] }))] })))] })] })] }));
687
+ }) })] })) : null, 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'
688
+ ? 'slx-badge-ok'
689
+ : inv.status === 'overdue'
690
+ ? 'slx-badge-warn'
691
+ : 'slx-badge-accent'}`, children: inv.status })] }, inv.id)))] }))] })))] })] })] }));
433
692
  if (!modal) {
434
693
  return (_jsx("div", { className: "slx-profile-modal", style: { maxHeight: 'none' }, children: bodyInner }));
435
694
  }
436
695
  // Modal overlay — click on backdrop closes, click inside modal does not.
437
696
  // Use onMouseDown + onClick for desktop + mobile reliability; close button also works via stopPropagation.
438
- return (_jsx("div", { className: "slx-overlay", onMouseDown: (e) => {
697
+ return (
698
+ // biome-ignore lint/a11y/useKeyWithClickEvents: overlay click is for mouse; keyboard Escape handled in effect
699
+ _jsx("div", { className: "slx-overlay", onMouseDown: (e) => {
439
700
  if (e.target === e.currentTarget)
440
701
  onClose?.();
441
702
  }, onClick: (e) => {