@basictech/react 0.8.0-beta.4 → 0.9.0-beta.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/changelog.md +65 -0
- package/dist/index.d.mts +1258 -285
- package/dist/index.d.ts +1258 -285
- package/dist/index.js +2898 -1425
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2858 -1418
- package/dist/index.mjs.map +1 -1
- package/package.json +27 -8
- package/readme.md +331 -214
- package/.turbo/turbo-build.log +0 -24
- package/AUTH_IMPLEMENTATION_GUIDE.md +0 -2011
- package/src/AuthContext.tsx +0 -591
- package/src/config.ts +0 -9
- package/src/context.tsx +0 -122
- package/src/core/auth/AuthManager.ts +0 -1371
- package/src/core/db/RemoteCollection.ts +0 -308
- package/src/core/db/RemoteDB.ts +0 -40
- package/src/core/db/index.ts +0 -7
- package/src/core/db/types.ts +0 -140
- package/src/dev/BasicDevToolbar.tsx +0 -665
- package/src/index.ts +0 -36
- package/src/sync/index.ts +0 -288
- package/src/sync/syncProtocol.js +0 -291
- package/src/sync/tokenRegistry.ts +0 -20
- package/src/updater/updateMigrations.ts +0 -22
- package/src/updater/versionUpdater.ts +0 -153
- package/src/utils/network.ts +0 -135
- package/src/utils/normalizeClientId.ts +0 -22
- package/src/utils/resolveDid.ts +0 -101
- package/src/utils/schema.ts +0 -119
- package/src/utils/storage.ts +0 -67
- package/tsconfig.json +0 -9
- package/tsup.config.ts +0 -11
package/src/AuthContext.tsx
DELETED
|
@@ -1,591 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
useCallback,
|
|
3
|
-
useEffect,
|
|
4
|
-
useRef,
|
|
5
|
-
useState,
|
|
6
|
-
Suspense,
|
|
7
|
-
lazy,
|
|
8
|
-
} from 'react'
|
|
9
|
-
|
|
10
|
-
import { BasicSync, initDexieExtensions } from './sync'
|
|
11
|
-
import { RemoteDB, DBMode, BasicDB } from './core/db'
|
|
12
|
-
import { AuthManager } from './core/auth/AuthManager'
|
|
13
|
-
import type {
|
|
14
|
-
User,
|
|
15
|
-
AuthResult,
|
|
16
|
-
GetTokenOptions,
|
|
17
|
-
PdsEndpoints,
|
|
18
|
-
AuthStatus,
|
|
19
|
-
} from './core/auth/AuthManager'
|
|
20
|
-
|
|
21
|
-
import { log } from './config'
|
|
22
|
-
import { version as currentVersion } from '../package.json'
|
|
23
|
-
import { createVersionUpdater } from './updater/versionUpdater'
|
|
24
|
-
import { getMigrations } from './updater/updateMigrations'
|
|
25
|
-
import { BasicStorage, LocalStorageAdapter } from './utils/storage'
|
|
26
|
-
import {
|
|
27
|
-
isDevelopment,
|
|
28
|
-
checkForNewVersion,
|
|
29
|
-
getSyncStatus,
|
|
30
|
-
} from './utils/network'
|
|
31
|
-
import { validateAndCheckSchema } from './utils/schema'
|
|
32
|
-
import {
|
|
33
|
-
BasicContext,
|
|
34
|
-
DBStatus,
|
|
35
|
-
noDb,
|
|
36
|
-
type BasicSchemaDevInfo,
|
|
37
|
-
} from './context'
|
|
38
|
-
|
|
39
|
-
const BasicDevToolbar = lazy(() =>
|
|
40
|
-
import('./dev/BasicDevToolbar').then((m) => ({ default: m.BasicDevToolbar })),
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
export type { BasicStorage, LocalStorageAdapter } from './utils/storage'
|
|
44
|
-
export type { DBMode, BasicDB, Collection } from './core/db'
|
|
45
|
-
export type {
|
|
46
|
-
Token,
|
|
47
|
-
User,
|
|
48
|
-
AuthResult,
|
|
49
|
-
GetTokenOptions,
|
|
50
|
-
PdsEndpoints,
|
|
51
|
-
AuthStatus,
|
|
52
|
-
} from './core/auth/AuthManager'
|
|
53
|
-
export type { BasicContextType, BasicSchemaDevInfo } from './context'
|
|
54
|
-
export { DBStatus, useBasic, BasicContext } from './context'
|
|
55
|
-
|
|
56
|
-
export type AuthConfig = {
|
|
57
|
-
scopes?: string | string[]
|
|
58
|
-
/** @deprecated Use pds_url instead */
|
|
59
|
-
server_url?: string
|
|
60
|
-
/** PDS URL for auth and data (default: https://pds.basic.id) */
|
|
61
|
-
pds_url?: string
|
|
62
|
-
/** Admin server URL for connect reporting (default: https://api.basic.tech) */
|
|
63
|
-
admin_url?: string
|
|
64
|
-
ws_url?: string
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export type BasicProviderProps = {
|
|
68
|
-
children: React.ReactNode
|
|
69
|
-
/**
|
|
70
|
-
* @deprecated Project ID is now extracted from schema.project_id.
|
|
71
|
-
* This prop is kept for backward compatibility but can be omitted.
|
|
72
|
-
*/
|
|
73
|
-
project_id?: string
|
|
74
|
-
/** The Basic schema object containing project_id and table definitions */
|
|
75
|
-
schema?: any
|
|
76
|
-
debug?: boolean
|
|
77
|
-
storage?: BasicStorage
|
|
78
|
-
auth?: AuthConfig
|
|
79
|
-
/**
|
|
80
|
-
* Database mode - determines which implementation is used
|
|
81
|
-
* - 'sync': Uses Dexie + WebSocket for local-first sync (default)
|
|
82
|
-
* - 'remote': Uses REST API calls directly to server
|
|
83
|
-
*/
|
|
84
|
-
dbMode?: DBMode
|
|
85
|
-
/** Show floating dev toolbar (localhost, NODE_ENV=development, or debug=true). */
|
|
86
|
-
devToolbar?: boolean
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const DEFAULT_AUTH_CONFIG = {
|
|
90
|
-
scopes: 'profile,email,app:admin',
|
|
91
|
-
pds_url: 'https://pds.basic.id',
|
|
92
|
-
admin_url: 'https://api.basic.tech',
|
|
93
|
-
ws_url: 'wss://pds.basic.id/ws',
|
|
94
|
-
} as const
|
|
95
|
-
|
|
96
|
-
type ErrorObject = {
|
|
97
|
-
code: string
|
|
98
|
-
title: string
|
|
99
|
-
message: string
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
type AuthSnapshot = {
|
|
103
|
-
isSignedIn: boolean
|
|
104
|
-
hasToken: boolean
|
|
105
|
-
isAuthReady: boolean
|
|
106
|
-
authStatus: AuthStatus
|
|
107
|
-
authErrorCode: string | null
|
|
108
|
-
user: User | null
|
|
109
|
-
did: string | null
|
|
110
|
-
tokenScope: string | null
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function snapshotAuth(mgr: AuthManager): AuthSnapshot {
|
|
114
|
-
return {
|
|
115
|
-
isSignedIn: mgr.isSignedIn,
|
|
116
|
-
hasToken: !!mgr.token,
|
|
117
|
-
isAuthReady: mgr.isAuthReady,
|
|
118
|
-
authStatus: mgr.authStatus,
|
|
119
|
-
authErrorCode: mgr.authErrorCode,
|
|
120
|
-
user: mgr.user,
|
|
121
|
-
did: mgr.did,
|
|
122
|
-
tokenScope: mgr.tokenScope,
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function BasicProvider({
|
|
127
|
-
children,
|
|
128
|
-
project_id: project_id_prop,
|
|
129
|
-
schema,
|
|
130
|
-
debug = false,
|
|
131
|
-
storage,
|
|
132
|
-
auth,
|
|
133
|
-
dbMode = 'sync',
|
|
134
|
-
devToolbar = false,
|
|
135
|
-
}: BasicProviderProps) {
|
|
136
|
-
const project_id = schema?.project_id || project_id_prop
|
|
137
|
-
|
|
138
|
-
if (auth?.server_url && !auth?.pds_url) {
|
|
139
|
-
log('Warning: auth.server_url is deprecated, use auth.pds_url instead')
|
|
140
|
-
}
|
|
141
|
-
const authConfig = {
|
|
142
|
-
scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
|
|
143
|
-
pds_url: auth?.pds_url || auth?.server_url || DEFAULT_AUTH_CONFIG.pds_url,
|
|
144
|
-
admin_url: auth?.admin_url || DEFAULT_AUTH_CONFIG.admin_url,
|
|
145
|
-
ws_url: auth?.ws_url || DEFAULT_AUTH_CONFIG.ws_url,
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const scopesString = Array.isArray(authConfig.scopes)
|
|
149
|
-
? authConfig.scopes.join(' ')
|
|
150
|
-
: authConfig.scopes
|
|
151
|
-
|
|
152
|
-
const storageRef = useRef<BasicStorage>(storage || new LocalStorageAdapter())
|
|
153
|
-
const storageAdapter = storageRef.current
|
|
154
|
-
|
|
155
|
-
const schemaRef = useRef(schema)
|
|
156
|
-
schemaRef.current = schema
|
|
157
|
-
|
|
158
|
-
const [authState, setAuthState] = useState<AuthSnapshot>({
|
|
159
|
-
isSignedIn: false,
|
|
160
|
-
hasToken: false,
|
|
161
|
-
isAuthReady: false,
|
|
162
|
-
authStatus: 'bootstrapping',
|
|
163
|
-
authErrorCode: null,
|
|
164
|
-
user: null,
|
|
165
|
-
did: null,
|
|
166
|
-
tokenScope: null,
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
const authRef = useRef<AuthManager>(null!)
|
|
170
|
-
if (!authRef.current) {
|
|
171
|
-
authRef.current = new AuthManager(
|
|
172
|
-
{
|
|
173
|
-
projectId: project_id,
|
|
174
|
-
scopes: scopesString,
|
|
175
|
-
pdsUrl: authConfig.pds_url,
|
|
176
|
-
adminUrl: authConfig.admin_url,
|
|
177
|
-
debug,
|
|
178
|
-
},
|
|
179
|
-
storageAdapter,
|
|
180
|
-
() => setAuthState(snapshotAuth(authRef.current)),
|
|
181
|
-
)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const syncRef = useRef<BasicSync | null>(null)
|
|
185
|
-
const remoteDbRef = useRef<RemoteDB | null>(null)
|
|
186
|
-
const [shouldConnect, setShouldConnect] = useState(false)
|
|
187
|
-
const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.OFFLINE)
|
|
188
|
-
const [isDbReady, setIsDbReady] = useState(false)
|
|
189
|
-
const [error, setError] = useState<ErrorObject | null>(null)
|
|
190
|
-
const [schemaDevInfo, setSchemaDevInfo] = useState<BasicSchemaDevInfo | null>(
|
|
191
|
-
null,
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
const isDevMode = () => isDevelopment(debug)
|
|
195
|
-
|
|
196
|
-
const refreshSchemaStatus = useCallback(async () => {
|
|
197
|
-
const s = schemaRef.current
|
|
198
|
-
if (!s) {
|
|
199
|
-
setSchemaDevInfo(
|
|
200
|
-
project_id
|
|
201
|
-
? {
|
|
202
|
-
projectId: project_id,
|
|
203
|
-
localVersion: undefined,
|
|
204
|
-
status: 'no_schema',
|
|
205
|
-
valid: false,
|
|
206
|
-
lastCheckedAt: Date.now(),
|
|
207
|
-
}
|
|
208
|
-
: null,
|
|
209
|
-
)
|
|
210
|
-
return
|
|
211
|
-
}
|
|
212
|
-
const result = await validateAndCheckSchema(s)
|
|
213
|
-
if (!result.isValid) {
|
|
214
|
-
const errText =
|
|
215
|
-
result.errors
|
|
216
|
-
?.map((e: { message?: string }) => e.message || '')
|
|
217
|
-
.join('; ') || 'invalid'
|
|
218
|
-
setSchemaDevInfo({
|
|
219
|
-
projectId: s.project_id ?? null,
|
|
220
|
-
localVersion: s.version,
|
|
221
|
-
status: 'invalid',
|
|
222
|
-
valid: false,
|
|
223
|
-
lastCheckedAt: Date.now(),
|
|
224
|
-
error: errText,
|
|
225
|
-
})
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
setSchemaDevInfo({
|
|
229
|
-
projectId: s.project_id ?? null,
|
|
230
|
-
localVersion: s.version,
|
|
231
|
-
status: result.schemaStatus.status ?? 'unknown',
|
|
232
|
-
valid: result.schemaStatus.valid,
|
|
233
|
-
lastCheckedAt: Date.now(),
|
|
234
|
-
})
|
|
235
|
-
}, [project_id])
|
|
236
|
-
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
const runVersionUpdater = async () => {
|
|
239
|
-
try {
|
|
240
|
-
const versionUpdater = createVersionUpdater(
|
|
241
|
-
storageAdapter,
|
|
242
|
-
currentVersion,
|
|
243
|
-
getMigrations(),
|
|
244
|
-
)
|
|
245
|
-
const updateResult = await versionUpdater.checkAndUpdate()
|
|
246
|
-
|
|
247
|
-
if (updateResult.updated) {
|
|
248
|
-
log(
|
|
249
|
-
`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`,
|
|
250
|
-
)
|
|
251
|
-
} else {
|
|
252
|
-
log(`App version ${updateResult.toVersion} is current`)
|
|
253
|
-
}
|
|
254
|
-
} catch (error) {
|
|
255
|
-
log('Version update failed:', error)
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
runVersionUpdater()
|
|
260
|
-
authRef.current.initialize()
|
|
261
|
-
|
|
262
|
-
return authRef.current.setupNetworkListeners()
|
|
263
|
-
}, [])
|
|
264
|
-
|
|
265
|
-
useEffect(() => {
|
|
266
|
-
async function initSyncDb(options: { shouldConnect: boolean }) {
|
|
267
|
-
if (!syncRef.current) {
|
|
268
|
-
log('Initializing Basic Sync DB')
|
|
269
|
-
|
|
270
|
-
await initDexieExtensions()
|
|
271
|
-
|
|
272
|
-
syncRef.current = new BasicSync('basicdb', { schema: schema })
|
|
273
|
-
|
|
274
|
-
syncRef.current.syncable.on('statusChanged', (status: number) => {
|
|
275
|
-
const newStatus = getSyncStatus(status) as DBStatus
|
|
276
|
-
setDbStatus(newStatus)
|
|
277
|
-
|
|
278
|
-
if (newStatus === DBStatus.ERROR_WILL_RETRY) {
|
|
279
|
-
log(
|
|
280
|
-
'Sync entered ERROR_WILL_RETRY - reconciling auth session before retry',
|
|
281
|
-
)
|
|
282
|
-
authRef.current
|
|
283
|
-
.reconcileSession('sync retry', {
|
|
284
|
-
forceRefresh: true,
|
|
285
|
-
throttleMs: 0,
|
|
286
|
-
})
|
|
287
|
-
.catch(() => {})
|
|
288
|
-
}
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
if (options.shouldConnect) {
|
|
292
|
-
setShouldConnect(true)
|
|
293
|
-
} else {
|
|
294
|
-
log('Sync is disabled')
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
setIsDbReady(true)
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function initRemoteDb() {
|
|
302
|
-
if (!remoteDbRef.current) {
|
|
303
|
-
if (!project_id) {
|
|
304
|
-
setError({
|
|
305
|
-
code: 'missing_project_id',
|
|
306
|
-
title: 'Project ID Required',
|
|
307
|
-
message:
|
|
308
|
-
'Remote mode requires a project_id. Provide it via schema.project_id or the project_id prop.',
|
|
309
|
-
})
|
|
310
|
-
setIsDbReady(true)
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
log('Initializing Basic Remote DB')
|
|
315
|
-
remoteDbRef.current = new RemoteDB({
|
|
316
|
-
serverUrl: authConfig.pds_url,
|
|
317
|
-
projectId: project_id,
|
|
318
|
-
getToken: (opts) => authRef.current.getToken(opts),
|
|
319
|
-
schema: schema,
|
|
320
|
-
debug: debug,
|
|
321
|
-
onAuthError: (error) => {
|
|
322
|
-
log('RemoteDB auth error:', error)
|
|
323
|
-
if (error.errorType === 'forbidden') {
|
|
324
|
-
log('403 Forbidden - user lacks required scope, not signing out')
|
|
325
|
-
return
|
|
326
|
-
}
|
|
327
|
-
authRef.current
|
|
328
|
-
.reconcileSession(`remote db ${error.errorType}`, {
|
|
329
|
-
forceRefresh: error.errorType !== 'network',
|
|
330
|
-
throttleMs: 0,
|
|
331
|
-
})
|
|
332
|
-
.catch((reconcileError) => {
|
|
333
|
-
log('RemoteDB auth recovery failed:', reconcileError)
|
|
334
|
-
})
|
|
335
|
-
},
|
|
336
|
-
})
|
|
337
|
-
setDbStatus(DBStatus.ONLINE)
|
|
338
|
-
setIsDbReady(true)
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async function checkSchema() {
|
|
343
|
-
const result = await validateAndCheckSchema(schema)
|
|
344
|
-
|
|
345
|
-
if (!result.isValid) {
|
|
346
|
-
let errorMessage = ''
|
|
347
|
-
if (result.errors) {
|
|
348
|
-
result.errors.forEach((err: any, index: number) => {
|
|
349
|
-
errorMessage += `${index + 1}: ${err.message} - at ${err.instancePath}\n`
|
|
350
|
-
})
|
|
351
|
-
}
|
|
352
|
-
setSchemaDevInfo({
|
|
353
|
-
projectId: schema?.project_id ?? null,
|
|
354
|
-
localVersion: schema?.version,
|
|
355
|
-
status: 'invalid',
|
|
356
|
-
valid: false,
|
|
357
|
-
lastCheckedAt: Date.now(),
|
|
358
|
-
error: errorMessage.trim() || undefined,
|
|
359
|
-
})
|
|
360
|
-
setError({
|
|
361
|
-
code: 'schema_invalid',
|
|
362
|
-
title: 'Basic Schema is invalid!',
|
|
363
|
-
message: errorMessage,
|
|
364
|
-
})
|
|
365
|
-
setIsDbReady(true)
|
|
366
|
-
return null
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
setSchemaDevInfo({
|
|
370
|
-
projectId: schema?.project_id ?? null,
|
|
371
|
-
localVersion: schema?.version,
|
|
372
|
-
status: result.schemaStatus.status ?? 'unknown',
|
|
373
|
-
valid: result.schemaStatus.valid,
|
|
374
|
-
lastCheckedAt: Date.now(),
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
if (dbMode === 'remote') {
|
|
378
|
-
initRemoteDb()
|
|
379
|
-
} else {
|
|
380
|
-
if (result.schemaStatus.valid) {
|
|
381
|
-
await initSyncDb({ shouldConnect: true })
|
|
382
|
-
} else {
|
|
383
|
-
if (result.schemaStatus.status === 'unpublished') {
|
|
384
|
-
log(
|
|
385
|
-
'Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync.',
|
|
386
|
-
)
|
|
387
|
-
} else {
|
|
388
|
-
log('Schema is invalid!', result.schemaStatus)
|
|
389
|
-
}
|
|
390
|
-
await initSyncDb({ shouldConnect: false })
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
checkForNewVersion()
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (schema) {
|
|
398
|
-
checkSchema()
|
|
399
|
-
} else {
|
|
400
|
-
setSchemaDevInfo(
|
|
401
|
-
project_id
|
|
402
|
-
? {
|
|
403
|
-
projectId: project_id,
|
|
404
|
-
localVersion: undefined,
|
|
405
|
-
status: 'no_schema',
|
|
406
|
-
valid: false,
|
|
407
|
-
lastCheckedAt: Date.now(),
|
|
408
|
-
}
|
|
409
|
-
: null,
|
|
410
|
-
)
|
|
411
|
-
if (dbMode === 'remote' && project_id) {
|
|
412
|
-
initRemoteDb()
|
|
413
|
-
} else {
|
|
414
|
-
setIsDbReady(true)
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}, [])
|
|
418
|
-
|
|
419
|
-
useEffect(() => {
|
|
420
|
-
if (
|
|
421
|
-
authState.hasToken &&
|
|
422
|
-
syncRef.current &&
|
|
423
|
-
authState.isSignedIn &&
|
|
424
|
-
authState.authStatus !== 'reauth_required' &&
|
|
425
|
-
shouldConnect
|
|
426
|
-
) {
|
|
427
|
-
log('connecting to db...')
|
|
428
|
-
|
|
429
|
-
syncRef.current
|
|
430
|
-
?.connect({
|
|
431
|
-
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
432
|
-
ws_url: authConfig.ws_url,
|
|
433
|
-
})
|
|
434
|
-
.catch((e: any) => {
|
|
435
|
-
log('error connecting to db', e)
|
|
436
|
-
})
|
|
437
|
-
}
|
|
438
|
-
}, [
|
|
439
|
-
authState.authStatus,
|
|
440
|
-
authState.isSignedIn,
|
|
441
|
-
authState.hasToken,
|
|
442
|
-
shouldConnect,
|
|
443
|
-
])
|
|
444
|
-
|
|
445
|
-
useEffect(() => {
|
|
446
|
-
if (authState.authStatus !== 'reauth_required' || !syncRef.current) {
|
|
447
|
-
return
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
log('Auth requires reauthentication - disconnecting sync without deleting local DB')
|
|
451
|
-
setDbStatus(DBStatus.ERROR_TOKEN_EXPIRED)
|
|
452
|
-
syncRef.current
|
|
453
|
-
.disconnect({ ws_url: authConfig.ws_url })
|
|
454
|
-
.catch((disconnectError: unknown) => {
|
|
455
|
-
log('Error disconnecting sync after auth invalidation:', disconnectError)
|
|
456
|
-
})
|
|
457
|
-
}, [authConfig.ws_url, authState.authStatus])
|
|
458
|
-
|
|
459
|
-
// TODO: replace reload with proper sync DB teardown + re-init so
|
|
460
|
-
// sign-out → sign-in works without a full page reload.
|
|
461
|
-
const handleSignOut = async () => {
|
|
462
|
-
await authRef.current.signOut()
|
|
463
|
-
if (syncRef.current) {
|
|
464
|
-
try {
|
|
465
|
-
await syncRef.current.close()
|
|
466
|
-
await syncRef.current.delete({ disableAutoOpen: false })
|
|
467
|
-
syncRef.current = null
|
|
468
|
-
} catch (error) {
|
|
469
|
-
console.error('Error during database cleanup:', error)
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if (typeof window !== 'undefined') {
|
|
473
|
-
window.location.reload()
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const handleSignIn = async () => {
|
|
478
|
-
try {
|
|
479
|
-
await authRef.current.signIn()
|
|
480
|
-
} catch (error) {
|
|
481
|
-
if (isDevMode()) {
|
|
482
|
-
setError({
|
|
483
|
-
code: 'signin_error',
|
|
484
|
-
title: 'Sign-in Failed',
|
|
485
|
-
message:
|
|
486
|
-
(error as Error).message ||
|
|
487
|
-
'An error occurred during sign-in. Please try again.',
|
|
488
|
-
})
|
|
489
|
-
}
|
|
490
|
-
throw error
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const handleSignInWithHandle = async (handle: string) => {
|
|
495
|
-
try {
|
|
496
|
-
await authRef.current.signInWithHandle(handle)
|
|
497
|
-
} catch (error) {
|
|
498
|
-
if (isDevMode()) {
|
|
499
|
-
setError({
|
|
500
|
-
code: 'signin_error',
|
|
501
|
-
title: 'Sign-in Failed',
|
|
502
|
-
message:
|
|
503
|
-
(error as Error).message ||
|
|
504
|
-
'An error occurred during sign-in. Please try again.',
|
|
505
|
-
})
|
|
506
|
-
}
|
|
507
|
-
throw error
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const getCurrentDb = (): BasicDB => {
|
|
512
|
-
if (dbMode === 'remote') {
|
|
513
|
-
return remoteDbRef.current || noDb
|
|
514
|
-
}
|
|
515
|
-
return syncRef.current || noDb
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const contextValue = {
|
|
519
|
-
isReady: authState.isAuthReady,
|
|
520
|
-
isSignedIn: authState.isSignedIn,
|
|
521
|
-
authStatus: authState.authStatus,
|
|
522
|
-
authErrorCode: authState.authErrorCode,
|
|
523
|
-
user: authState.user,
|
|
524
|
-
did: authState.did,
|
|
525
|
-
scope: authState.tokenScope,
|
|
526
|
-
hasScope: (s: string) => authRef.current.hasScope(s),
|
|
527
|
-
missingScopes: () => authRef.current.missingScopes(),
|
|
528
|
-
|
|
529
|
-
signIn: handleSignIn,
|
|
530
|
-
signInWithHandle: handleSignInWithHandle,
|
|
531
|
-
signOut: handleSignOut,
|
|
532
|
-
signInWithCode: (code: string, state?: string) =>
|
|
533
|
-
authRef.current.signInWithCode(code, state),
|
|
534
|
-
|
|
535
|
-
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
536
|
-
getSignInUrl: (redirectUri?: string) =>
|
|
537
|
-
authRef.current.getSignInUrl(redirectUri),
|
|
538
|
-
|
|
539
|
-
db: getCurrentDb(),
|
|
540
|
-
dbStatus,
|
|
541
|
-
dbMode,
|
|
542
|
-
|
|
543
|
-
devInfo: schemaDevInfo,
|
|
544
|
-
refreshSchemaStatus,
|
|
545
|
-
|
|
546
|
-
isAuthReady: authState.isAuthReady,
|
|
547
|
-
signin: handleSignIn,
|
|
548
|
-
signout: handleSignOut,
|
|
549
|
-
signinWithCode: (code: string, state?: string) =>
|
|
550
|
-
authRef.current.signInWithCode(code, state),
|
|
551
|
-
getSignInLink: (redirectUri?: string) =>
|
|
552
|
-
authRef.current.getSignInUrl(redirectUri),
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
return (
|
|
556
|
-
<BasicContext.Provider value={contextValue}>
|
|
557
|
-
{error && isDevMode() && <ErrorDisplay error={error} />}
|
|
558
|
-
{devToolbar && isDevMode() && (
|
|
559
|
-
<Suspense fallback={null}>
|
|
560
|
-
<BasicDevToolbar debug={debug} />
|
|
561
|
-
</Suspense>
|
|
562
|
-
)}
|
|
563
|
-
{isDbReady && authState.isAuthReady && children}
|
|
564
|
-
</BasicContext.Provider>
|
|
565
|
-
)
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function ErrorDisplay({ error }: { error: ErrorObject }) {
|
|
569
|
-
return (
|
|
570
|
-
<div
|
|
571
|
-
style={{
|
|
572
|
-
position: 'absolute',
|
|
573
|
-
top: 20,
|
|
574
|
-
left: 20,
|
|
575
|
-
color: 'black',
|
|
576
|
-
backgroundColor: '#f8d7da',
|
|
577
|
-
border: '1px solid #f5c6cb',
|
|
578
|
-
borderRadius: '4px',
|
|
579
|
-
padding: '20px',
|
|
580
|
-
maxWidth: '400px',
|
|
581
|
-
margin: '20px auto',
|
|
582
|
-
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
|
583
|
-
fontFamily: 'monospace',
|
|
584
|
-
}}
|
|
585
|
-
>
|
|
586
|
-
<h3 style={{ fontSize: '0.8rem', opacity: 0.8 }}>code: {error.code}</h3>
|
|
587
|
-
<h1 style={{ fontSize: '1.2rem', lineHeight: 1.5 }}>{error.title}</h1>
|
|
588
|
-
<p>{error.message}</p>
|
|
589
|
-
</div>
|
|
590
|
-
)
|
|
591
|
-
}
|
package/src/config.ts
DELETED
package/src/context.tsx
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext } from 'react'
|
|
2
|
-
import type { BasicDB } from './core/db'
|
|
3
|
-
import type {
|
|
4
|
-
User,
|
|
5
|
-
AuthResult,
|
|
6
|
-
GetTokenOptions,
|
|
7
|
-
AuthStatus,
|
|
8
|
-
} from './core/auth/AuthManager'
|
|
9
|
-
import type { DBMode } from './core/db'
|
|
10
|
-
|
|
11
|
-
export enum DBStatus {
|
|
12
|
-
LOADING = 'LOADING',
|
|
13
|
-
OFFLINE = 'OFFLINE',
|
|
14
|
-
CONNECTING = 'CONNECTING',
|
|
15
|
-
ONLINE = 'ONLINE',
|
|
16
|
-
SYNCING = 'SYNCING',
|
|
17
|
-
ERROR = 'ERROR',
|
|
18
|
-
/** Sync-layer error with automatic retry (maps from dexie-syncable status 4). */
|
|
19
|
-
ERROR_WILL_RETRY = 'ERROR_WILL_RETRY',
|
|
20
|
-
/**
|
|
21
|
-
* Auth-driven status: set by the provider when `authStatus` transitions to
|
|
22
|
-
* `reauth_required`, causing sync to disconnect. Unlike the other statuses
|
|
23
|
-
* this is NOT mapped from a dexie-syncable status code — it is set
|
|
24
|
-
* programmatically by `BasicProvider` to signal that the token is
|
|
25
|
-
* definitively invalid and the user must re-authenticate before sync can
|
|
26
|
-
* resume.
|
|
27
|
-
*/
|
|
28
|
-
ERROR_TOKEN_EXPIRED = 'ERROR_TOKEN_EXPIRED',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Snapshot of local schema vs server (for dev toolbar and debugging). */
|
|
32
|
-
export type BasicSchemaDevInfo = {
|
|
33
|
-
projectId: string | null
|
|
34
|
-
localVersion: number | undefined
|
|
35
|
-
status: string
|
|
36
|
-
valid: boolean
|
|
37
|
-
lastCheckedAt: number
|
|
38
|
-
error?: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Context type for useBasic hook
|
|
43
|
-
*/
|
|
44
|
-
export type BasicContextType = {
|
|
45
|
-
isReady: boolean
|
|
46
|
-
isSignedIn: boolean
|
|
47
|
-
authStatus: AuthStatus
|
|
48
|
-
authErrorCode: string | null
|
|
49
|
-
user: User | null
|
|
50
|
-
did: string | null
|
|
51
|
-
scope: string | null
|
|
52
|
-
hasScope: (scope: string) => boolean
|
|
53
|
-
missingScopes: () => string[]
|
|
54
|
-
|
|
55
|
-
signIn: () => Promise<void>
|
|
56
|
-
signInWithHandle: (handle: string) => Promise<void>
|
|
57
|
-
signOut: () => Promise<void>
|
|
58
|
-
signInWithCode: (code: string, state?: string) => Promise<AuthResult>
|
|
59
|
-
|
|
60
|
-
getToken: (options?: GetTokenOptions) => Promise<string>
|
|
61
|
-
getSignInUrl: (redirectUri?: string) => Promise<string>
|
|
62
|
-
|
|
63
|
-
db: BasicDB
|
|
64
|
-
dbStatus: DBStatus
|
|
65
|
-
dbMode: DBMode
|
|
66
|
-
|
|
67
|
-
/** Local schema vs server status; null if no schema on the provider. */
|
|
68
|
-
devInfo: BasicSchemaDevInfo | null
|
|
69
|
-
/** Re-run remote schema check (dev toolbar). */
|
|
70
|
-
refreshSchemaStatus: () => Promise<void>
|
|
71
|
-
|
|
72
|
-
isAuthReady: boolean
|
|
73
|
-
signin: () => Promise<void>
|
|
74
|
-
signout: () => Promise<void>
|
|
75
|
-
signinWithCode: (code: string, state?: string) => Promise<AuthResult>
|
|
76
|
-
getSignInLink: (redirectUri?: string) => Promise<string>
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const noDb: BasicDB = {
|
|
80
|
-
collection: () => {
|
|
81
|
-
throw new Error('no basicdb found - initialization failed. double check your schema.')
|
|
82
|
-
},
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export const BasicContext = createContext<BasicContextType>({
|
|
86
|
-
isReady: false,
|
|
87
|
-
isSignedIn: false,
|
|
88
|
-
authStatus: 'bootstrapping',
|
|
89
|
-
authErrorCode: null,
|
|
90
|
-
user: null,
|
|
91
|
-
did: null,
|
|
92
|
-
scope: null,
|
|
93
|
-
hasScope: () => false,
|
|
94
|
-
missingScopes: () => [],
|
|
95
|
-
|
|
96
|
-
signIn: () => Promise.resolve(),
|
|
97
|
-
signInWithHandle: () => Promise.resolve(),
|
|
98
|
-
signOut: () => Promise.resolve(),
|
|
99
|
-
signInWithCode: () => Promise.resolve({ success: false }),
|
|
100
|
-
|
|
101
|
-
getToken: (_options?: GetTokenOptions) => Promise.reject(new Error('no token')),
|
|
102
|
-
getSignInUrl: () => Promise.resolve(''),
|
|
103
|
-
|
|
104
|
-
db: noDb,
|
|
105
|
-
dbStatus: DBStatus.LOADING,
|
|
106
|
-
dbMode: 'sync',
|
|
107
|
-
|
|
108
|
-
devInfo: null,
|
|
109
|
-
refreshSchemaStatus: async () => {},
|
|
110
|
-
|
|
111
|
-
isAuthReady: false,
|
|
112
|
-
signin: () => Promise.resolve(),
|
|
113
|
-
signout: () => Promise.resolve(),
|
|
114
|
-
signinWithCode: () => Promise.resolve({ success: false }),
|
|
115
|
-
getSignInLink: () => Promise.resolve(''),
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
export function useBasic() {
|
|
119
|
-
return useContext(BasicContext)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export { noDb }
|