@elvix.is/sdk 0.1.2 → 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 +1,235 @@
1
- export { ElvixActionResult, ElvixUser, ElvixVerifyErr, ElvixVerifyOk, ElvixVerifyResult } from './index.js';
1
+ import * as react from 'react';
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;
22
+
23
+ /**
24
+ * Public types for the React surface. Mirrors the elvix.is bootstrap
25
+ * envelope so customers can type their host code without importing
26
+ * private elvix internals.
27
+ */
28
+ type ElvixBrand = {
29
+ light: {
30
+ primary: string;
31
+ on: string;
32
+ };
33
+ dark: {
34
+ primary: string;
35
+ on: string;
36
+ };
37
+ };
38
+ type ElvixSignInMethod = "google" | "email_otp" | "passkey" | "username";
39
+ type ElvixBootstrapEnvelope = {
40
+ applicationId: string;
41
+ clientId: string;
42
+ urlSlug: string;
43
+ appName: string;
44
+ logoUrl: string | null;
45
+ logoUrlDark: string | null;
46
+ iconUrl: string | null;
47
+ iconUrlDark: string | null;
48
+ brand: ElvixBrand;
49
+ methods: {
50
+ google: boolean;
51
+ emailOtp: boolean;
52
+ passkey: boolean;
53
+ username: boolean;
54
+ };
55
+ legal: {
56
+ privacyPolicyUrl: string;
57
+ termsOfServiceUrl: string;
58
+ supportEmail: string;
59
+ supportUrl: string | null;
60
+ };
61
+ signInVerb: "signin" | "login";
62
+ signinGate: "public" | "private_beta" | "closed";
63
+ };
64
+ type ElvixSignInResultOk = {
65
+ ok: true;
66
+ /** Where the host should send the user (if anywhere). */
67
+ redirect?: string;
68
+ /** Sign-in factor that succeeded. */
69
+ method: ElvixSignInMethod;
70
+ };
71
+ type ElvixSignInResultErr = {
72
+ ok: false;
73
+ error: string;
74
+ message?: string;
75
+ };
76
+ type ElvixSignInResult = ElvixSignInResultOk | ElvixSignInResultErr;
77
+ /** Theme override. Omit to inherit the Console-configured pair. */
78
+ type ElvixTheme = "light" | "dark" | "system";
79
+
80
+ type ElvixContextValue = {
81
+ clientId: string | undefined;
82
+ baseUrl: string;
83
+ app: ElvixBootstrapEnvelope | null;
84
+ appError: string | null;
85
+ resolvedTheme: "light" | "dark";
86
+ };
87
+ declare function useElvixApp(): ElvixBootstrapEnvelope | null;
88
+ declare function useElvixContext(): ElvixContextValue;
89
+ declare function ElvixProvider({ clientId, theme, brand, baseUrl, children, className, }: {
90
+ clientId?: string;
91
+ theme?: ElvixTheme;
92
+ brand?: ElvixBrand;
93
+ /** Override the elvix origin (testing, proxy setups). */
94
+ baseUrl?: string;
95
+ children: ReactNode;
96
+ className?: string;
97
+ }): react.JSX.Element;
98
+
99
+ /**
100
+ * `<ElvixSignIn>` — drop-in sign-in surface.
101
+ *
102
+ * Reads enabled methods from the Console-configured bootstrap envelope
103
+ * (`<ElvixProvider clientId>` must be in scope). Renders the methods
104
+ * the customer turned on; never invents UI the Console denied.
105
+ *
106
+ * MVP supports:
107
+ * - Email OTP (the most common path)
108
+ * - Google redirect ("Continue with Google")
109
+ *
110
+ * Passkey + username-OTP follow in 0.2.x point releases.
111
+ *
112
+ * `onResult({ ok, ... })` is the only post-success hook the SDK
113
+ * exposes. Hosts navigate from the callback; this component never
114
+ * calls `router.push` itself.
115
+ */
116
+ declare function ElvixSignIn({ onResult, redirectAfterSignIn, className, }: {
117
+ onResult?: (r: ElvixSignInResult) => void;
118
+ /** Default redirect target on success when the server doesn't echo one. */
119
+ redirectAfterSignIn?: string;
120
+ className?: string;
121
+ }): react.JSX.Element;
122
+
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
@@ -0,0 +1,894 @@
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
+
52
+ // src/react/elvix-provider.tsx
53
+ import {
54
+ createContext,
55
+ useContext,
56
+ useEffect,
57
+ useMemo,
58
+ useState
59
+ } from "react";
60
+ var ELVIX_DEFAULT_BRAND = {
61
+ light: { primary: "#5d4dff", on: "#ffffff" },
62
+ dark: { primary: "#8e7dff", on: "#0a0a0b" }
63
+ };
64
+ var DEFAULT_BASE_URL = "https://elvix.is";
65
+ var BOOTSTRAP_URL = (baseUrl, clientId) => `${baseUrl}/api/v1/bootstrap/${encodeURIComponent(clientId)}`;
66
+ var ElvixContext = createContext(null);
67
+ function useElvixApp() {
68
+ const ctx = useContext(ElvixContext);
69
+ return ctx?.app ?? null;
70
+ }
71
+ function useElvixContext() {
72
+ const ctx = useContext(ElvixContext);
73
+ if (!ctx) {
74
+ throw new Error("Elvix components must be wrapped in <ElvixProvider>.");
75
+ }
76
+ return ctx;
77
+ }
78
+ function ElvixProvider({
79
+ clientId,
80
+ theme,
81
+ brand,
82
+ baseUrl,
83
+ children,
84
+ className = ""
85
+ }) {
86
+ const resolvedBaseUrl = baseUrl ?? DEFAULT_BASE_URL;
87
+ const [app, setApp] = useState(null);
88
+ const [appError, setAppError] = useState(null);
89
+ const [systemDark, setSystemDark] = useState(false);
90
+ useEffect(() => {
91
+ if (!clientId) {
92
+ setApp(null);
93
+ setAppError(null);
94
+ return;
95
+ }
96
+ const ctrl = new AbortController();
97
+ fetch(BOOTSTRAP_URL(resolvedBaseUrl, clientId), { signal: ctrl.signal }).then((r) => r.json()).then((body) => {
98
+ if (body?.success && body?.data) {
99
+ setApp(body.data);
100
+ setAppError(null);
101
+ } else {
102
+ setAppError(body?.errorMessage ?? "bootstrap_failed");
103
+ }
104
+ }).catch((e) => {
105
+ if (e?.name === "AbortError") return;
106
+ setAppError(e instanceof Error ? e.message : "bootstrap_failed");
107
+ });
108
+ return () => ctrl.abort();
109
+ }, [clientId, resolvedBaseUrl]);
110
+ useEffect(() => {
111
+ if (typeof window === "undefined") return;
112
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
113
+ setSystemDark(mq.matches);
114
+ const sync = (e) => setSystemDark(e.matches);
115
+ mq.addEventListener("change", sync);
116
+ return () => mq.removeEventListener("change", sync);
117
+ }, []);
118
+ const effectiveTheme = useMemo(() => {
119
+ if (theme === "light") return "light";
120
+ if (theme === "dark") return "dark";
121
+ return systemDark ? "dark" : "light";
122
+ }, [theme, systemDark]);
123
+ const effectiveBrand = brand ?? appBrand(app) ?? ELVIX_DEFAULT_BRAND;
124
+ const pair = effectiveBrand[effectiveTheme];
125
+ const cssVars = useMemo(
126
+ () => ({
127
+ "--elvix-primary": pair.primary,
128
+ "--elvix-on-primary": pair.on,
129
+ "--elvix-primary-12": withAlpha(pair.primary, 0.12),
130
+ "--elvix-primary-35": withAlpha(pair.primary, 0.35),
131
+ "--elvix-primary-55": withAlpha(pair.primary, 0.55),
132
+ "--elvix-primary-strong": pair.primary
133
+ }),
134
+ [pair.primary, pair.on]
135
+ );
136
+ const value = {
137
+ clientId,
138
+ baseUrl: resolvedBaseUrl,
139
+ app,
140
+ appError,
141
+ resolvedTheme: effectiveTheme
142
+ };
143
+ return /* @__PURE__ */ React.createElement(ElvixContext.Provider, { value }, /* @__PURE__ */ React.createElement(
144
+ "div",
145
+ {
146
+ "data-elvix-theme": effectiveTheme,
147
+ style: cssVars,
148
+ className: (effectiveTheme === "dark" ? "dark " : "") + "elvix-sdk-root " + className
149
+ },
150
+ children
151
+ ));
152
+ }
153
+ function appBrand(app) {
154
+ if (!app?.brand) return null;
155
+ return app.brand;
156
+ }
157
+ function withAlpha(hex, a) {
158
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
159
+ if (!m) return hex;
160
+ const n = Number.parseInt(m[1], 16);
161
+ const r = n >> 16 & 255;
162
+ const g = n >> 8 & 255;
163
+ const b = n & 255;
164
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
165
+ }
166
+
167
+ // src/react/elvix-sign-in.tsx
168
+ import { useState as useState2 } from "react";
169
+ function ElvixSignIn({
170
+ onResult,
171
+ redirectAfterSignIn,
172
+ className = ""
173
+ }) {
174
+ const ctx = useElvixContext();
175
+ const app = useElvixApp();
176
+ const [step, setStep] = useState2("identify");
177
+ const [email, setEmail] = useState2("");
178
+ const [code, setCode] = useState2("");
179
+ const [challengeId, setChallengeId] = useState2(null);
180
+ const [busy, setBusy] = useState2(false);
181
+ const [error, setError] = useState2(null);
182
+ const verb = app?.signInVerb === "login" ? "Log in" : "Sign in";
183
+ function fail(error2, message) {
184
+ setError(message ?? error2);
185
+ onResult?.({ ok: false, error: error2, message });
186
+ }
187
+ async function startGoogle() {
188
+ if (!ctx.clientId) return fail("missing_client_id", "ElvixProvider needs a clientId.");
189
+ window.location.assign(
190
+ `${ctx.baseUrl}/api/auth/google/start?intent=app&clientId=${encodeURIComponent(ctx.clientId)}`
191
+ );
192
+ }
193
+ async function startOtp(e) {
194
+ e.preventDefault();
195
+ if (!email.trim()) return fail("invalid_input", "Enter an email.");
196
+ setBusy(true);
197
+ setError(null);
198
+ try {
199
+ const res = await fetch(`${ctx.baseUrl}/api/auth/otp/start`, {
200
+ method: "POST",
201
+ headers: { "content-type": "application/json" },
202
+ credentials: "include",
203
+ body: JSON.stringify({
204
+ email: email.trim(),
205
+ intent: "app",
206
+ clientId: ctx.clientId
207
+ })
208
+ });
209
+ const body = await res.json();
210
+ if (!res.ok || !body.success || !body.data?.challengeId) {
211
+ return fail(body.errorMessage ?? "otp_start_failed");
212
+ }
213
+ setChallengeId(body.data.challengeId);
214
+ setStep("code");
215
+ } catch (e2) {
216
+ fail("network", e2 instanceof Error ? e2.message : void 0);
217
+ } finally {
218
+ setBusy(false);
219
+ }
220
+ }
221
+ async function verifyOtp(e) {
222
+ e.preventDefault();
223
+ if (!challengeId) return;
224
+ if (code.trim().length !== 6) return fail("invalid_input", "Enter the 6-digit code.");
225
+ setBusy(true);
226
+ setError(null);
227
+ try {
228
+ const res = await fetch(`${ctx.baseUrl}/api/auth/otp/verify`, {
229
+ method: "POST",
230
+ headers: { "content-type": "application/json" },
231
+ credentials: "include",
232
+ body: JSON.stringify({ challengeId, code: code.trim() })
233
+ });
234
+ const body = await res.json();
235
+ if (!res.ok || !body.success) {
236
+ return fail(body.errorMessage ?? "otp_verify_failed");
237
+ }
238
+ setStep("done");
239
+ onResult?.({
240
+ ok: true,
241
+ method: "email_otp",
242
+ redirect: body.data?.redirect ?? redirectAfterSignIn
243
+ });
244
+ } catch (e2) {
245
+ fail("network", e2 instanceof Error ? e2.message : void 0);
246
+ } finally {
247
+ setBusy(false);
248
+ }
249
+ }
250
+ const card = `elvix-card ${className}`.trim();
251
+ if (step === "done") {
252
+ return /* @__PURE__ */ React.createElement("div", { className: card, "data-elvix-pane": "done" }, /* @__PURE__ */ React.createElement("p", null, "Signed in."));
253
+ }
254
+ return /* @__PURE__ */ React.createElement("div", { className: card, "data-elvix-pane": step }, /* @__PURE__ */ React.createElement("h2", { className: "elvix-h" }, app?.appName ? `${verb} to ${app.appName}` : verb), step === "identify" && /* @__PURE__ */ React.createElement(React.Fragment, null, app?.methods.google && /* @__PURE__ */ React.createElement(
255
+ "button",
256
+ {
257
+ type: "button",
258
+ onClick: startGoogle,
259
+ disabled: busy,
260
+ className: "elvix-btn elvix-btn-google",
261
+ "data-elvix-method": "google"
262
+ },
263
+ "Continue with Google"
264
+ ), app?.methods.emailOtp && /* @__PURE__ */ React.createElement("form", { onSubmit: startOtp, "data-elvix-method": "email_otp", className: "elvix-otp-form" }, /* @__PURE__ */ React.createElement(
265
+ "input",
266
+ {
267
+ type: "email",
268
+ value: email,
269
+ onChange: (ev) => setEmail(ev.target.value),
270
+ placeholder: "you@example.com",
271
+ required: true,
272
+ disabled: busy,
273
+ className: "elvix-input"
274
+ }
275
+ ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Sending\u2026" : "Send code"))), step === "code" && /* @__PURE__ */ React.createElement("form", { onSubmit: verifyOtp, className: "elvix-otp-form" }, /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "We sent a 6-digit code to ", /* @__PURE__ */ React.createElement("strong", null, email), "."), /* @__PURE__ */ React.createElement(
276
+ "input",
277
+ {
278
+ type: "text",
279
+ inputMode: "numeric",
280
+ pattern: "[0-9]*",
281
+ maxLength: 6,
282
+ value: code,
283
+ onChange: (ev) => setCode(ev.target.value.replace(/\D/g, "")),
284
+ placeholder: "123456",
285
+ required: true,
286
+ disabled: busy,
287
+ className: "elvix-input"
288
+ }
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));
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
+ }
876
+ export {
877
+ ElvixAddressBook,
878
+ ElvixAvatar,
879
+ ElvixBanner,
880
+ ElvixCard,
881
+ ElvixDeactivate,
882
+ ElvixExport,
883
+ ElvixIdentityForm,
884
+ ElvixLanguages,
885
+ ElvixLeave,
886
+ ElvixLegalEntities,
887
+ ElvixProvider,
888
+ ElvixRegion,
889
+ ElvixSessions,
890
+ ElvixSignIn,
891
+ ElvixUsername,
892
+ useElvixApp,
893
+ useElvixContext
894
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvix.is/sdk",
3
- "version": "0.1.2",
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",
@@ -56,13 +56,16 @@
56
56
  "next": ">=15"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
- "next": { "optional": true }
59
+ "next": {
60
+ "optional": true
61
+ }
60
62
  },
61
63
  "dependencies": {
62
64
  "@modelcontextprotocol/sdk": "^1.0.4"
63
65
  },
64
66
  "devDependencies": {
65
67
  "@types/node": "^22.7.5",
68
+ "@types/react": "^19.2.15",
66
69
  "tsup": "^8.3.0",
67
70
  "typescript": "^5.6.3",
68
71
  "vitest": "^2.1.2"