@elvix.is/sdk 0.2.0 → 0.3.0

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/dist/react.d.ts CHANGED
@@ -1,5 +1,24 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
+ import { ElvixActionResult } from './index.js';
4
+ export { ElvixUser, ElvixVerifyResult } from './index.js';
5
+
6
+ /**
7
+ * `<ElvixCard>` — the chrome every nested `<Elvix*>` mutation surface
8
+ * lives in. Brand-tinted border, secured-by-elvix footer, scrollable
9
+ * body + pinned footer pattern. Pure presentation; no state.
10
+ *
11
+ * Customers don't usually need to wrap manually — most `<Elvix*>`
12
+ * components render their own ElvixCard internally. Exported for
13
+ * cases where a host wants to compose multiple components inside one
14
+ * card (e.g. an account page row).
15
+ */
16
+ declare function ElvixCard({ title, footer, className, children, }: {
17
+ title?: ReactNode;
18
+ footer?: ReactNode;
19
+ className?: string;
20
+ children: ReactNode;
21
+ }): react.JSX.Element;
3
22
 
4
23
  /**
5
24
  * Public types for the React surface. Mirrors the elvix.is bootstrap
@@ -101,4 +120,116 @@ declare function ElvixSignIn({ onResult, redirectAfterSignIn, className, }: {
101
120
  className?: string;
102
121
  }): react.JSX.Element;
103
122
 
104
- export { type ElvixBootstrapEnvelope, type ElvixBrand, ElvixProvider, ElvixSignIn, type ElvixSignInMethod, type ElvixSignInResult, type ElvixSignInResultErr, type ElvixSignInResultOk, type ElvixTheme, useElvixApp, useElvixContext };
123
+ /**
124
+ * `<ElvixUsername>` — claim or change the end-user's username for the
125
+ * current Application. PATCH /api/account/apps/<appId>/username.
126
+ * Render a single in-frame card with two panes: edit + done.
127
+ */
128
+ declare function ElvixUsername({ onResult, }: {
129
+ onResult?: (r: ElvixActionResult<{
130
+ username: string;
131
+ }>) => void;
132
+ }): react.JSX.Element;
133
+
134
+ /**
135
+ * `<ElvixAvatar>` — upload / replace the end-user's avatar for this
136
+ * Application. Reads file → base64 → PATCH /api/account/apps/<appId>/avatar.
137
+ */
138
+ declare function ElvixAvatar({ onResult, }: {
139
+ onResult?: (r: ElvixActionResult<{
140
+ avatarUrl: string;
141
+ }>) => void;
142
+ }): react.JSX.Element;
143
+
144
+ /**
145
+ * `<ElvixBanner>` — upload / replace the end-user's profile banner
146
+ * (16:9 cover image) for this Application.
147
+ */
148
+ declare function ElvixBanner({ onResult, }: {
149
+ onResult?: (r: ElvixActionResult<{
150
+ bannerUrl: string;
151
+ }>) => void;
152
+ }): react.JSX.Element;
153
+
154
+ /**
155
+ * `<ElvixIdentityForm>` — edit the end-user's display name + bio for
156
+ * the current Application. Lightweight per-app profile fields.
157
+ */
158
+ declare function ElvixIdentityForm({ initialName, initialBio, onResult, }: {
159
+ initialName?: string;
160
+ initialBio?: string;
161
+ onResult?: (r: ElvixActionResult<{
162
+ name: string;
163
+ bio: string;
164
+ }>) => void;
165
+ }): react.JSX.Element;
166
+
167
+ /**
168
+ * `<ElvixRegion>` — set the end-user's region (ISO 3166-1 alpha-2
169
+ * country code + timezone). Used by elvix for data-residency hints
170
+ * and locale defaults.
171
+ */
172
+ declare function ElvixRegion({ initialCountry, initialTimezone, onResult, }: {
173
+ initialCountry?: string;
174
+ initialTimezone?: string;
175
+ onResult?: (r: ElvixActionResult<{
176
+ country: string;
177
+ timezone: string;
178
+ }>) => void;
179
+ }): react.JSX.Element;
180
+
181
+ /**
182
+ * `<ElvixLanguages>` — set the end-user's preferred languages (BCP-47
183
+ * tag list, ordered by preference). The first entry drives UI locale.
184
+ */
185
+ declare function ElvixLanguages({ initial, onResult, }: {
186
+ initial?: string[];
187
+ onResult?: (r: ElvixActionResult<{
188
+ languages: string[];
189
+ }>) => void;
190
+ }): react.JSX.Element;
191
+
192
+ declare function ElvixSessions({ onResult, }: {
193
+ onResult?: (r: ElvixActionResult<{
194
+ revoked: number;
195
+ }>) => void;
196
+ }): react.JSX.Element;
197
+
198
+ /**
199
+ * `<ElvixExport>` — GDPR Art. 15 data-export request. Triggers an
200
+ * async server-side zip + emails a single-use download link to the
201
+ * end-user's bound email address.
202
+ */
203
+ declare function ElvixExport({ onResult, }: {
204
+ onResult?: (r: ElvixActionResult<{
205
+ requestId: string;
206
+ }>) => void;
207
+ }): react.JSX.Element;
208
+
209
+ declare function ElvixDeactivate({ onResult, }: {
210
+ onResult?: (r: ElvixActionResult) => void;
211
+ }): react.JSX.Element;
212
+
213
+ declare function ElvixLeave({ onResult, }: {
214
+ onResult?: (r: ElvixActionResult) => void;
215
+ }): react.JSX.Element;
216
+
217
+ /**
218
+ * `<ElvixAddressBook>` — list / add / remove the end-user's addresses
219
+ * on this Application. Read uses GET /api/account/apps/<appId>/addresses;
220
+ * mutations POST + DELETE on the same path.
221
+ */
222
+ declare function ElvixAddressBook({ onResult, }: {
223
+ onResult?: (r: ElvixActionResult) => void;
224
+ }): react.JSX.Element;
225
+
226
+ /**
227
+ * `<ElvixLegalEntities>` — list / add / remove the end-user's legal
228
+ * entities (company names + tax IDs) on this Application. Useful for
229
+ * B2B apps that bill at the entity level.
230
+ */
231
+ declare function ElvixLegalEntities({ onResult, }: {
232
+ onResult?: (r: ElvixActionResult) => void;
233
+ }): react.JSX.Element;
234
+
235
+ export { ElvixActionResult, ElvixAddressBook, ElvixAvatar, ElvixBanner, type ElvixBootstrapEnvelope, type ElvixBrand, ElvixCard, ElvixDeactivate, ElvixExport, ElvixIdentityForm, ElvixLanguages, ElvixLeave, ElvixLegalEntities, ElvixProvider, ElvixRegion, ElvixSessions, ElvixSignIn, type ElvixSignInMethod, type ElvixSignInResult, type ElvixSignInResultErr, type ElvixSignInResultOk, type ElvixTheme, ElvixUsername, useElvixApp, useElvixContext };
package/dist/react.js CHANGED
@@ -1,3 +1,54 @@
1
+ // src/react/elvix-card.tsx
2
+ function ElvixCard({
3
+ title,
4
+ footer,
5
+ className = "",
6
+ children
7
+ }) {
8
+ return /* @__PURE__ */ React.createElement(
9
+ "div",
10
+ {
11
+ className: `elvix-card ${className}`.trim(),
12
+ style: {
13
+ border: "1px solid var(--elvix-primary-12, rgba(93,77,255,0.12))",
14
+ borderRadius: "14px",
15
+ background: "white",
16
+ overflow: "hidden",
17
+ display: "flex",
18
+ flexDirection: "column",
19
+ maxWidth: "440px",
20
+ width: "100%"
21
+ }
22
+ },
23
+ title && /* @__PURE__ */ React.createElement(
24
+ "div",
25
+ {
26
+ style: {
27
+ padding: "20px 24px 0",
28
+ fontSize: "16px",
29
+ fontWeight: 600,
30
+ color: "var(--elvix-primary-strong, #5d4dff)"
31
+ }
32
+ },
33
+ title
34
+ ),
35
+ /* @__PURE__ */ React.createElement("div", { style: { padding: "16px 24px", flex: 1 } }, children),
36
+ footer && /* @__PURE__ */ React.createElement(
37
+ "div",
38
+ {
39
+ style: {
40
+ padding: "12px 24px",
41
+ borderTop: "1px solid var(--elvix-primary-12, rgba(93,77,255,0.12))",
42
+ background: "rgba(0,0,0,0.02)",
43
+ fontSize: "12px",
44
+ color: "rgba(0,0,0,0.55)"
45
+ }
46
+ },
47
+ footer
48
+ )
49
+ );
50
+ }
51
+
1
52
  // src/react/elvix-provider.tsx
