@basictech/react 0.8.0-beta.1 → 0.8.0-beta.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/.turbo/turbo-build.log +10 -10
- package/changelog.md +6 -0
- package/dist/index.d.mts +59 -43
- package/dist/index.d.ts +59 -43
- package/dist/index.js +928 -161
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +919 -154
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- package/readme.md +33 -0
- package/src/AuthContext.tsx +153 -177
- package/src/context.tsx +104 -0
- package/src/dev/BasicDevToolbar.tsx +665 -0
- package/src/index.ts +3 -2
- package/src/utils/network.ts +68 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basictech/react",
|
|
3
|
-
"version": "0.8.0-beta.
|
|
3
|
+
"version": "0.8.0-beta.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsup",
|
|
14
14
|
"dev": "tsup --watch",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
15
16
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
16
17
|
},
|
|
17
18
|
"author": "",
|
|
18
19
|
"license": "ISC",
|
|
19
20
|
"dependencies": {
|
|
21
|
+
"semver": "^7.7.2",
|
|
20
22
|
"ajv": "^8.17.1",
|
|
21
23
|
"dexie": "^4.2.1",
|
|
22
24
|
"dexie-observable": "^4.0.1-beta.13",
|
package/readme.md
CHANGED
|
@@ -99,6 +99,7 @@ Root provider component. Must wrap your entire app.
|
|
|
99
99
|
schema={schema} // Required: Your Basic schema
|
|
100
100
|
debug={false} // Optional: Enable console logging
|
|
101
101
|
dbMode="sync" // Optional: "sync" (default) or "remote"
|
|
102
|
+
devToolbar={false} // Optional: Floating dev status bar (localhost / dev / debug)
|
|
102
103
|
/>
|
|
103
104
|
```
|
|
104
105
|
|
|
@@ -109,6 +110,7 @@ Root provider component. Must wrap your entire app.
|
|
|
109
110
|
| `schema` | `object` | required | Schema with `project_id` and `tables` |
|
|
110
111
|
| `debug` | `boolean` | `false` | Enable debug logging |
|
|
111
112
|
| `dbMode` | `"sync" \| "remote"` | `"sync"` | Database mode |
|
|
113
|
+
| `devToolbar` | `boolean` | `false` | Show the Basic dev toolbar (only when `localhost`, `NODE_ENV === "development"`, or `debug={true}`) |
|
|
112
114
|
|
|
113
115
|
#### Database Modes
|
|
114
116
|
|
|
@@ -139,6 +141,10 @@ const {
|
|
|
139
141
|
db, // Database instance
|
|
140
142
|
dbStatus, // DBStatus - see below
|
|
141
143
|
dbMode, // "sync" | "remote"
|
|
144
|
+
|
|
145
|
+
// Dev / schema snapshot (for custom tooling)
|
|
146
|
+
devInfo, // BasicSchemaDevInfo | null — local vs remote schema status
|
|
147
|
+
refreshSchemaStatus, // () => Promise<void> — re-fetch schema status from API
|
|
142
148
|
} = useBasic()
|
|
143
149
|
```
|
|
144
150
|
|
|
@@ -160,6 +166,33 @@ Import the enum for comparisons: `import { useBasic, DBStatus } from '@basictech
|
|
|
160
166
|
|
|
161
167
|
---
|
|
162
168
|
|
|
169
|
+
### Development toolbar
|
|
170
|
+
|
|
171
|
+
A small floating bar (similar in spirit to Next.js dev indicators) shows **auth**, **database mode**, **sync status**, and **schema vs server** health. It is **opt-in** and only appears in development: `localhost` / `127.0.0.1` / `.local`, `NODE_ENV === "development"`, or when `debug` is `true` on the provider or on the standalone component.
|
|
172
|
+
|
|
173
|
+
**Option A — provider flag**
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
<BasicProvider schema={schema} devToolbar debug>
|
|
177
|
+
<App />
|
|
178
|
+
</BasicProvider>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Option B — place the component yourself** (must be under `BasicProvider`; respects the same visibility rules, or pass `debug` to force):
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import { BasicDevToolbar } from '@basictech/react'
|
|
185
|
+
|
|
186
|
+
<BasicProvider schema={schema}>
|
|
187
|
+
<App />
|
|
188
|
+
<BasicDevToolbar />
|
|
189
|
+
</BasicProvider>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The expanded panel includes **Refresh schema** (re-runs the remote schema check) and **Copy debug info** (JSON snapshot **without** raw access tokens). You can also read `devInfo` and call `refreshSchemaStatus()` from `useBasic()` for your own UI.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
163
196
|
### `useQuery()`
|
|
164
197
|
|
|
165
198
|
Live query hook - automatically re-renders when data changes.
|
package/src/AuthContext.tsx
CHANGED
|
@@ -1,165 +1,75 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState, Suspense, lazy } from 'react'
|
|
2
2
|
|
|
3
3
|
import { BasicSync, initDexieExtensions } from './sync'
|
|
4
4
|
import { RemoteDB, DBMode, BasicDB } from './core/db'
|
|
5
5
|
import { AuthManager } from './core/auth/AuthManager'
|
|
6
|
-
import type {
|
|
6
|
+
import type { User, AuthResult, GetTokenOptions, PdsEndpoints } from './core/auth/AuthManager'
|
|
7
7
|
|
|
8
8
|
import { log } from './config'
|
|
9
9
|
import { version as currentVersion } from '../package.json'
|
|
10
10
|
import { createVersionUpdater } from './updater/versionUpdater'
|
|
11
11
|
import { getMigrations } from './updater/updateMigrations'
|
|
12
|
-
import { BasicStorage, LocalStorageAdapter
|
|
12
|
+
import { BasicStorage, LocalStorageAdapter } from './utils/storage'
|
|
13
13
|
import { isDevelopment, checkForNewVersion, getSyncStatus } from './utils/network'
|
|
14
14
|
import { validateAndCheckSchema } from './utils/schema'
|
|
15
|
+
import { BasicContext, DBStatus, noDb, type BasicSchemaDevInfo } from './context'
|
|
16
|
+
|
|
17
|
+
const BasicDevToolbar = lazy(() =>
|
|
18
|
+
import('./dev/BasicDevToolbar').then((m) => ({ default: m.BasicDevToolbar }))
|
|
19
|
+
)
|
|
15
20
|
|
|
16
21
|
export type { BasicStorage, LocalStorageAdapter } from './utils/storage'
|
|
17
22
|
export type { DBMode, BasicDB, Collection } from './core/db'
|
|
18
|
-
export type { Token, User, AuthResult, GetTokenOptions, PdsEndpoints }
|
|
23
|
+
export type { Token, User, AuthResult, GetTokenOptions, PdsEndpoints } from './core/auth/AuthManager'
|
|
24
|
+
export type { BasicContextType, BasicSchemaDevInfo } from './context'
|
|
25
|
+
export { DBStatus, useBasic, BasicContext } from './context'
|
|
19
26
|
|
|
20
27
|
export type AuthConfig = {
|
|
21
|
-
scopes?: string | string[]
|
|
28
|
+
scopes?: string | string[]
|
|
22
29
|
/** @deprecated Use pds_url instead */
|
|
23
|
-
server_url?: string
|
|
30
|
+
server_url?: string
|
|
24
31
|
/** PDS URL for auth and data (default: https://pds.basic.id) */
|
|
25
|
-
pds_url?: string
|
|
32
|
+
pds_url?: string
|
|
26
33
|
/** Admin server URL for connect reporting (default: https://api.basic.tech) */
|
|
27
|
-
admin_url?: string
|
|
28
|
-
ws_url?: string
|
|
34
|
+
admin_url?: string
|
|
35
|
+
ws_url?: string
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export type BasicProviderProps = {
|
|
32
|
-
children: React.ReactNode
|
|
33
|
-
/**
|
|
34
|
-
* @deprecated Project ID is now extracted from schema.project_id.
|
|
39
|
+
children: React.ReactNode
|
|
40
|
+
/**
|
|
41
|
+
* @deprecated Project ID is now extracted from schema.project_id.
|
|
35
42
|
* This prop is kept for backward compatibility but can be omitted.
|
|
36
43
|
*/
|
|
37
|
-
project_id?: string
|
|
44
|
+
project_id?: string
|
|
38
45
|
/** The Basic schema object containing project_id and table definitions */
|
|
39
|
-
schema?: any
|
|
40
|
-
debug?: boolean
|
|
41
|
-
storage?: BasicStorage
|
|
42
|
-
auth?: AuthConfig
|
|
46
|
+
schema?: any
|
|
47
|
+
debug?: boolean
|
|
48
|
+
storage?: BasicStorage
|
|
49
|
+
auth?: AuthConfig
|
|
43
50
|
/**
|
|
44
51
|
* Database mode - determines which implementation is used
|
|
45
52
|
* - 'sync': Uses Dexie + WebSocket for local-first sync (default)
|
|
46
53
|
* - 'remote': Uses REST API calls directly to server
|
|
47
54
|
*/
|
|
48
|
-
dbMode?: DBMode
|
|
55
|
+
dbMode?: DBMode
|
|
56
|
+
/** Show floating dev toolbar (localhost, NODE_ENV=development, or debug=true). */
|
|
57
|
+
devToolbar?: boolean
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
const DEFAULT_AUTH_CONFIG = {
|
|
52
61
|
scopes: 'profile,email,app:admin',
|
|
53
62
|
pds_url: 'https://pds.basic.id',
|
|
54
63
|
admin_url: 'https://api.basic.tech',
|
|
55
|
-
ws_url: 'wss://pds.basic.id/ws'
|
|
64
|
+
ws_url: 'wss://pds.basic.id/ws',
|
|
56
65
|
} as const
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
export enum DBStatus {
|
|
60
|
-
LOADING = "LOADING",
|
|
61
|
-
OFFLINE = "OFFLINE",
|
|
62
|
-
CONNECTING = "CONNECTING",
|
|
63
|
-
ONLINE = "ONLINE",
|
|
64
|
-
SYNCING = "SYNCING",
|
|
65
|
-
ERROR = "ERROR",
|
|
66
|
-
/** Sync reported an error but will retry (e.g. expired token). Used for status code 4 from dexie-syncable. */
|
|
67
|
-
ERROR_WILL_RETRY = "ERROR_WILL_RETRY",
|
|
68
|
-
/** Token expired; the SDK is refreshing and will reconnect automatically. */
|
|
69
|
-
ERROR_TOKEN_EXPIRED = "ERROR_TOKEN_EXPIRED"
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Context type for useBasic hook
|
|
74
|
-
*/
|
|
75
|
-
export type BasicContextType = {
|
|
76
|
-
// Auth state
|
|
77
|
-
isReady: boolean;
|
|
78
|
-
isSignedIn: boolean;
|
|
79
|
-
user: User | null;
|
|
80
|
-
/** The user's DID (Decentralized Identifier), extracted from the access token `sub` claim */
|
|
81
|
-
did: string | null;
|
|
82
|
-
/** Space-separated scope string from the access token */
|
|
83
|
-
scope: string | null;
|
|
84
|
-
/** Check if a specific scope is granted (e.g., hasScope('profile')) */
|
|
85
|
-
hasScope: (scope: string) => boolean;
|
|
86
|
-
/** Returns scopes that were requested but not granted in the current token */
|
|
87
|
-
missingScopes: () => string[];
|
|
88
|
-
|
|
89
|
-
// Auth actions (new camelCase naming)
|
|
90
|
-
signIn: () => Promise<void>;
|
|
91
|
-
signInWithHandle: (handle: string) => Promise<void>;
|
|
92
|
-
signOut: () => Promise<void>;
|
|
93
|
-
signInWithCode: (code: string, state?: string) => Promise<AuthResult>;
|
|
94
|
-
|
|
95
|
-
// Token management
|
|
96
|
-
getToken: (options?: GetTokenOptions) => Promise<string>;
|
|
97
|
-
getSignInUrl: (redirectUri?: string) => Promise<string>;
|
|
98
|
-
|
|
99
|
-
// DB access
|
|
100
|
-
db: BasicDB;
|
|
101
|
-
dbStatus: DBStatus;
|
|
102
|
-
dbMode: DBMode;
|
|
103
|
-
|
|
104
|
-
// Legacy aliases (deprecated - will be removed in future version)
|
|
105
|
-
/** @deprecated Use isReady instead */
|
|
106
|
-
isAuthReady: boolean;
|
|
107
|
-
/** @deprecated Use signIn instead */
|
|
108
|
-
signin: () => Promise<void>;
|
|
109
|
-
/** @deprecated Use signOut instead */
|
|
110
|
-
signout: () => Promise<void>;
|
|
111
|
-
/** @deprecated Use signInWithCode instead */
|
|
112
|
-
signinWithCode: (code: string, state?: string) => Promise<AuthResult>;
|
|
113
|
-
/** @deprecated Use getSignInUrl instead */
|
|
114
|
-
getSignInLink: (redirectUri?: string) => Promise<string>;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const noDb: BasicDB = {
|
|
118
|
-
collection: () => {
|
|
119
|
-
throw new Error('no basicdb found - initialization failed. double check your schema.')
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export const BasicContext = createContext<BasicContextType>({
|
|
124
|
-
// Auth state
|
|
125
|
-
isReady: false,
|
|
126
|
-
isSignedIn: false,
|
|
127
|
-
user: null,
|
|
128
|
-
did: null,
|
|
129
|
-
scope: null,
|
|
130
|
-
hasScope: () => false,
|
|
131
|
-
missingScopes: () => [],
|
|
132
|
-
|
|
133
|
-
// Auth actions
|
|
134
|
-
signIn: () => Promise.resolve(),
|
|
135
|
-
signInWithHandle: () => Promise.resolve(),
|
|
136
|
-
signOut: () => Promise.resolve(),
|
|
137
|
-
signInWithCode: () => Promise.resolve({ success: false }),
|
|
138
|
-
|
|
139
|
-
// Token management
|
|
140
|
-
getToken: (_options?: GetTokenOptions) => Promise.reject(new Error('no token')),
|
|
141
|
-
getSignInUrl: () => Promise.resolve(""),
|
|
142
|
-
|
|
143
|
-
// DB access
|
|
144
|
-
db: noDb,
|
|
145
|
-
dbStatus: DBStatus.LOADING,
|
|
146
|
-
dbMode: 'sync',
|
|
147
|
-
|
|
148
|
-
// Legacy aliases
|
|
149
|
-
isAuthReady: false,
|
|
150
|
-
signin: () => Promise.resolve(),
|
|
151
|
-
signout: () => Promise.resolve(),
|
|
152
|
-
signinWithCode: () => Promise.resolve({ success: false }),
|
|
153
|
-
getSignInLink: () => Promise.resolve("")
|
|
154
|
-
});
|
|
155
|
-
|
|
156
67
|
type ErrorObject = {
|
|
157
|
-
code: string
|
|
158
|
-
title: string
|
|
159
|
-
message: string
|
|
68
|
+
code: string
|
|
69
|
+
title: string
|
|
70
|
+
message: string
|
|
160
71
|
}
|
|
161
72
|
|
|
162
|
-
// Tracks the subset of AuthManager state that React effects depend on.
|
|
163
73
|
type AuthSnapshot = {
|
|
164
74
|
isSignedIn: boolean
|
|
165
75
|
hasToken: boolean
|
|
@@ -187,11 +97,11 @@ export function BasicProvider({
|
|
|
187
97
|
debug = false,
|
|
188
98
|
storage,
|
|
189
99
|
auth,
|
|
190
|
-
dbMode = 'sync'
|
|
100
|
+
dbMode = 'sync',
|
|
101
|
+
devToolbar = false,
|
|
191
102
|
}: BasicProviderProps) {
|
|
192
103
|
const project_id = schema?.project_id || project_id_prop
|
|
193
104
|
|
|
194
|
-
// Merge auth config with defaults (server_url is deprecated in favor of pds_url)
|
|
195
105
|
if (auth?.server_url && !auth?.pds_url) {
|
|
196
106
|
log('Warning: auth.server_url is deprecated, use auth.pds_url instead')
|
|
197
107
|
}
|
|
@@ -199,7 +109,7 @@ export function BasicProvider({
|
|
|
199
109
|
scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
|
|
200
110
|
pds_url: auth?.pds_url || auth?.server_url || DEFAULT_AUTH_CONFIG.pds_url,
|
|
201
111
|
admin_url: auth?.admin_url || DEFAULT_AUTH_CONFIG.admin_url,
|
|
202
|
-
ws_url: auth?.ws_url || DEFAULT_AUTH_CONFIG.ws_url
|
|
112
|
+
ws_url: auth?.ws_url || DEFAULT_AUTH_CONFIG.ws_url,
|
|
203
113
|
}
|
|
204
114
|
|
|
205
115
|
const scopesString = Array.isArray(authConfig.scopes)
|
|
@@ -209,7 +119,9 @@ export function BasicProvider({
|
|
|
209
119
|
const storageRef = useRef<BasicStorage>(storage || new LocalStorageAdapter())
|
|
210
120
|
const storageAdapter = storageRef.current
|
|
211
121
|
|
|
212
|
-
|
|
122
|
+
const schemaRef = useRef(schema)
|
|
123
|
+
schemaRef.current = schema
|
|
124
|
+
|
|
213
125
|
const [authState, setAuthState] = useState<AuthSnapshot>({
|
|
214
126
|
isSignedIn: false,
|
|
215
127
|
hasToken: false,
|
|
@@ -234,19 +146,56 @@ export function BasicProvider({
|
|
|
234
146
|
)
|
|
235
147
|
}
|
|
236
148
|
|
|
237
|
-
// --- DB state (stays in React) ---
|
|
238
149
|
const syncRef = useRef<BasicSync | null>(null)
|
|
239
150
|
const remoteDbRef = useRef<RemoteDB | null>(null)
|
|
240
151
|
const [shouldConnect, setShouldConnect] = useState(false)
|
|
241
152
|
const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.OFFLINE)
|
|
242
153
|
const [isReady, setIsReady] = useState(false)
|
|
243
154
|
const [error, setError] = useState<ErrorObject | null>(null)
|
|
155
|
+
const [schemaDevInfo, setSchemaDevInfo] = useState<BasicSchemaDevInfo | null>(null)
|
|
244
156
|
|
|
245
157
|
const isDevMode = () => isDevelopment(debug)
|
|
246
158
|
|
|
247
|
-
|
|
159
|
+
const refreshSchemaStatus = useCallback(async () => {
|
|
160
|
+
const s = schemaRef.current
|
|
161
|
+
if (!s) {
|
|
162
|
+
setSchemaDevInfo(
|
|
163
|
+
project_id
|
|
164
|
+
? {
|
|
165
|
+
projectId: project_id,
|
|
166
|
+
localVersion: undefined,
|
|
167
|
+
status: 'no_schema',
|
|
168
|
+
valid: false,
|
|
169
|
+
lastCheckedAt: Date.now(),
|
|
170
|
+
}
|
|
171
|
+
: null,
|
|
172
|
+
)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
const result = await validateAndCheckSchema(s)
|
|
176
|
+
if (!result.isValid) {
|
|
177
|
+
const errText =
|
|
178
|
+
result.errors?.map((e: { message?: string }) => e.message || '').join('; ') || 'invalid'
|
|
179
|
+
setSchemaDevInfo({
|
|
180
|
+
projectId: s.project_id ?? null,
|
|
181
|
+
localVersion: s.version,
|
|
182
|
+
status: 'invalid',
|
|
183
|
+
valid: false,
|
|
184
|
+
lastCheckedAt: Date.now(),
|
|
185
|
+
error: errText,
|
|
186
|
+
})
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
setSchemaDevInfo({
|
|
190
|
+
projectId: s.project_id ?? null,
|
|
191
|
+
localVersion: s.version,
|
|
192
|
+
status: result.schemaStatus.status ?? 'unknown',
|
|
193
|
+
valid: result.schemaStatus.valid,
|
|
194
|
+
lastCheckedAt: Date.now(),
|
|
195
|
+
})
|
|
196
|
+
}, [project_id])
|
|
197
|
+
|
|
248
198
|
useEffect(() => {
|
|
249
|
-
// Version updater (SDK migration, not auth-related)
|
|
250
199
|
const runVersionUpdater = async () => {
|
|
251
200
|
try {
|
|
252
201
|
const versionUpdater = createVersionUpdater(storageAdapter, currentVersion, getMigrations())
|
|
@@ -268,7 +217,6 @@ export function BasicProvider({
|
|
|
268
217
|
return authRef.current.setupNetworkListeners()
|
|
269
218
|
}, [])
|
|
270
219
|
|
|
271
|
-
// --- DB init (separate mount effect) ---
|
|
272
220
|
useEffect(() => {
|
|
273
221
|
async function initSyncDb(options: { shouldConnect: boolean }) {
|
|
274
222
|
if (!syncRef.current) {
|
|
@@ -276,7 +224,7 @@ export function BasicProvider({
|
|
|
276
224
|
|
|
277
225
|
await initDexieExtensions()
|
|
278
226
|
|
|
279
|
-
syncRef.current = new BasicSync('basicdb', { schema: schema })
|
|
227
|
+
syncRef.current = new BasicSync('basicdb', { schema: schema })
|
|
280
228
|
|
|
281
229
|
syncRef.current.syncable.on('statusChanged', (status: number) => {
|
|
282
230
|
const newStatus = getSyncStatus(status) as DBStatus
|
|
@@ -304,7 +252,8 @@ export function BasicProvider({
|
|
|
304
252
|
setError({
|
|
305
253
|
code: 'missing_project_id',
|
|
306
254
|
title: 'Project ID Required',
|
|
307
|
-
message:
|
|
255
|
+
message:
|
|
256
|
+
'Remote mode requires a project_id. Provide it via schema.project_id or the project_id prop.',
|
|
308
257
|
})
|
|
309
258
|
setIsReady(true)
|
|
310
259
|
return
|
|
@@ -320,7 +269,7 @@ export function BasicProvider({
|
|
|
320
269
|
onAuthError: (error) => {
|
|
321
270
|
log('RemoteDB auth error:', error)
|
|
322
271
|
handleSignOut()
|
|
323
|
-
}
|
|
272
|
+
},
|
|
324
273
|
})
|
|
325
274
|
setDbStatus(DBStatus.ONLINE)
|
|
326
275
|
setIsReady(true)
|
|
@@ -333,19 +282,35 @@ export function BasicProvider({
|
|
|
333
282
|
if (!result.isValid) {
|
|
334
283
|
let errorMessage = ''
|
|
335
284
|
if (result.errors) {
|
|
336
|
-
result.errors.forEach((
|
|
337
|
-
errorMessage += `${index + 1}: ${
|
|
285
|
+
result.errors.forEach((err: any, index: number) => {
|
|
286
|
+
errorMessage += `${index + 1}: ${err.message} - at ${err.instancePath}\n`
|
|
338
287
|
})
|
|
339
288
|
}
|
|
289
|
+
setSchemaDevInfo({
|
|
290
|
+
projectId: schema?.project_id ?? null,
|
|
291
|
+
localVersion: schema?.version,
|
|
292
|
+
status: 'invalid',
|
|
293
|
+
valid: false,
|
|
294
|
+
lastCheckedAt: Date.now(),
|
|
295
|
+
error: errorMessage.trim() || undefined,
|
|
296
|
+
})
|
|
340
297
|
setError({
|
|
341
298
|
code: 'schema_invalid',
|
|
342
299
|
title: 'Basic Schema is invalid!',
|
|
343
|
-
message: errorMessage
|
|
300
|
+
message: errorMessage,
|
|
344
301
|
})
|
|
345
302
|
setIsReady(true)
|
|
346
303
|
return null
|
|
347
304
|
}
|
|
348
305
|
|
|
306
|
+
setSchemaDevInfo({
|
|
307
|
+
projectId: schema?.project_id ?? null,
|
|
308
|
+
localVersion: schema?.version,
|
|
309
|
+
status: result.schemaStatus.status ?? 'unknown',
|
|
310
|
+
valid: result.schemaStatus.valid,
|
|
311
|
+
lastCheckedAt: Date.now(),
|
|
312
|
+
})
|
|
313
|
+
|
|
349
314
|
if (dbMode === 'remote') {
|
|
350
315
|
initRemoteDb()
|
|
351
316
|
} else {
|
|
@@ -367,30 +332,40 @@ export function BasicProvider({
|
|
|
367
332
|
if (schema) {
|
|
368
333
|
checkSchema()
|
|
369
334
|
} else {
|
|
335
|
+
setSchemaDevInfo(
|
|
336
|
+
project_id
|
|
337
|
+
? {
|
|
338
|
+
projectId: project_id,
|
|
339
|
+
localVersion: undefined,
|
|
340
|
+
status: 'no_schema',
|
|
341
|
+
valid: false,
|
|
342
|
+
lastCheckedAt: Date.now(),
|
|
343
|
+
}
|
|
344
|
+
: null,
|
|
345
|
+
)
|
|
370
346
|
if (dbMode === 'remote' && project_id) {
|
|
371
347
|
initRemoteDb()
|
|
372
348
|
} else {
|
|
373
349
|
setIsReady(true)
|
|
374
350
|
}
|
|
375
351
|
}
|
|
376
|
-
}, [])
|
|
352
|
+
}, [])
|
|
377
353
|
|
|
378
|
-
// --- Connect sync DB when auth is ready ---
|
|
379
354
|
useEffect(() => {
|
|
380
355
|
if (authState.hasToken && syncRef.current && authState.isSignedIn && shouldConnect) {
|
|
381
356
|
log('connecting to db...')
|
|
382
357
|
|
|
383
|
-
syncRef.current
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
358
|
+
syncRef.current
|
|
359
|
+
?.connect({
|
|
360
|
+
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
361
|
+
ws_url: authConfig.ws_url,
|
|
362
|
+
})
|
|
387
363
|
.catch((e: any) => {
|
|
388
364
|
log('error connecting to db', e)
|
|
389
365
|
})
|
|
390
366
|
}
|
|
391
367
|
}, [authState.isSignedIn, authState.hasToken, shouldConnect])
|
|
392
368
|
|
|
393
|
-
// --- Sign out (auth cleanup + sync teardown) ---
|
|
394
369
|
const handleSignOut = async () => {
|
|
395
370
|
await authRef.current.signOut()
|
|
396
371
|
if (syncRef.current) {
|
|
@@ -405,7 +380,6 @@ export function BasicProvider({
|
|
|
405
380
|
}
|
|
406
381
|
}
|
|
407
382
|
|
|
408
|
-
// --- Sign in wrappers (add dev-mode error display) ---
|
|
409
383
|
const handleSignIn = async () => {
|
|
410
384
|
try {
|
|
411
385
|
await authRef.current.signIn()
|
|
@@ -414,7 +388,8 @@ export function BasicProvider({
|
|
|
414
388
|
setError({
|
|
415
389
|
code: 'signin_error',
|
|
416
390
|
title: 'Sign-in Failed',
|
|
417
|
-
message:
|
|
391
|
+
message:
|
|
392
|
+
(error as Error).message || 'An error occurred during sign-in. Please try again.',
|
|
418
393
|
})
|
|
419
394
|
}
|
|
420
395
|
throw error
|
|
@@ -429,14 +404,14 @@ export function BasicProvider({
|
|
|
429
404
|
setError({
|
|
430
405
|
code: 'signin_error',
|
|
431
406
|
title: 'Sign-in Failed',
|
|
432
|
-
message:
|
|
407
|
+
message:
|
|
408
|
+
(error as Error).message || 'An error occurred during sign-in. Please try again.',
|
|
433
409
|
})
|
|
434
410
|
}
|
|
435
411
|
throw error
|
|
436
412
|
}
|
|
437
413
|
}
|
|
438
414
|
|
|
439
|
-
// --- DB accessor ---
|
|
440
415
|
const getCurrentDb = (): BasicDB => {
|
|
441
416
|
if (dbMode === 'remote') {
|
|
442
417
|
return remoteDbRef.current || noDb
|
|
@@ -444,33 +419,30 @@ export function BasicProvider({
|
|
|
444
419
|
return syncRef.current || noDb
|
|
445
420
|
}
|
|
446
421
|
|
|
447
|
-
|
|
448
|
-
const contextValue: BasicContextType = {
|
|
449
|
-
// Auth state
|
|
422
|
+
const contextValue = {
|
|
450
423
|
isReady: authState.isAuthReady,
|
|
451
424
|
isSignedIn: authState.isSignedIn,
|
|
452
425
|
user: authState.user,
|
|
453
426
|
did: authState.did,
|
|
454
427
|
scope: authState.tokenScope,
|
|
455
|
-
hasScope: (
|
|
428
|
+
hasScope: (s: string) => authRef.current.hasScope(s),
|
|
456
429
|
missingScopes: () => authRef.current.missingScopes(),
|
|
457
430
|
|
|
458
|
-
// Auth actions
|
|
459
431
|
signIn: handleSignIn,
|
|
460
432
|
signInWithHandle: handleSignInWithHandle,
|
|
461
433
|
signOut: handleSignOut,
|
|
462
434
|
signInWithCode: (code: string, state?: string) => authRef.current.signInWithCode(code, state),
|
|
463
435
|
|
|
464
|
-
// Token management
|
|
465
436
|
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
466
437
|
getSignInUrl: (redirectUri?: string) => authRef.current.getSignInUrl(redirectUri),
|
|
467
438
|
|
|
468
|
-
// DB access
|
|
469
439
|
db: getCurrentDb(),
|
|
470
440
|
dbStatus,
|
|
471
441
|
dbMode,
|
|
472
442
|
|
|
473
|
-
|
|
443
|
+
devInfo: schemaDevInfo,
|
|
444
|
+
refreshSchemaStatus,
|
|
445
|
+
|
|
474
446
|
isAuthReady: authState.isAuthReady,
|
|
475
447
|
signin: handleSignIn,
|
|
476
448
|
signout: handleSignOut,
|
|
@@ -481,33 +453,37 @@ export function BasicProvider({
|
|
|
481
453
|
return (
|
|
482
454
|
<BasicContext.Provider value={contextValue}>
|
|
483
455
|
{error && isDevMode() && <ErrorDisplay error={error} />}
|
|
456
|
+
{devToolbar && isDevMode() && (
|
|
457
|
+
<Suspense fallback={null}>
|
|
458
|
+
<BasicDevToolbar debug={debug} />
|
|
459
|
+
</Suspense>
|
|
460
|
+
)}
|
|
484
461
|
{isReady && children}
|
|
485
462
|
</BasicContext.Provider>
|
|
486
463
|
)
|
|
487
464
|
}
|
|
488
465
|
|
|
489
466
|
function ErrorDisplay({ error }: { error: ErrorObject }) {
|
|
490
|
-
return
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
return useContext(BasicContext);
|
|
467
|
+
return (
|
|
468
|
+
<div
|
|
469
|
+
style={{
|
|
470
|
+
position: 'absolute',
|
|
471
|
+
top: 20,
|
|
472
|
+
left: 20,
|
|
473
|
+
color: 'black',
|
|
474
|
+
backgroundColor: '#f8d7da',
|
|
475
|
+
border: '1px solid #f5c6cb',
|
|
476
|
+
borderRadius: '4px',
|
|
477
|
+
padding: '20px',
|
|
478
|
+
maxWidth: '400px',
|
|
479
|
+
margin: '20px auto',
|
|
480
|
+
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
|
481
|
+
fontFamily: 'monospace',
|
|
482
|
+
}}
|
|
483
|
+
>
|
|
484
|
+
<h3 style={{ fontSize: '0.8rem', opacity: 0.8 }}>code: {error.code}</h3>
|
|
485
|
+
<h1 style={{ fontSize: '1.2rem', lineHeight: 1.5 }}>{error.title}</h1>
|
|
486
|
+
<p>{error.message}</p>
|
|
487
|
+
</div>
|
|
488
|
+
)
|
|
513
489
|
}
|