@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.
- package/package.json +1 -1
- package/src/auth.ts +31 -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.
|
|
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
|
-
|
|
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
|
|
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> {
|