@viibestack/ui 0.6.0 → 0.6.1

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 +31 -3
  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.1",
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
@@ -256,14 +256,17 @@ export interface MonetizationStatus {
256
256
  billing_interval: "monthly" | "annual";
257
257
  status: string; // mirrors Stripe verbatim: trialing/active/past_due/canceled/...
258
258
  trial_end: string | null;
259
+ seats: number; // 1 unless the app owner set pricing_model to per-seat
259
260
  }
260
261
 
261
262
  // Redirects the browser to Stripe Checkout to start (or restart) the
262
263
  // current end-user's subscription. Resolves once the redirect has been
263
264
  // issued (ok: true) -- there's no "after checkout" callback here; check
264
265
  // 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> {
266
+ // this app's own URL with ?billing=success/cancelled). `seats` only matters
267
+ // when the app owner set per-seat pricing (ignored server-side otherwise) --
268
+ // omit it for flat-priced apps.
269
+ export async function createMonetizationCheckout(interval: "monthly" | "annual", seats?: number): Promise<ActionResult> {
267
270
  const stored = loadStoredSession();
268
271
  if (!stored) return { ok: false, error: "not signed in" };
269
272
  const appId = await resolveAppId();
@@ -272,7 +275,7 @@ export async function createMonetizationCheckout(interval: "monthly" | "annual")
272
275
  const res = await fetch(`${window.location.origin}/api/apps/${appId}/monetization/checkout`, {
273
276
  method: "POST",
274
277
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${stored.jwt}` },
275
- body: JSON.stringify({ billing_interval: interval }),
278
+ body: JSON.stringify({ billing_interval: interval, seats }),
276
279
  });
277
280
  const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
278
281
  if (!res.ok || !body.url) return { ok: false, error: body.error ?? "failed to start checkout" };
@@ -302,3 +305,28 @@ export async function getMonetizationStatus(): Promise<MonetizationStatus | null
302
305
  return null;
303
306
  }
304
307
  }
308
+
309
+ // The standard "need help with this error?" report -- distinct from the
310
+ // automatic window.onerror/unhandledrejection capture every generated app
311
+ // already wires up (that one posts directly to .../errors via raw fetch per
312
+ // integrationRequirements(), no packages/ui helper involved). This one is
313
+ // for the explicit prompt: when an error surfaces, show a small toast/banner
314
+ // asking the end-user if they want to report it, and call this if they say
315
+ // yes. No sign-in required -- reporter_email is optional context, not auth.
316
+ // Server-side this always emails the app owner (072_app_error_log_workflow.sql's
317
+ // alerted_at still dedupes a recurring message within the same hour).
318
+ export async function reportError(message: string, details?: Record<string, unknown>, reporterEmail?: string): Promise<ActionResult> {
319
+ const appId = await resolveAppId();
320
+ if (!appId) return { ok: false, error: "couldn't determine this app's id" };
321
+ try {
322
+ const res = await fetch(`${window.location.origin}/api/apps/${appId}/errors/report`, {
323
+ method: "POST",
324
+ headers: { "Content-Type": "application/json" },
325
+ body: JSON.stringify({ message, details, reporter_email: reporterEmail }),
326
+ });
327
+ if (!res.ok) return { ok: false, error: "failed to report" };
328
+ return { ok: true };
329
+ } catch {
330
+ return { ok: false, error: "network error" };
331
+ }
332
+ }
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> {