@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 +235 -1
- package/dist/react.js +894 -0
- package/package.json +5 -2
package/dist/react.d.ts
CHANGED
|
@@ -1 +1,235 @@
|
|
|
1
|
-
|
|
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.
|
|
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": {
|
|
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"
|