@viibestack/ui 0.6.3 → 0.6.5
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 +2 -2
- package/src/auth.ts +38 -9
- package/src/badge.tsx +74 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viibestack/ui",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Dependency-free React UI kit -- icons, core components (Button, Card, Input, Modal, etc.), a real per-app data client (listRows/upsertRow/deleteRow/captureClientError),
|
|
3
|
+
"version": "0.6.5",
|
|
4
|
+
"description": "Dependency-free React UI kit -- icons, core components (Button, Card, Input, Modal, etc.), a real per-app data client (listRows/upsertRow/deleteRow/captureClientError), a real per-app end-user auth client (signUp/logIn/logOut/getCurrentUser/reportError), and a self-gating PoweredByBadge component -- backed by ViibeStack's own platform-managed data store.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"types": "./src/index.ts",
|
package/src/auth.ts
CHANGED
|
@@ -171,6 +171,39 @@ export async function logOut(): Promise<void> {
|
|
|
171
171
|
await callAppAuth("/logout", { method: "POST", body: JSON.stringify({ session_id: stored.sessionId }) });
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// Single-flight guard for the refresh call below -- without this, calling
|
|
175
|
+
// getCurrentUser() from more than one place on the same page load (a nav
|
|
176
|
+
// component AND a page component each checking auth in their own
|
|
177
|
+
// useEffect, a completely normal shape for a generated app) fires two
|
|
178
|
+
// concurrent /session/refresh calls with the SAME still-stored session_id.
|
|
179
|
+
// The auth service rotates that id on every refresh (see services/auth's
|
|
180
|
+
// own comment on that route), so only the first of the two UPDATEs
|
|
181
|
+
// actually matches a row -- the second gets back "session expired" even
|
|
182
|
+
// though the first one just succeeded, and its `saveStoredSession(null)`
|
|
183
|
+
// then wipes out the perfectly valid session the first call just saved,
|
|
184
|
+
// logging a real signed-in user out. Confirmed as the root cause of a
|
|
185
|
+
// real_accounts logout report (2026-07-19) -- funneling every concurrent
|
|
186
|
+
// caller through the SAME in-flight promise means only one refresh request
|
|
187
|
+
// is ever sent for a given expiring token, regardless of how many
|
|
188
|
+
// components independently call getCurrentUser() at once. Does not cover
|
|
189
|
+
// two separate browser TABS refreshing at once (each has its own module
|
|
190
|
+
// state) -- that's a rarer trigger and would need cross-tab coordination
|
|
191
|
+
// this fix doesn't attempt.
|
|
192
|
+
let refreshInFlight: Promise<AppUser | null> | null = null;
|
|
193
|
+
|
|
194
|
+
async function refreshCurrentSession(sessionId: string): Promise<AppUser | null> {
|
|
195
|
+
const result = await callAppAuth<SessionResponse>("/session/refresh", {
|
|
196
|
+
method: "POST",
|
|
197
|
+
body: JSON.stringify({ session_id: sessionId }),
|
|
198
|
+
});
|
|
199
|
+
if (!result.ok) {
|
|
200
|
+
saveStoredSession(null);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
saveStoredSession(result.body);
|
|
204
|
+
return result.body.user;
|
|
205
|
+
}
|
|
206
|
+
|
|
174
207
|
// The one function generated app code is expected to call on every page
|
|
175
208
|
// load to find out "is anyone logged in" -- returns null if there's no
|
|
176
209
|
// session, or if the session has expired/been revoked server-side (in
|
|
@@ -183,16 +216,12 @@ export async function getCurrentUser(): Promise<AppUser | null> {
|
|
|
183
216
|
const needsRefresh = exp == null || exp - Math.floor(Date.now() / 1000) < 120;
|
|
184
217
|
if (!needsRefresh) return stored.user;
|
|
185
218
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (!result.ok) {
|
|
191
|
-
saveStoredSession(null);
|
|
192
|
-
return null;
|
|
219
|
+
if (!refreshInFlight) {
|
|
220
|
+
refreshInFlight = refreshCurrentSession(stored.sessionId).finally(() => {
|
|
221
|
+
refreshInFlight = null;
|
|
222
|
+
});
|
|
193
223
|
}
|
|
194
|
-
|
|
195
|
-
return result.body.user;
|
|
224
|
+
return refreshInFlight;
|
|
196
225
|
}
|
|
197
226
|
|
|
198
227
|
export async function requestPasswordReset(email: string): Promise<ActionResult> {
|
package/src/badge.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Small fixed "Powered by ViibeStack" badge every generated app is required
|
|
4
|
+
// to render once near its root layout (see integrationRequirements() item
|
|
5
|
+
// 11 in services/deploy/src/mcp-agent.ts, and scratch-scaffold.ts's
|
|
6
|
+
// layout.tsx template, which injects this automatically for from-scratch
|
|
7
|
+
// builds). Self-gating: resolves this app's own tenant plan at runtime via
|
|
8
|
+
// GET /api/apps/:id/badge-status and renders nothing once that tenant is
|
|
9
|
+
// actively paying for a plan that waives the badge (Growing Business and
|
|
10
|
+
// above) -- no build-time branching needed, so the same shipped code keeps
|
|
11
|
+
// working correctly if a tenant upgrades/downgrades after the app was
|
|
12
|
+
// already deployed.
|
|
13
|
+
import { useEffect, useState } from "react";
|
|
14
|
+
|
|
15
|
+
let showBadgePromise: Promise<boolean> | null = null;
|
|
16
|
+
|
|
17
|
+
function resolveShowBadge(): Promise<boolean> {
|
|
18
|
+
if (!showBadgePromise) {
|
|
19
|
+
showBadgePromise = fetch(`${window.location.origin}/api/apps/resolve?hostname=${window.location.hostname}`)
|
|
20
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
21
|
+
.then((data: { app_id?: string } | null) => {
|
|
22
|
+
if (!data?.app_id) return true;
|
|
23
|
+
return fetch(`${window.location.origin}/api/apps/${data.app_id}/badge-status`)
|
|
24
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
25
|
+
.then((status: { show_badge?: boolean } | null) => status?.show_badge ?? true);
|
|
26
|
+
})
|
|
27
|
+
.catch(() => true);
|
|
28
|
+
}
|
|
29
|
+
return showBadgePromise;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function PoweredByBadge() {
|
|
33
|
+
const [show, setShow] = useState(false);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
let cancelled = false;
|
|
37
|
+
resolveShowBadge().then((result) => {
|
|
38
|
+
if (!cancelled) setShow(result);
|
|
39
|
+
});
|
|
40
|
+
return () => {
|
|
41
|
+
cancelled = true;
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
if (!show) return null;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<a
|
|
49
|
+
href="https://viibestack.ai?utm_source=powered_by_badge"
|
|
50
|
+
target="_blank"
|
|
51
|
+
rel="noopener noreferrer"
|
|
52
|
+
style={{
|
|
53
|
+
position: "fixed",
|
|
54
|
+
bottom: "1rem",
|
|
55
|
+
right: "1rem",
|
|
56
|
+
zIndex: 9999,
|
|
57
|
+
display: "flex",
|
|
58
|
+
alignItems: "center",
|
|
59
|
+
gap: "0.375rem",
|
|
60
|
+
padding: "0.375rem 0.75rem",
|
|
61
|
+
borderRadius: "9999px",
|
|
62
|
+
backgroundColor: "rgba(17, 17, 17, 0.85)",
|
|
63
|
+
color: "#fff",
|
|
64
|
+
fontSize: "0.75rem",
|
|
65
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
66
|
+
fontWeight: 500,
|
|
67
|
+
textDecoration: "none",
|
|
68
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.2)",
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
Powered by ViibeStack
|
|
72
|
+
</a>
|
|
73
|
+
);
|
|
74
|
+
}
|
package/src/index.ts
CHANGED