@basictech/react 0.8.0-beta.1 → 0.8.0-beta.3
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 +12 -0
- package/dist/index.d.mts +59 -43
- package/dist/index.d.ts +59 -43
- package/dist/index.js +1015 -200
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1006 -193
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- package/readme.md +33 -0
- package/src/AuthContext.tsx +157 -177
- package/src/context.tsx +104 -0
- package/src/core/auth/AuthManager.ts +64 -40
- package/src/dev/BasicDevToolbar.tsx +665 -0
- package/src/index.ts +3 -2
- package/src/sync/syncProtocol.js +30 -0
- package/src/utils/network.ts +69 -16
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.3",
|
|
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
|
|
@@ -319,8 +268,12 @@ export function BasicProvider({
|
|
|
319
268
|
debug: debug,
|
|
320
269
|
onAuthError: (error) => {
|
|
321
270
|
log('RemoteDB auth error:', error)
|
|
271
|
+
if (error.errorType === 'forbidden') {
|
|
272
|
+
log('403 Forbidden - user lacks required scope, not signing out')
|
|
273
|
+
return
|
|
274
|
+
}
|
|
322
275
|
handleSignOut()
|
|
323
|
-
}
|
|
276
|
+
},
|
|
324
277
|
})
|
|
325
278
|
setDbStatus(DBStatus.ONLINE)
|
|
326
279
|
setIsReady(true)
|
|
@@ -333,19 +286,35 @@ export function BasicProvider({
|
|
|
333
286
|
if (!result.isValid) {
|
|
334
287
|
let errorMessage = ''
|
|
335
288
|
if (result.errors) {
|
|
336
|
-
result.errors.forEach((
|
|
337
|
-
errorMessage += `${index + 1}: ${
|
|
289
|
+
result.errors.forEach((err: any, index: number) => {
|
|
290
|
+
errorMessage += `${index + 1}: ${err.message} - at ${err.instancePath}\n`
|
|
338
291
|
})
|
|
339
292
|
}
|
|
293
|
+
setSchemaDevInfo({
|
|
294
|
+
projectId: schema?.project_id ?? null,
|
|
295
|
+
localVersion: schema?.version,
|
|
296
|
+
status: 'invalid',
|
|
297
|
+
valid: false,
|
|
298
|
+
lastCheckedAt: Date.now(),
|
|
299
|
+
error: errorMessage.trim() || undefined,
|
|
300
|
+
})
|
|
340
301
|
setError({
|
|
341
302
|
code: 'schema_invalid',
|
|
342
303
|
title: 'Basic Schema is invalid!',
|
|
343
|
-
message: errorMessage
|
|
304
|
+
message: errorMessage,
|
|
344
305
|
})
|
|
345
306
|
setIsReady(true)
|
|
346
307
|
return null
|
|
347
308
|
}
|
|
348
309
|
|
|
310
|
+
setSchemaDevInfo({
|
|
311
|
+
projectId: schema?.project_id ?? null,
|
|
312
|
+
localVersion: schema?.version,
|
|
313
|
+
status: result.schemaStatus.status ?? 'unknown',
|
|
314
|
+
valid: result.schemaStatus.valid,
|
|
315
|
+
lastCheckedAt: Date.now(),
|
|
316
|
+
})
|
|
317
|
+
|
|
349
318
|
if (dbMode === 'remote') {
|
|
350
319
|
initRemoteDb()
|
|
351
320
|
} else {
|
|
@@ -367,30 +336,40 @@ export function BasicProvider({
|
|
|
367
336
|
if (schema) {
|
|
368
337
|
checkSchema()
|
|
369
338
|
} else {
|
|
339
|
+
setSchemaDevInfo(
|
|
340
|
+
project_id
|
|
341
|
+
? {
|
|
342
|
+
projectId: project_id,
|
|
343
|
+
localVersion: undefined,
|
|
344
|
+
status: 'no_schema',
|
|
345
|
+
valid: false,
|
|
346
|
+
lastCheckedAt: Date.now(),
|
|
347
|
+
}
|
|
348
|
+
: null,
|
|
349
|
+
)
|
|
370
350
|
if (dbMode === 'remote' && project_id) {
|
|
371
351
|
initRemoteDb()
|
|
372
352
|
} else {
|
|
373
353
|
setIsReady(true)
|
|
374
354
|
}
|
|
375
355
|
}
|
|
376
|
-
}, [])
|
|
356
|
+
}, [])
|
|
377
357
|
|
|
378
|
-
// --- Connect sync DB when auth is ready ---
|
|
379
358
|
useEffect(() => {
|
|
380
359
|
if (authState.hasToken && syncRef.current && authState.isSignedIn && shouldConnect) {
|
|
381
360
|
log('connecting to db...')
|
|
382
361
|
|
|
383
|
-
syncRef.current
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
362
|
+
syncRef.current
|
|
363
|
+
?.connect({
|
|
364
|
+
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
365
|
+
ws_url: authConfig.ws_url,
|
|
366
|
+
})
|
|
387
367
|
.catch((e: any) => {
|
|
388
368
|
log('error connecting to db', e)
|
|
389
369
|
})
|
|
390
370
|
}
|
|
391
371
|
}, [authState.isSignedIn, authState.hasToken, shouldConnect])
|
|
392
372
|
|
|
393
|
-
// --- Sign out (auth cleanup + sync teardown) ---
|
|
394
373
|
const handleSignOut = async () => {
|
|
395
374
|
await authRef.current.signOut()
|
|
396
375
|
if (syncRef.current) {
|
|
@@ -405,7 +384,6 @@ export function BasicProvider({
|
|
|
405
384
|
}
|
|
406
385
|
}
|
|
407
386
|
|
|
408
|
-
// --- Sign in wrappers (add dev-mode error display) ---
|
|
409
387
|
const handleSignIn = async () => {
|
|
410
388
|
try {
|
|
411
389
|
await authRef.current.signIn()
|
|
@@ -414,7 +392,8 @@ export function BasicProvider({
|
|
|
414
392
|
setError({
|
|
415
393
|
code: 'signin_error',
|
|
416
394
|
title: 'Sign-in Failed',
|
|
417
|
-
message:
|
|
395
|
+
message:
|
|
396
|
+
(error as Error).message || 'An error occurred during sign-in. Please try again.',
|
|
418
397
|
})
|
|
419
398
|
}
|
|
420
399
|
throw error
|
|
@@ -429,14 +408,14 @@ export function BasicProvider({
|
|
|
429
408
|
setError({
|
|
430
409
|
code: 'signin_error',
|
|
431
410
|
title: 'Sign-in Failed',
|
|
432
|
-
message:
|
|
411
|
+
message:
|
|
412
|
+
(error as Error).message || 'An error occurred during sign-in. Please try again.',
|
|
433
413
|
})
|
|
434
414
|
}
|
|
435
415
|
throw error
|
|
436
416
|
}
|
|
437
417
|
}
|
|
438
418
|
|
|
439
|
-
// --- DB accessor ---
|
|
440
419
|
const getCurrentDb = (): BasicDB => {
|
|
441
420
|
if (dbMode === 'remote') {
|
|
442
421
|
return remoteDbRef.current || noDb
|
|
@@ -444,33 +423,30 @@ export function BasicProvider({
|
|
|
444
423
|
return syncRef.current || noDb
|
|
445
424
|
}
|
|
446
425
|
|
|
447
|
-
|
|
448
|
-
const contextValue: BasicContextType = {
|
|
449
|
-
// Auth state
|
|
426
|
+
const contextValue = {
|
|
450
427
|
isReady: authState.isAuthReady,
|
|
451
428
|
isSignedIn: authState.isSignedIn,
|
|
452
429
|
user: authState.user,
|
|
453
430
|
did: authState.did,
|
|
454
431
|
scope: authState.tokenScope,
|
|
455
|
-
hasScope: (
|
|
432
|
+
hasScope: (s: string) => authRef.current.hasScope(s),
|
|
456
433
|
missingScopes: () => authRef.current.missingScopes(),
|
|
457
434
|
|
|
458
|
-
// Auth actions
|
|
459
435
|
signIn: handleSignIn,
|
|
460
436
|
signInWithHandle: handleSignInWithHandle,
|
|
461
437
|
signOut: handleSignOut,
|
|
462
438
|
signInWithCode: (code: string, state?: string) => authRef.current.signInWithCode(code, state),
|
|
463
439
|
|
|
464
|
-
// Token management
|
|
465
440
|
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
466
441
|
getSignInUrl: (redirectUri?: string) => authRef.current.getSignInUrl(redirectUri),
|
|
467
442
|
|
|
468
|
-
// DB access
|
|
469
443
|
db: getCurrentDb(),
|
|
470
444
|
dbStatus,
|
|
471
445
|
dbMode,
|
|
472
446
|
|
|
473
|
-
|
|
447
|
+
devInfo: schemaDevInfo,
|
|
448
|
+
refreshSchemaStatus,
|
|
449
|
+
|
|
474
450
|
isAuthReady: authState.isAuthReady,
|
|
475
451
|
signin: handleSignIn,
|
|
476
452
|
signout: handleSignOut,
|
|
@@ -481,33 +457,37 @@ export function BasicProvider({
|
|
|
481
457
|
return (
|
|
482
458
|
<BasicContext.Provider value={contextValue}>
|
|
483
459
|
{error && isDevMode() && <ErrorDisplay error={error} />}
|
|
460
|
+
{devToolbar && isDevMode() && (
|
|
461
|
+
<Suspense fallback={null}>
|
|
462
|
+
<BasicDevToolbar debug={debug} />
|
|
463
|
+
</Suspense>
|
|
464
|
+
)}
|
|
484
465
|
{isReady && children}
|
|
485
466
|
</BasicContext.Provider>
|
|
486
467
|
)
|
|
487
468
|
}
|
|
488
469
|
|
|
489
470
|
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);
|
|
471
|
+
return (
|
|
472
|
+
<div
|
|
473
|
+
style={{
|
|
474
|
+
position: 'absolute',
|
|
475
|
+
top: 20,
|
|
476
|
+
left: 20,
|
|
477
|
+
color: 'black',
|
|
478
|
+
backgroundColor: '#f8d7da',
|
|
479
|
+
border: '1px solid #f5c6cb',
|
|
480
|
+
borderRadius: '4px',
|
|
481
|
+
padding: '20px',
|
|
482
|
+
maxWidth: '400px',
|
|
483
|
+
margin: '20px auto',
|
|
484
|
+
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
|
485
|
+
fontFamily: 'monospace',
|
|
486
|
+
}}
|
|
487
|
+
>
|
|
488
|
+
<h3 style={{ fontSize: '0.8rem', opacity: 0.8 }}>code: {error.code}</h3>
|
|
489
|
+
<h1 style={{ fontSize: '1.2rem', lineHeight: 1.5 }}>{error.title}</h1>
|
|
490
|
+
<p>{error.message}</p>
|
|
491
|
+
</div>
|
|
492
|
+
)
|
|
513
493
|
}
|