2
53
  import {
3
54
  createContext,
@@ -237,9 +288,607 @@ function ElvixSignIn({
237
288
  }
238
289
  ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Verifying\u2026" : verb)), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error));
239
290
  }
291
+
292
+ // src/react/elvix-username.tsx
293
+ import { useState as useState3 } from "react";
294
+
295
+ // src/react/lib.ts
296
+ async function appPost(opts, path, body) {
297
+ try {
298
+ const res = await fetch(`${opts.baseUrl}/api/account/apps/${opts.applicationId}${path}`, {
299
+ method: "POST",
300
+ headers: { "content-type": "application/json" },
301
+ credentials: "include",
302
+ body: JSON.stringify(body)
303
+ });
304
+ const json = await res.json();
305
+ if (!res.ok || !json.success) {
306
+ return { ok: false, error: json.errorMessage ?? "request_failed" };
307
+ }
308
+ return { ok: true, data: json.data };
309
+ } catch (e) {
310
+ return { ok: false, error: "network", message: e instanceof Error ? e.message : void 0 };
311
+ }
312
+ }
313
+ async function appPatch(opts, path, body) {
314
+ try {
315
+ const res = await fetch(`${opts.baseUrl}/api/account/apps/${opts.applicationId}${path}`, {
316
+ method: "PATCH",
317
+ headers: { "content-type": "application/json" },
318
+ credentials: "include",
319
+ body: JSON.stringify(body)
320
+ });
321
+ const json = await res.json();
322
+ if (!res.ok || !json.success) {
323
+ return { ok: false, error: json.errorMessage ?? "request_failed" };
324
+ }
325
+ return { ok: true, data: json.data };
326
+ } catch (e) {
327
+ return { ok: false, error: "network", message: e instanceof Error ? e.message : void 0 };
328
+ }
329
+ }
330
+ async function appDelete(opts, path) {
331
+ try {
332
+ const res = await fetch(`${opts.baseUrl}/api/account/apps/${opts.applicationId}${path}`, {
333
+ method: "DELETE",
334
+ credentials: "include"
335
+ });
336
+ const json = await res.json().catch(() => ({}));
337
+ if (!res.ok || json.success !== void 0 && !json.success) {
338
+ return { ok: false, error: json.errorMessage ?? "request_failed" };
339
+ }
340
+ return { ok: true, data: json.data };
341
+ } catch (e) {
342
+ return { ok: false, error: "network", message: e instanceof Error ? e.message : void 0 };
343
+ }
344
+ }
345
+
346
+ // src/react/elvix-username.tsx
347
+ function ElvixUsername({
348
+ onResult
349
+ }) {
350
+ const ctx = useElvixContext();
351
+ const [value, setValue] = useState3("");
352
+ const [busy, setBusy] = useState3(false);
353
+ const [error, setError] = useState3(null);
354
+ const [done, setDone] = useState3(null);
355
+ async function submit(e) {
356
+ e.preventDefault();
357
+ if (!ctx.app) return;
358
+ setBusy(true);
359
+ setError(null);
360
+ const result = await appPatch(
361
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
362
+ "/username",
363
+ { username: value.trim().toLowerCase() }
364
+ );
365
+ setBusy(false);
366
+ if (!result.ok) {
367
+ setError(result.error);
368
+ } else {
369
+ setDone(result.data?.username ?? value.trim().toLowerCase());
370
+ }
371
+ onResult?.(result);
372
+ }
373
+ if (done) {
374
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Username saved" }, /* @__PURE__ */ React.createElement("p", null, "You are now ", /* @__PURE__ */ React.createElement("strong", null, "@", done), "."));
375
+ }
376
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Choose a username" }, /* @__PURE__ */ React.createElement("form", { onSubmit: submit, className: "elvix-form" }, /* @__PURE__ */ React.createElement(
377
+ "input",
378
+ {
379
+ type: "text",
380
+ value,
381
+ onChange: (e) => setValue(e.target.value.toLowerCase()),
382
+ placeholder: "alice",
383
+ pattern: "[a-z][a-z0-9._]{2,28}[a-z0-9]",
384
+ required: true,
385
+ disabled: busy,
386
+ className: "elvix-input"
387
+ }
388
+ ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy || value.length < 4, className: "elvix-btn elvix-btn-primary" }, busy ? "Saving\u2026" : "Claim"), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error)));
389
+ }
390
+
391
+ // src/react/elvix-avatar.tsx
392
+ import { useState as useState4 } from "react";
393
+ function ElvixAvatar({
394
+ onResult
395
+ }) {
396
+ const ctx = useElvixContext();
397
+ const [busy, setBusy] = useState4(false);
398
+ const [error, setError] = useState4(null);
399
+ const [preview, setPreview] = useState4(null);
400
+ async function onFile(e) {
401
+ const file = e.target.files?.[0];
402
+ if (!file || !ctx.app) return;
403
+ if (file.size > 4 * 1024 * 1024) {
404
+ setError("file_too_large");
405
+ return;
406
+ }
407
+ setBusy(true);
408
+ setError(null);
409
+ const buf = await file.arrayBuffer();
410
+ const b64 = btoa(String.fromCharCode(...new Uint8Array(buf)));
411
+ const dataUrl = `data:${file.type};base64,${b64}`;
412
+ setPreview(dataUrl);
413
+ const result = await appPatch(
414
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
415
+ "/avatar",
416
+ { avatarDataUrl: dataUrl }
417
+ );
418
+ setBusy(false);
419
+ if (!result.ok) setError(result.error);
420
+ onResult?.(result);
421
+ }
422
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Avatar" }, preview && /* @__PURE__ */ React.createElement(
423
+ "img",
424
+ {
425
+ src: preview,
426
+ alt: "avatar preview",
427
+ style: { width: 96, height: 96, borderRadius: "50%", objectFit: "cover", marginBottom: 12 }
428
+ }
429
+ ), /* @__PURE__ */ React.createElement("input", { type: "file", accept: "image/png,image/jpeg,image/webp", onChange: onFile, disabled: busy }), busy && /* @__PURE__ */ React.createElement("p", null, "Uploading\u2026"), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error));
430
+ }
431
+
432
+ // src/react/elvix-banner.tsx
433
+ import { useState as useState5 } from "react";
434
+ function ElvixBanner({
435
+ onResult
436
+ }) {
437
+ const ctx = useElvixContext();
438
+ const [busy, setBusy] = useState5(false);
439
+ const [error, setError] = useState5(null);
440
+ const [preview, setPreview] = useState5(null);
441
+ async function onFile(e) {
442
+ const file = e.target.files?.[0];
443
+ if (!file || !ctx.app) return;
444
+ if (file.size > 8 * 1024 * 1024) {
445
+ setError("file_too_large");
446
+ return;
447
+ }
448
+ setBusy(true);
449
+ setError(null);
450
+ const buf = await file.arrayBuffer();
451
+ const b64 = btoa(String.fromCharCode(...new Uint8Array(buf)));
452
+ const dataUrl = `data:${file.type};base64,${b64}`;
453
+ setPreview(dataUrl);
454
+ const result = await appPatch(
455
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
456
+ "/banner",
457
+ { bannerDataUrl: dataUrl }
458
+ );
459
+ setBusy(false);
460
+ if (!result.ok) setError(result.error);
461
+ onResult?.(result);
462
+ }
463
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Banner" }, preview && /* @__PURE__ */ React.createElement(
464
+ "img",
465
+ {
466
+ src: preview,
467
+ alt: "banner preview",
468
+ style: { width: "100%", aspectRatio: "16/9", objectFit: "cover", borderRadius: 10, marginBottom: 12 }
469
+ }
470
+ ), /* @__PURE__ */ React.createElement("input", { type: "file", accept: "image/png,image/jpeg,image/webp", onChange: onFile, disabled: busy }), busy && /* @__PURE__ */ React.createElement("p", null, "Uploading\u2026"), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error));
471
+ }
472
+
473
+ // src/react/elvix-identity-form.tsx
474
+ import { useState as useState6 } from "react";
475
+ function ElvixIdentityForm({
476
+ initialName = "",
477
+ initialBio = "",
478
+ onResult
479
+ }) {
480
+ const ctx = useElvixContext();
481
+ const [name, setName] = useState6(initialName);
482
+ const [bio, setBio] = useState6(initialBio);
483
+ const [busy, setBusy] = useState6(false);
484
+ const [error, setError] = useState6(null);
485
+ const [saved, setSaved] = useState6(false);
486
+ async function submit(e) {
487
+ e.preventDefault();
488
+ if (!ctx.app) return;
489
+ setBusy(true);
490
+ setError(null);
491
+ const result = await appPatch(
492
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
493
+ "/identity",
494
+ { name: name.trim(), bio: bio.trim() }
495
+ );
496
+ setBusy(false);
497
+ if (!result.ok) setError(result.error);
498
+ else setSaved(true);
499
+ onResult?.(result);
500
+ }
501
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Identity" }, /* @__PURE__ */ React.createElement("form", { onSubmit: submit, className: "elvix-form" }, /* @__PURE__ */ React.createElement("label", null, "Name", /* @__PURE__ */ React.createElement("input", { value: name, onChange: (e) => setName(e.target.value), maxLength: 80, disabled: busy, className: "elvix-input" })), /* @__PURE__ */ React.createElement("label", null, "Bio", /* @__PURE__ */ React.createElement("textarea", { value: bio, onChange: (e) => setBio(e.target.value), maxLength: 500, rows: 3, disabled: busy, className: "elvix-input" })), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Saving\u2026" : "Save"), saved && /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "Saved."), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error)));
502
+ }
503
+
504
+ // src/react/elvix-region.tsx
505
+ import { useState as useState7 } from "react";
506
+ function ElvixRegion({
507
+ initialCountry = "",
508
+ initialTimezone = "",
509
+ onResult
510
+ }) {
511
+ const ctx = useElvixContext();
512
+ const [country, setCountry] = useState7(initialCountry);
513
+ const [timezone, setTimezone] = useState7(initialTimezone);
514
+ const [busy, setBusy] = useState7(false);
515
+ const [error, setError] = useState7(null);
516
+ const [saved, setSaved] = useState7(false);
517
+ async function submit(e) {
518
+ e.preventDefault();
519
+ if (!ctx.app) return;
520
+ setBusy(true);
521
+ setError(null);
522
+ const result = await appPatch(
523
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
524
+ "/region",
525
+ { country: country.toUpperCase().slice(0, 2), timezone: timezone.trim() }
526
+ );
527
+ setBusy(false);
528
+ if (!result.ok) setError(result.error);
529
+ else setSaved(true);
530
+ onResult?.(result);
531
+ }
532
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Region" }, /* @__PURE__ */ React.createElement("form", { onSubmit: submit, className: "elvix-form" }, /* @__PURE__ */ React.createElement("label", null, "Country (ISO-2)", /* @__PURE__ */ React.createElement("input", { value: country, onChange: (e) => setCountry(e.target.value.toUpperCase()), maxLength: 2, pattern: "[A-Z]{2}", disabled: busy, className: "elvix-input" })), /* @__PURE__ */ React.createElement("label", null, "Timezone", /* @__PURE__ */ React.createElement("input", { value: timezone, onChange: (e) => setTimezone(e.target.value), placeholder: "Europe/Berlin", disabled: busy, className: "elvix-input" })), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Saving\u2026" : "Save"), saved && /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "Saved."), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error)));
533
+ }
534
+
535
+ // src/react/elvix-languages.tsx
536
+ import { useState as useState8 } from "react";
537
+ function ElvixLanguages({
538
+ initial = [],
539
+ onResult
540
+ }) {
541
+ const ctx = useElvixContext();
542
+ const [raw, setRaw] = useState8(initial.join(", "));
543
+ const [busy, setBusy] = useState8(false);
544
+ const [error, setError] = useState8(null);
545
+ const [saved, setSaved] = useState8(false);
546
+ async function submit(e) {
547
+ e.preventDefault();
548
+ if (!ctx.app) return;
549
+ setBusy(true);
550
+ setError(null);
551
+ const languages = raw.split(",").map((s) => s.trim()).filter(Boolean);
552
+ const result = await appPatch(
553
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
554
+ "/languages",
555
+ { languages }
556
+ );
557
+ setBusy(false);
558
+ if (!result.ok) setError(result.error);
559
+ else setSaved(true);
560
+ onResult?.(result);
561
+ }
562
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Languages" }, /* @__PURE__ */ React.createElement("form", { onSubmit: submit, className: "elvix-form" }, /* @__PURE__ */ React.createElement("label", null, "Preferred languages (comma-separated BCP-47 tags)", /* @__PURE__ */ React.createElement("input", { value: raw, onChange: (e) => setRaw(e.target.value), placeholder: "en-GB, de-DE", disabled: busy, className: "elvix-input" })), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Saving\u2026" : "Save"), saved && /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "Saved."), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error)));
563
+ }
564
+
565
+ // src/react/elvix-sessions.tsx
566
+ import { useEffect as useEffect2, useState as useState9 } from "react";
567
+ function ElvixSessions({
568
+ onResult
569
+ }) {
570
+ const ctx = useElvixContext();
571
+ const [rows, setRows] = useState9(null);
572
+ const [error, setError] = useState9(null);
573
+ const [busy, setBusy] = useState9(false);
574
+ useEffect2(() => {
575
+ if (!ctx.app) return;
576
+ fetch(`${ctx.baseUrl}/api/account/apps/${ctx.app.applicationId}/sessions`, {
577
+ credentials: "include"
578
+ }).then((r) => r.json()).then((j) => {
579
+ if (j.success && j.data) setRows(j.data.sessions);
580
+ else setError("load_failed");
581
+ }).catch(() => setError("network"));
582
+ }, [ctx.app, ctx.baseUrl]);
583
+ async function revoke(id) {
584
+ if (!ctx.app) return;
585
+ setBusy(true);
586
+ const result = await appDelete(
587
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
588
+ `/sessions/${id}`
589
+ );
590
+ setBusy(false);
591
+ if (result.ok) setRows((prev) => prev?.filter((s) => s.id !== id) ?? null);
592
+ onResult?.(result);
593
+ }
594
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Active sessions" }, error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error), !rows && !error && /* @__PURE__ */ React.createElement("p", null, "Loading\u2026"), rows && /* @__PURE__ */ React.createElement("ul", { style: { listStyle: "none", padding: 0, margin: 0 } }, rows.map((s) => /* @__PURE__ */ React.createElement("li", { key: s.id, style: { padding: "10px 0", borderBottom: "1px solid rgba(0,0,0,0.06)" } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "space-between", gap: 12 } }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { style: { fontSize: 13, fontWeight: 500 } }, s.device, s.current && /* @__PURE__ */ React.createElement("span", { style: { marginLeft: 8, color: "var(--elvix-primary-strong)", fontSize: 11 } }, "\xB7 this device")), /* @__PURE__ */ React.createElement("div", { style: { fontSize: 11, color: "rgba(0,0,0,0.55)" } }, s.country ?? "\u2014", " \xB7 since ", new Date(s.createdAt).toLocaleDateString())), !s.current && /* @__PURE__ */ React.createElement("button", { type: "button", disabled: busy, onClick: () => revoke(s.id), className: "elvix-btn elvix-btn-ghost" }, "Revoke"))))));
595
+ }
596
+
597
+ // src/react/elvix-export.tsx
598
+ import { useState as useState10 } from "react";
599
+ function ElvixExport({
600
+ onResult
601
+ }) {
602
+ const ctx = useElvixContext();
603
+ const [busy, setBusy] = useState10(false);
604
+ const [done, setDone] = useState10(false);
605
+ const [error, setError] = useState10(null);
606
+ async function start() {
607
+ if (!ctx.app) return;
608
+ setBusy(true);
609
+ setError(null);
610
+ const result = await appPost(
611
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
612
+ "/export",
613
+ {}
614
+ );
615
+ setBusy(false);
616
+ if (!result.ok) setError(result.error);
617
+ else setDone(true);
618
+ onResult?.(result);
619
+ }
620
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Export my data" }, /* @__PURE__ */ React.createElement("p", { style: { fontSize: 13, color: "rgba(0,0,0,0.6)" } }, "Request a zip of every record we hold for you in this app. Delivery by email; single-use download link valid for 24h."), done ? /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "Request queued. Check your email.") : /* @__PURE__ */ React.createElement("button", { type: "button", onClick: start, disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Queuing\u2026" : "Request export"), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error));
621
+ }
622
+
623
+ // src/react/elvix-deactivate.tsx
624
+ import { useState as useState11 } from "react";
625
+ function ElvixDeactivate({
626
+ onResult
627
+ }) {
628
+ const ctx = useElvixContext();
629
+ const [pane, setPane] = useState11("warn");
630
+ const [challengeId, setChallengeId] = useState11(null);
631
+ const [code, setCode] = useState11("");
632
+ const [busy, setBusy] = useState11(false);
633
+ const [error, setError] = useState11(null);
634
+ async function startChallenge() {
635
+ if (!ctx.app) return;
636
+ setBusy(true);
637
+ setError(null);
638
+ const result = await appPost(
639
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
640
+ "/membership/challenge",
641
+ { action: "deactivate" }
642
+ );
643
+ setBusy(false);
644
+ if (!result.ok || !result.data?.challengeId) {
645
+ setError(result.ok ? "no_challenge" : result.error);
646
+ return;
647
+ }
648
+ setChallengeId(result.data.challengeId);
649
+ setPane("otp");
650
+ }
651
+ async function confirm(e) {
652
+ e.preventDefault();
653
+ if (!ctx.app || !challengeId) return;
654
+ setBusy(true);
655
+ setError(null);
656
+ const result = await appPost(
657
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
658
+ "/membership/deactivate",
659
+ { challengeId, code: code.trim() }
660
+ );
661
+ setBusy(false);
662
+ if (!result.ok) {
663
+ setError(result.error);
664
+ onResult?.(result);
665
+ return;
666
+ }
667
+ setPane("done");
668
+ onResult?.(result);
669
+ }
670
+ if (pane === "done") {
671
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Deactivated" }, /* @__PURE__ */ React.createElement("p", null, "Your access has been paused. Sign in again to restore it."));
672
+ }
673
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Deactivate account" }, pane === "warn" && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("p", { style: { fontSize: 13, color: "rgba(0,0,0,0.6)" } }, "Pause your membership. You can restore it any time by signing in again. No data is deleted."), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: startChallenge, disabled: busy, className: "elvix-btn elvix-btn-danger" }, busy ? "Sending\u2026" : "Send code")), pane === "otp" && /* @__PURE__ */ React.createElement("form", { onSubmit: confirm, className: "elvix-form" }, /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "We sent a 6-digit code to your email."), /* @__PURE__ */ React.createElement(
674
+ "input",
675
+ {
676
+ type: "text",
677
+ inputMode: "numeric",
678
+ pattern: "[0-9]*",
679
+ maxLength: 6,
680
+ value: code,
681
+ onChange: (e) => setCode(e.target.value.replace(/\D/g, "")),
682
+ required: true,
683
+ disabled: busy,
684
+ className: "elvix-input"
685
+ }
686
+ ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy || code.length !== 6, className: "elvix-btn elvix-btn-danger" }, busy ? "Deactivating\u2026" : "Confirm")), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error));
687
+ }
688
+
689
+ // src/react/elvix-leave.tsx
690
+ import { useState as useState12 } from "react";
691
+ function ElvixLeave({
692
+ onResult
693
+ }) {
694
+ const ctx = useElvixContext();
695
+ const [pane, setPane] = useState12("warn");
696
+ const [challengeId, setChallengeId] = useState12(null);
697
+ const [code, setCode] = useState12("");
698
+ const [busy, setBusy] = useState12(false);
699
+ const [error, setError] = useState12(null);
700
+ async function startChallenge() {
701
+ if (!ctx.app) return;
702
+ setBusy(true);
703
+ setError(null);
704
+ const result = await appPost(
705
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
706
+ "/membership/challenge",
707
+ { action: "leave" }
708
+ );
709
+ setBusy(false);
710
+ if (!result.ok || !result.data?.challengeId) {
711
+ setError(result.ok ? "no_challenge" : result.error);
712
+ return;
713
+ }
714
+ setChallengeId(result.data.challengeId);
715
+ setPane("otp");
716
+ }
717
+ async function confirm(e) {
718
+ e.preventDefault();
719
+ if (!ctx.app || !challengeId) return;
720
+ setBusy(true);
721
+ setError(null);
722
+ const result = await appPost(
723
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
724
+ "/membership/leave",
725
+ { challengeId, code: code.trim() }
726
+ );
727
+ setBusy(false);
728
+ if (!result.ok) {
729
+ setError(result.error);
730
+ onResult?.(result);
731
+ return;
732
+ }
733
+ setPane("done");
734
+ onResult?.(result);
735
+ }
736
+ if (pane === "done") {
737
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "You've left" }, /* @__PURE__ */ React.createElement("p", null, "You've left this app. Your data is archived; sign in again to rejoin."));
738
+ }
739
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Leave this app" }, pane === "warn" && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("p", { style: { fontSize: 13, color: "rgba(0,0,0,0.6)" } }, "Remove yourself from this app. Audit trail is preserved; you can sign back in any time to rejoin."), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: startChallenge, disabled: busy, className: "elvix-btn elvix-btn-danger" }, busy ? "Sending\u2026" : "Send code")), pane === "otp" && /* @__PURE__ */ React.createElement("form", { onSubmit: confirm, className: "elvix-form" }, /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "We sent a 6-digit code to your email."), /* @__PURE__ */ React.createElement(
740
+ "input",
741
+ {
742
+ type: "text",
743
+ inputMode: "numeric",
744
+ pattern: "[0-9]*",
745
+ maxLength: 6,
746
+ value: code,
747
+ onChange: (e) => setCode(e.target.value.replace(/\D/g, "")),
748
+ required: true,
749
+ disabled: busy,
750
+ className: "elvix-input"
751
+ }
752
+ ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy || code.length !== 6, className: "elvix-btn elvix-btn-danger" }, busy ? "Leaving\u2026" : "Confirm leave")), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error));
753
+ }
754
+
755
+ // src/react/elvix-address-book.tsx
756
+ import { useEffect as useEffect3, useState as useState13 } from "react";
757
+ function ElvixAddressBook({
758
+ onResult
759
+ }) {
760
+ const ctx = useElvixContext();
761
+ const [rows, setRows] = useState13(null);
762
+ const [error, setError] = useState13(null);
763
+ const [busy, setBusy] = useState13(false);
764
+ const [adding, setAdding] = useState13(false);
765
+ const [form, setForm] = useState13({
766
+ label: "Home",
767
+ line1: "",
768
+ postalCode: "",
769
+ city: "",
770
+ country: ""
771
+ });
772
+ function reload() {
773
+ if (!ctx.app) return;
774
+ fetch(`${ctx.baseUrl}/api/account/apps/${ctx.app.applicationId}/addresses`, {
775
+ credentials: "include"
776
+ }).then((r) => r.json()).then((j) => {
777
+ if (j.success && j.data) setRows(j.data.addresses);
778
+ else setError("load_failed");
779
+ }).catch(() => setError("network"));
780
+ }
781
+ useEffect3(() => {
782
+ reload();
783
+ }, [ctx.app, ctx.baseUrl]);
784
+ async function add(e) {
785
+ e.preventDefault();
786
+ if (!ctx.app) return;
787
+ setBusy(true);
788
+ const result = await appPost(
789
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
790
+ "/addresses",
791
+ form
792
+ );
793
+ setBusy(false);
794
+ if (result.ok) {
795
+ setAdding(false);
796
+ setForm({ label: "Home", line1: "", postalCode: "", city: "", country: "" });
797
+ reload();
798
+ } else {
799
+ setError(result.error);
800
+ }
801
+ onResult?.(result);
802
+ }
803
+ async function remove(id) {
804
+ if (!ctx.app) return;
805
+ setBusy(true);
806
+ const result = await appDelete(
807
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
808
+ `/addresses/${id}`
809
+ );
810
+ setBusy(false);
811
+ if (result.ok) setRows((prev) => prev?.filter((a) => a.id !== id) ?? null);
812
+ onResult?.(result);
813
+ }
814
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Addresses" }, error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error), !rows && !error && /* @__PURE__ */ React.createElement("p", null, "Loading\u2026"), rows && rows.length === 0 && /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "No addresses yet."), rows?.map((a) => /* @__PURE__ */ React.createElement("div", { key: a.id, style: { padding: "8px 0", borderBottom: "1px solid rgba(0,0,0,0.06)", display: "flex", justifyContent: "space-between", gap: 12 } }, /* @__PURE__ */ React.createElement("div", { style: { fontSize: 13 } }, /* @__PURE__ */ React.createElement("div", { style: { fontWeight: 500 } }, a.label), /* @__PURE__ */ React.createElement("div", { style: { color: "rgba(0,0,0,0.55)" } }, a.line1, a.line2 ? `, ${a.line2}` : "", ", ", a.postalCode, " ", a.city, ", ", a.country)), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: busy, onClick: () => remove(a.id), className: "elvix-btn elvix-btn-ghost" }, "Remove"))), !adding && /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setAdding(true), className: "elvix-btn elvix-btn-primary", style: { marginTop: 12 } }, "Add address"), adding && /* @__PURE__ */ React.createElement("form", { onSubmit: add, className: "elvix-form", style: { marginTop: 12 } }, /* @__PURE__ */ React.createElement("input", { value: form.label, onChange: (e) => setForm({ ...form, label: e.target.value }), placeholder: "Label", className: "elvix-input" }), /* @__PURE__ */ React.createElement("input", { value: form.line1, onChange: (e) => setForm({ ...form, line1: e.target.value }), placeholder: "Street", required: true, className: "elvix-input" }), /* @__PURE__ */ React.createElement("input", { value: form.postalCode, onChange: (e) => setForm({ ...form, postalCode: e.target.value }), placeholder: "Postal code", required: true, className: "elvix-input" }), /* @__PURE__ */ React.createElement("input", { value: form.city, onChange: (e) => setForm({ ...form, city: e.target.value }), placeholder: "City", required: true, className: "elvix-input" }), /* @__PURE__ */ React.createElement("input", { value: form.country, onChange: (e) => setForm({ ...form, country: e.target.value.toUpperCase() }), placeholder: "Country (ISO-2)", maxLength: 2, required: true, className: "elvix-input" }), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Saving\u2026" : "Save")));
815
+ }
816
+
817
+ // src/react/elvix-legal-entities.tsx
818
+ import { useEffect as useEffect4, useState as useState14 } from "react";
819
+ function ElvixLegalEntities({
820
+ onResult
821
+ }) {
822
+ const ctx = useElvixContext();
823
+ const [rows, setRows] = useState14(null);
824
+ const [error, setError] = useState14(null);
825
+ const [busy, setBusy] = useState14(false);
826
+ const [adding, setAdding] = useState14(false);
827
+ const [form, setForm] = useState14({
828
+ legalName: "",
829
+ taxId: "",
830
+ country: ""
831
+ });
832
+ function reload() {
833
+ if (!ctx.app) return;
834
+ fetch(`${ctx.baseUrl}/api/account/apps/${ctx.app.applicationId}/legal-entities`, {
835
+ credentials: "include"
836
+ }).then((r) => r.json()).then((j) => {
837
+ if (j.success && j.data) setRows(j.data.entities);
838
+ else setError("load_failed");
839
+ }).catch(() => setError("network"));
840
+ }
841
+ useEffect4(() => {
842
+ reload();
843
+ }, [ctx.app, ctx.baseUrl]);
844
+ async function add(e) {
845
+ e.preventDefault();
846
+ if (!ctx.app) return;
847
+ setBusy(true);
848
+ const result = await appPost(
849
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
850
+ "/legal-entities",
851
+ form
852
+ );
853
+ setBusy(false);
854
+ if (result.ok) {
855
+ setAdding(false);
856
+ setForm({ legalName: "", taxId: "", country: "" });
857
+ reload();
858
+ } else {
859
+ setError(result.error);
860
+ }
861
+ onResult?.(result);
862
+ }
863
+ async function remove(id) {
864
+ if (!ctx.app) return;
865
+ setBusy(true);
866
+ const result = await appDelete(
867
+ { baseUrl: ctx.baseUrl, applicationId: ctx.app.applicationId },
868
+ `/legal-entities/${id}`
869
+ );
870
+ setBusy(false);
871
+ if (result.ok) setRows((prev) => prev?.filter((a) => a.id !== id) ?? null);
872
+ onResult?.(result);
873
+ }
874
+ return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Legal entities" }, error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error), !rows && !error && /* @__PURE__ */ React.createElement("p", null, "Loading\u2026"), rows && rows.length === 0 && /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "No legal entities yet."), rows?.map((e) => /* @__PURE__ */ React.createElement("div", { key: e.id, style: { padding: "8px 0", borderBottom: "1px solid rgba(0,0,0,0.06)", display: "flex", justifyContent: "space-between", gap: 12 } }, /* @__PURE__ */ React.createElement("div", { style: { fontSize: 13 } }, /* @__PURE__ */ React.createElement("div", { style: { fontWeight: 500 } }, e.legalName), /* @__PURE__ */ React.createElement("div", { style: { color: "rgba(0,0,0,0.55)" } }, e.taxId, " \xB7 ", e.country)), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: busy, onClick: () => remove(e.id), className: "elvix-btn elvix-btn-ghost" }, "Remove"))), !adding && /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setAdding(true), className: "elvix-btn elvix-btn-primary", style: { marginTop: 12 } }, "Add entity"), adding && /* @__PURE__ */ React.createElement("form", { onSubmit: add, className: "elvix-form", style: { marginTop: 12 } }, /* @__PURE__ */ React.createElement("input", { value: form.legalName, onChange: (e) => setForm({ ...form, legalName: e.target.value }), placeholder: "Legal name", required: true, className: "elvix-input" }), /* @__PURE__ */ React.createElement("input", { value: form.taxId, onChange: (e) => setForm({ ...form, taxId: e.target.value }), placeholder: "Tax / VAT ID", required: true, className: "elvix-input" }), /* @__PURE__ */ React.createElement("input", { value: form.country, onChange: (e) => setForm({ ...form, country: e.target.value.toUpperCase() }), placeholder: "Country (ISO-2)", maxLength: 2, required: true, className: "elvix-input" }), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Saving\u2026" : "Save")));
875
+ }
240
876
  export {
877
+ ElvixAddressBook,
878
+ ElvixAvatar,
879
+ ElvixBanner,
880
+ ElvixCard,
881
+ ElvixDeactivate,
882
+ ElvixExport,
883
+ ElvixIdentityForm,
884
+ ElvixLanguages,
885
+ ElvixLeave,
886
+ ElvixLegalEntities,
241
887
  ElvixProvider,
888
+ ElvixRegion,
889
+ ElvixSessions,
242
890
  ElvixSignIn,
891
+ ElvixUsername,
243
892
  useElvixApp,
244
893
  useElvixContext
245
894
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvix.is/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Official elvix SDK. Drop-in React components, server helpers, and an MCP server so AI coding agents integrate elvix on the first try.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://elvix.is",