@viibestack/ui 0.6.0 → 0.6.2
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/package.json +1 -1
- package/src/auth.ts +34 -5
- package/src/data.ts +41 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viibestack/ui",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Dependency-free React UI kit -- icons, core components (Button, Card, Input, Modal, etc.), a real per-app data client (listRows/upsertRow/deleteRow/reportError), and a real per-app end-user auth client (signUp/logIn/logOut/getCurrentUser) backed by ViibeStack's own platform-managed data store.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
package/src/auth.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
export interface AppUser {
|
|
13
13
|
id: string;
|
|
14
14
|
email: string;
|
|
15
|
+
displayName?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export type AuthActionResult = { ok: true; user: AppUser } | { ok: false; error: string };
|
|
@@ -149,8 +150,8 @@ async function callAppAuth<T>(path: string, init?: RequestInit): Promise<{ ok: t
|
|
|
149
150
|
|
|
150
151
|
type SessionResponse = { jwt: string; sessionId: string; user: AppUser };
|
|
151
152
|
|
|
152
|
-
export async function signUp(email: string, password: string): Promise<AuthActionResult> {
|
|
153
|
-
const result = await callAppAuth<SessionResponse>("/signup", { method: "POST", body: JSON.stringify({ email, password }) });
|
|
153
|
+
export async function signUp(email: string, password: string, name?: string): Promise<AuthActionResult> {
|
|
154
|
+
const result = await callAppAuth<SessionResponse>("/signup", { method: "POST", body: JSON.stringify({ email, password, name }) });
|
|
154
155
|
if (!result.ok) return { ok: false, error: result.error };
|
|
155
156
|
saveStoredSession(result.body);
|
|
156
157
|
return { ok: true, user: result.body.user };
|
|
@@ -256,14 +257,17 @@ export interface MonetizationStatus {
|
|
|
256
257
|
billing_interval: "monthly" | "annual";
|
|
257
258
|
status: string; // mirrors Stripe verbatim: trialing/active/past_due/canceled/...
|
|
258
259
|
trial_end: string | null;
|
|
260
|
+
seats: number; // 1 unless the app owner set pricing_model to per-seat
|
|
259
261
|
}
|
|
260
262
|
|
|
261
263
|
// Redirects the browser to Stripe Checkout to start (or restart) the
|
|
262
264
|
// current end-user's subscription. Resolves once the redirect has been
|
|
263
265
|
// issued (ok: true) -- there's no "after checkout" callback here; check
|
|
264
266
|
// getMonetizationStatus() again once the user is back (Stripe redirects to
|
|
265
|
-
// this app's own URL with ?billing=success/cancelled).
|
|
266
|
-
|
|
267
|
+
// this app's own URL with ?billing=success/cancelled). `seats` only matters
|
|
268
|
+
// when the app owner set per-seat pricing (ignored server-side otherwise) --
|
|
269
|
+
// omit it for flat-priced apps.
|
|
270
|
+
export async function createMonetizationCheckout(interval: "monthly" | "annual", seats?: number): Promise<ActionResult> {
|
|
267
271
|
const stored = loadStoredSession();
|
|
268
272
|
if (!stored) return { ok: false, error: "not signed in" };
|
|
269
273
|
const appId = await resolveAppId();
|
|
@@ -272,7 +276,7 @@ export async function createMonetizationCheckout(interval: "monthly" | "annual")
|
|
|
272
276
|
const res = await fetch(`${window.location.origin}/api/apps/${appId}/monetization/checkout`, {
|
|
273
277
|
method: "POST",
|
|
274
278
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${stored.jwt}` },
|
|
275
|
-
body: JSON.stringify({ billing_interval: interval }),
|
|
279
|
+
body: JSON.stringify({ billing_interval: interval, seats }),
|
|
276
280
|
});
|
|
277
281
|
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
278
282
|
if (!res.ok || !body.url) return { ok: false, error: body.error ?? "failed to start checkout" };
|
|
@@ -302,3 +306,28 @@ export async function getMonetizationStatus(): Promise<MonetizationStatus | null
|
|
|
302
306
|
return null;
|
|
303
307
|
}
|
|
304
308
|
}
|
|
309
|
+
|
|
310
|
+
// The standard "need help with this error?" report -- distinct from the
|
|
311
|
+
// automatic window.onerror/unhandledrejection capture every generated app
|
|
312
|
+
// already wires up (that one posts directly to .../errors via raw fetch per
|
|
313
|
+
// integrationRequirements(), no packages/ui helper involved). This one is
|
|
314
|
+
// for the explicit prompt: when an error surfaces, show a small toast/banner
|
|
315
|
+
// asking the end-user if they want to report it, and call this if they say
|
|
316
|
+
// yes. No sign-in required -- reporter_email is optional context, not auth.
|
|
317
|
+
// Server-side this always emails the app owner (072_app_error_log_workflow.sql's
|
|
318
|
+
// alerted_at still dedupes a recurring message within the same hour).
|
|
319
|
+
export async function reportError(message: string, details?: Record<string, unknown>, reporterEmail?: string): Promise<ActionResult> {
|
|
320
|
+
const appId = await resolveAppId();
|
|
321
|
+
if (!appId) return { ok: false, error: "couldn't determine this app's id" };
|
|
322
|
+
try {
|
|
323
|
+
const res = await fetch(`${window.location.origin}/api/apps/${appId}/errors/report`, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
headers: { "Content-Type": "application/json" },
|
|
326
|
+
body: JSON.stringify({ message, details, reporter_email: reporterEmail }),
|
|
327
|
+
});
|
|
328
|
+
if (!res.ok) return { ok: false, error: "failed to report" };
|
|
329
|
+
return { ok: true };
|
|
330
|
+
} catch {
|
|
331
|
+
return { ok: false, error: "network error" };
|
|
332
|
+
}
|
|
333
|
+
}
|
package/src/data.ts
CHANGED
|
@@ -33,17 +33,51 @@ function resolveAppId(): Promise<string | null> {
|
|
|
33
33
|
return appIdPromise;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export
|
|
36
|
+
export interface RowsPage<T> {
|
|
37
|
+
rows: T[];
|
|
38
|
+
nextCursor: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// One page of a table, server-paginated (keyset on updated_at+id, newest
|
|
42
|
+
// first) -- for a table expected to grow into the thousands+ (a CRM's
|
|
43
|
+
// contacts, a helpdesk's tickets), build a real "load more"/infinite-scroll
|
|
44
|
+
// UI around this instead of assuming listRows() below hands back
|
|
45
|
+
// everything at once.
|
|
46
|
+
export async function listRowsPage<T extends { id: string }>(table: string, opts?: { limit?: number; cursor?: string }): Promise<RowsPage<T>> {
|
|
37
47
|
const appId = await resolveAppId();
|
|
38
|
-
if (!appId) return [];
|
|
48
|
+
if (!appId) return { rows: [], nextCursor: null };
|
|
39
49
|
try {
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
const params = new URLSearchParams();
|
|
51
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
52
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
53
|
+
const qs = params.toString();
|
|
54
|
+
const res = await fetch(`${window.location.origin}/api/apps/${appId}/data-public/${table}${qs ? `?${qs}` : ""}`, { headers: authHeaders() });
|
|
55
|
+
if (!res.ok) return { rows: [], nextCursor: null };
|
|
56
|
+
const data = (await res.json()) as { rows: T[]; next_cursor: string | null };
|
|
57
|
+
return { rows: data.rows ?? [], nextCursor: data.next_cursor ?? null };
|
|
44
58
|
} catch {
|
|
45
|
-
return [];
|
|
59
|
+
return { rows: [], nextCursor: null };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Convenience wrapper every existing generated app already calls expecting
|
|
64
|
+
// a plain array back. Auto-pages up to MAX_AUTO_PAGES so a table's growth
|
|
65
|
+
// no longer silently truncates at a hard 1000-row ceiling -- but this is
|
|
66
|
+
// still a "fetch it all into memory" call, not real pagination; a table
|
|
67
|
+
// genuinely headed into the tens of thousands of rows should move to
|
|
68
|
+
// listRowsPage() above and render incrementally instead.
|
|
69
|
+
const MAX_AUTO_PAGES = 20; // 20 x 1000-row pages = 20,000 rows before this stops and returns what it has
|
|
70
|
+
|
|
71
|
+
export async function listRows<T extends { id: string }>(table: string): Promise<T[]> {
|
|
72
|
+
const all: T[] = [];
|
|
73
|
+
let cursor: string | undefined;
|
|
74
|
+
for (let page = 0; page < MAX_AUTO_PAGES; page++) {
|
|
75
|
+
const { rows, nextCursor } = await listRowsPage<T>(table, { limit: 1000, cursor });
|
|
76
|
+
all.push(...rows);
|
|
77
|
+
if (!nextCursor) break;
|
|
78
|
+
cursor = nextCursor;
|
|
46
79
|
}
|
|
80
|
+
return all;
|
|
47
81
|
}
|
|
48
82
|
|
|
49
83
|
export async function upsertRow<T extends { id: string }>(table: string, row: T): Promise<void> {
|