@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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/auth.ts +34 -5
  3. 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.0",
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
- export async function createMonetizationCheckout(interval: "monthly" | "annual"): Promise<ActionResult> {
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 async function listRows<T extends { id: string }>(table: string): Promise<T[]> {
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 res = await fetch(`${window.location.origin}/api/apps/${appId}/data-public/${table}`, { headers: authHeaders() });
41
- if (!res.ok) return [];
42
- const data = (await res.json()) as { rows: T[] };
43
- return data.rows ?? [];
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> {