@basictech/react 0.7.0-beta.1 → 0.7.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.
@@ -2,31 +2,28 @@ import React, { createContext, useContext, useEffect, useState, useRef } from 'r
2
2
  import { jwtDecode } from 'jwt-decode'
3
3
 
4
4
  import { BasicSync } from './sync'
5
- import { get, add, update, deleteRecord } from './db'
6
- import { validateSchema, compareSchemas } from '@basictech/schema'
7
5
 
8
6
  import { log } from './config'
9
- import {version as currentVersion} from '../package.json'
10
- export interface BasicStorage {
11
- get(key: string): Promise<string | null>
12
- set(key: string, value: string): Promise<void>
13
- remove(key: string): Promise<void>
7
+ import { version as currentVersion } from '../package.json'
8
+ import { createVersionUpdater } from './updater/versionUpdater'
9
+ import { getMigrations } from './updater/updateMigrations'
10
+ import { BasicStorage, LocalStorageAdapter, STORAGE_KEYS, getCookie, setCookie, clearCookie } from './utils/storage'
11
+ import { isDevelopment, checkForNewVersion, cleanOAuthParamsFromUrl, getSyncStatus } from './utils/network'
12
+ import { getSchemaStatus, validateAndCheckSchema } from './utils/schema'
13
+
14
+ export type { BasicStorage, LocalStorageAdapter } from './utils/storage'
15
+
16
+ export type AuthConfig = {
17
+ scopes?: string | string[];
18
+ server_url?: string;
14
19
  }
15
20
 
16
- export class LocalStorageAdapter implements BasicStorage {
17
- async get(key: string): Promise<string | null> {
18
- return localStorage.getItem(key)
19
- }
20
-
21
- async set(key: string, value: string): Promise<void> {
22
- localStorage.setItem(key, value)
23
- }
24
-
25
- async remove(key: string): Promise<void> {
26
- localStorage.removeItem(key)
27
- }
21
+ const DEFAULT_AUTH_CONFIG: Required<AuthConfig> = {
22
+ scopes: 'profile email app:admin',
23
+ server_url: 'https://api.basic.tech'
28
24
  }
29
25
 
26
+
30
27
  type BasicSyncType = {
31
28
  basic_schema: any;
32
29
  connect: (options: { access_token: string }) => void;
@@ -92,171 +89,26 @@ export const BasicContext = createContext<{
92
89
  dbStatus: DBStatus.LOADING
93
90
  });
94
91
 
95
- const EmptyDB: BasicSyncType = {
96
- basic_schema: {},
97
- connect: () => {},
98
- debugeroo: () => {},
99
- isOpen: false,
100
- collection: () => {
101
- return {
102
- ref: {
103
- toArray: () => Promise.resolve([]),
104
- count: () => Promise.resolve(0)
105
- }
106
- }
107
- }
108
- }
109
-
110
- async function getSchemaStatus(schema: any) {
111
- const projectId = schema.project_id
112
- let status = ''
113
- const valid = validateSchema(schema)
114
-
115
- if (!valid.valid) {
116
- console.warn('BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled')
117
- return {
118
- valid: false,
119
- status: 'invalid',
120
- latest: null
121
- }
122
- }
123
-
124
- const latestSchema = await fetch(`https://api.basic.tech/project/${projectId}/schema`)
125
- .then(res => res.json())
126
- .then(data => data.data[0].schema)
127
- .catch(err => {
128
- return {
129
- valid: false,
130
- status: 'error',
131
- latest: null
132
- }
133
- })
134
-
135
- console.log('latestSchema', latestSchema)
136
-
137
- if (!latestSchema.version) {
138
- return {
139
- valid: false,
140
- status: 'error',
141
- latest: null
142
- }
143
- }
144
-
145
- if (latestSchema.version > schema.version) {
146
- // error_code: schema_behind
147
- console.warn('BasicDB Error: your local schema version is behind the latest. Found version:', schema.version, 'but expected', latestSchema.version, " - sync is disabled")
148
- return {
149
- valid: false,
150
- status: 'behind',
151
- latest: latestSchema
152
- }
153
- } else if (latestSchema.version < schema.version) {
154
- // error_code: schema_ahead
155
- console.warn('BasicDB Error: your local schema version is ahead of the latest. Found version:', schema.version, 'but expected', latestSchema.version, " - sync is disabled")
156
- return {
157
- valid: false,
158
- status: 'ahead',
159
- latest: latestSchema
160
- }
161
- } else if (latestSchema.version === schema.version) {
162
- const changes = compareSchemas(schema, latestSchema)
163
- if (changes.valid) {
164
- return {
165
- valid: true,
166
- status: 'current',
167
- latest: latestSchema
168
- }
169
- } else {
170
- // error_code: schema_conflict
171
- console.warn('BasicDB Error: your local schema is conflicting with the latest. Your version:', schema.version, 'does not match origin version', latestSchema.version, " - sync is disabled")
172
- return {
173
- valid: false,
174
- status: 'conflict',
175
- latest: latestSchema
176
- }
177
- }
178
- } else {
179
- return {
180
- valid: false,
181
- status: 'error',
182
- latest: null
183
- }
184
- }
185
- }
186
-
187
-
188
- function getSyncStatus(statusCode: number): string {
189
- switch (statusCode) {
190
- case -1:
191
- return "ERROR";
192
- case 0:
193
- return "OFFLINE";
194
- case 1:
195
- return "CONNECTING";
196
- case 2:
197
- return "ONLINE";
198
- case 3:
199
- return "SYNCING";
200
- case 4:
201
- return "ERROR_WILL_RETRY";
202
- default:
203
- return "UNKNOWN";
204
- }
205
- }
206
-
207
92
  type ErrorObject = {
208
93
  code: string;
209
94
  title: string;
210
95
  message: string;
211
96
  }
212
97
 
213
- async function checkForNewVersion(): Promise<{ hasNewVersion: boolean, latestVersion: string | null, currentVersion: string | null }> {
214
- try {
215
-
216
- const isBeta = currentVersion.includes('beta')
217
-
218
- const response = await fetch(`https://registry.npmjs.org/@basictech/react/${isBeta ? 'beta' : 'latest'}`);
219
- if (!response.ok) {
220
- throw new Error('Failed to fetch version from npm');
221
- }
222
-
223
- const data = await response.json();
224
- const latestVersion = data.version;
225
-
226
- if (latestVersion !== currentVersion) {
227
- console.warn('[basic] New version available:', latestVersion, `\nrun "npm install @basictech/react@${latestVersion}" to update`);
228
- }
229
- if (isBeta) {
230
- log('thank you for being on basictech/react beta :)')
231
- }
232
-
233
- return {
234
- hasNewVersion: currentVersion !== latestVersion,
235
- latestVersion,
236
- currentVersion
237
- };
238
- } catch (error) {
239
- log('Error checking for new version:', error);
240
- return {
241
- hasNewVersion: false,
242
- latestVersion: null,
243
- currentVersion: null
244
- };
245
- }
246
- }
247
-
248
- export function BasicProvider({
249
- children,
250
- project_id,
251
- schema,
252
- debug = false,
253
- storage
254
- }: {
255
- children: React.ReactNode,
256
- project_id?: string,
257
- schema?: any,
98
+ export function BasicProvider({
99
+ children,
100
+ project_id,
101
+ schema,
102
+ debug = false,
103
+ storage,
104
+ auth
105
+ }: {
106
+ children: React.ReactNode,
107
+ project_id?: string,
108
+ schema?: any,
258
109
  debug?: boolean,
259
- storage?: BasicStorage
110
+ storage?: BasicStorage,
111
+ auth?: AuthConfig
260
112
  }) {
261
113
  const [isAuthReady, setIsAuthReady] = useState(false)
262
114
  const [isSignedIn, setIsSignedIn] = useState<boolean>(false)
@@ -272,34 +124,21 @@ export function BasicProvider({
272
124
 
273
125
  const syncRef = useRef<BasicSync | null>(null);
274
126
  const storageAdapter = storage || new LocalStorageAdapter();
275
- const STORAGE_KEYS = {
276
- REFRESH_TOKEN: 'basic_refresh_token',
277
- USER_INFO: 'basic_user_info',
278
- AUTH_STATE: 'basic_auth_state',
279
- DEBUG: 'basic_debug'
127
+
128
+ // Merge auth config with defaults
129
+ const authConfig: Required<AuthConfig> = {
130
+ scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
131
+ server_url: auth?.server_url || DEFAULT_AUTH_CONFIG.server_url
280
132
  }
133
+
134
+ // Normalize scopes to space-separated string
135
+ const scopesString = Array.isArray(authConfig.scopes)
136
+ ? authConfig.scopes.join(' ')
137
+ : authConfig.scopes;
281
138
 
282
- const isDevelopment = () => {
283
- return (
284
- window.location.hostname === 'localhost' ||
285
- window.location.hostname === '127.0.0.1' ||
286
- window.location.hostname.includes('localhost') ||
287
- window.location.hostname.includes('127.0.0.1') ||
288
- window.location.hostname.includes('.local') ||
289
- process.env.NODE_ENV === 'development' ||
290
- debug === true
291
- )
292
- }
139
+ const isDevMode = () => isDevelopment(debug)
293
140
 
294
- const cleanOAuthParamsFromUrl = () => {
295
- if (window.location.search.includes('code') || window.location.search.includes('state')) {
296
- const url = new URL(window.location.href)
297
- url.searchParams.delete('code')
298
- url.searchParams.delete('state')
299
- window.history.pushState({}, document.title, url.pathname + url.search)
300
- log('Cleaned OAuth parameters from URL')
301
- }
302
- }
141
+ const cleanOAuthParams = () => cleanOAuthParamsFromUrl()
303
142
 
304
143
  useEffect(() => {
305
144
  const handleOnline = () => {
@@ -311,14 +150,14 @@ export function BasicProvider({
311
150
  if (token) {
312
151
  const refreshToken = token.refresh_token || localStorage.getItem('basic_refresh_token')
313
152
  if (refreshToken) {
314
- fetchToken(refreshToken).catch(error => {
153
+ fetchToken(refreshToken, true).catch(error => {
315
154
  log('Retry refresh failed:', error)
316
155
  })
317
156
  }
318
157
  }
319
158
  }
320
159
  }
321
-
160
+
322
161
  const handleOffline = () => {
323
162
  log('Network went offline')
324
163
  setIsOnline(false)
@@ -338,18 +177,18 @@ export function BasicProvider({
338
177
  if (!syncRef.current) {
339
178
  log('Initializing Basic DB')
340
179
  syncRef.current = new BasicSync('basicdb', { schema: schema });
341
-
180
+
342
181
  syncRef.current.syncable.on('statusChanged', (status: number, url: string) => {
343
182
  setDbStatus(getSyncStatus(status) as DBStatus)
344
183
  })
345
-
184
+
346
185
  // syncRef.current.syncable.getStatus().then((status: number) => {
347
186
  // setDbStatus(getSyncStatus(status) as DBStatus)
348
187
  // })
349
188
 
350
- if (options.shouldConnect) {
189
+ if (options.shouldConnect) {
351
190
  setShouldConnect(true)
352
- } else {
191
+ } else {
353
192
  log('Sync is disabled')
354
193
  }
355
194
 
@@ -358,16 +197,15 @@ export function BasicProvider({
358
197
  }
359
198
 
360
199
  async function checkSchema() {
361
- const valid = validateSchema(schema)
362
- if (!valid.valid) {
363
- log('Basic Schema is invalid!', valid.errors)
364
- console.group('Schema Errors')
200
+ const result = await validateAndCheckSchema(schema)
201
+
202
+ if (!result.isValid) {
365
203
  let errorMessage = ''
366
- valid.errors.forEach((error, index) => {
367
- log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`)
368
- errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
369
- })
370
- console.groupEnd()
204
+ if (result.errors) {
205
+ result.errors.forEach((error, index) => {
206
+ errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
207
+ })
208
+ }
371
209
  setError({
372
210
  code: 'schema_invalid',
373
211
  title: 'Basic Schema is invalid!',
@@ -377,22 +215,13 @@ export function BasicProvider({
377
215
  return null
378
216
  }
379
217
 
380
-
381
- let schemaStatus = { valid: false }
382
- if (schema.version !== 0) {
383
- schemaStatus = await getSchemaStatus(schema)
384
- log('schemaStatus', schemaStatus)
385
- }else {
386
- log("schema not published - at version 0")
387
- }
388
-
389
- if (schemaStatus.valid) {
218
+ if (result.schemaStatus.valid) {
390
219
  initDb({ shouldConnect: true })
391
220
  } else {
392
- log('Schema is invalid!', schemaStatus)
221
+ log('Schema is invalid!', result.schemaStatus)
393
222
  initDb({ shouldConnect: false })
394
223
  }
395
-
224
+
396
225
  checkForNewVersion()
397
226
  }
398
227
 
@@ -403,100 +232,110 @@ export function BasicProvider({
403
232
  }
404
233
  }, []);
405
234
 
406
-
407
235
  useEffect(() => {
408
- if (token && syncRef.current && isSignedIn && shouldConnect) {
409
- connectToDb()
410
- }
411
- }, [isSignedIn, shouldConnect])
236
+ async function connectToDb() {
237
+ if (token && syncRef.current && isSignedIn && shouldConnect) {
238
+ const tok = await getToken()
239
+ if (!tok) {
240
+ log('no token found')
241
+ return
242
+ }
412
243
 
413
- const connectToDb = async () => {
414
- const tok = await getToken()
415
- if (!tok) {
416
- log('no token found')
417
- return
418
- }
244
+ log('connecting to db...')
419
245
 
420
- log('connecting to db...')
246
+ syncRef.current?.connect({ access_token: tok })
247
+ .catch((e) => {
248
+ log('error connecting to db', e)
249
+ })
250
+ }
251
+ }
252
+ connectToDb()
421
253
 
422
- syncRef.current?.connect({ access_token: tok })
423
- .catch((e) => {
424
- log('error connecting to db', e)
425
- })
426
- }
254
+ }, [isSignedIn, shouldConnect])
427
255
 
428
256
  useEffect(() => {
429
257
  const initializeAuth = async () => {
430
258
  await storageAdapter.set(STORAGE_KEYS.DEBUG, debug ? 'true' : 'false')
431
259
 
432
260
  try {
433
- if (window.location.search.includes('code')) {
434
- let code = window.location?.search?.split('code=')[1]?.split('&')[0]
435
- if (!code) return
261
+ const versionUpdater = createVersionUpdater(storageAdapter, currentVersion, getMigrations())
262
+ const updateResult = await versionUpdater.checkAndUpdate()
263
+
264
+ if (updateResult.updated) {
265
+ log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`)
266
+ } else {
267
+ log(`App version ${updateResult.toVersion} is current`)
268
+ }
269
+ } catch (error) {
270
+ log('Version update failed:', error)
271
+ }
272
+
273
+ try {
274
+ if (window.location.search.includes('code')) {
275
+ let code = window.location?.search?.split('code=')[1]?.split('&')[0]
276
+ if (!code) return
277
+
278
+ const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
279
+ const urlState = window.location.search.split('state=')[1]?.split('&')[0]
280
+ if (!state || state !== urlState) {
281
+ log('error: auth state does not match')
282
+ setIsAuthReady(true)
436
283
 
437
- const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
438
- const urlState = window.location.search.split('state=')[1]?.split('&')[0]
439
- if (!state || state !== urlState) {
440
- log('error: auth state does not match')
441
- setIsAuthReady(true)
284
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
285
+ cleanOAuthParams()
286
+ return
287
+ }
442
288
 
443
289
  await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
444
- // Clean OAuth parameters from URL
445
- cleanOAuthParamsFromUrl()
446
- return
447
- }
290
+ cleanOAuthParams()
448
291
 
449
- await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
450
- // Clean OAuth parameters from URL
451
- cleanOAuthParamsFromUrl()
452
-
453
- fetchToken(code).catch((error) => {
454
- log('Error fetching token:', error)
455
- })
456
- } else {
457
- const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
458
- if (refreshToken) {
459
- log('Found refresh token in storage, attempting to refresh access token')
460
- fetchToken(refreshToken).catch((error) => {
461
- log('Error fetching refresh token:', error)
292
+ fetchToken(code, false).catch((error) => {
293
+ log('Error fetching token:', error)
462
294
  })
463
295
  } else {
464
- let cookie_token = getCookie('basic_token')
465
- if (cookie_token !== '') {
466
- const tokenData = JSON.parse(cookie_token)
467
- setToken(tokenData)
468
- if (tokenData.refresh_token) {
469
- await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token)
470
- }
471
- } else {
472
- const cachedUserInfo = await storageAdapter.get(STORAGE_KEYS.USER_INFO)
473
- if (cachedUserInfo) {
474
- try {
475
- const userData = JSON.parse(cachedUserInfo)
476
- setUser(userData)
477
- setIsSignedIn(true)
478
- log('Loaded cached user info for offline mode')
479
- } catch (error) {
480
- log('Error parsing cached user info:', error)
296
+ const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
297
+ if (refreshToken) {
298
+ log('Found refresh token in storage, attempting to refresh access token')
299
+ fetchToken(refreshToken, true).catch((error) => {
300
+ log('Error fetching refresh token:', error)
301
+ })
302
+ } else {
303
+ let cookie_token = getCookie('basic_token')
304
+ if (cookie_token !== '') {
305
+ const tokenData = JSON.parse(cookie_token)
306
+ setToken(tokenData)
307
+ if (tokenData.refresh_token) {
308
+ await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token)
309
+ }
310
+ } else {
311
+ const cachedUserInfo = await storageAdapter.get(STORAGE_KEYS.USER_INFO)
312
+ if (cachedUserInfo) {
313
+ try {
314
+ const userData = JSON.parse(cachedUserInfo)
315
+ setUser(userData)
316
+ setIsSignedIn(true)
317
+ log('Loaded cached user info for offline mode')
318
+ } catch (error) {
319
+ log('Error parsing cached user info:', error)
320
+ }
481
321
  }
322
+ setIsAuthReady(true)
482
323
  }
483
- setIsAuthReady(true)
484
324
  }
485
325
  }
486
- }
487
326
 
488
327
  } catch (e) {
489
328
  log('error getting token', e)
490
329
  }
491
330
  }
492
-
331
+
493
332
  initializeAuth()
494
333
  }, [])
495
334
 
496
335
  useEffect(() => {
497
336
  async function fetchUser(acc_token: string) {
498
337
  console.info('fetching user')
499
- const user = await fetch('https://api.basic.tech/auth/userInfo', {
338
+ const user = await fetch(`${authConfig.server_url}/auth/userInfo`, {
500
339
  method: 'GET',
501
340
  headers: {
502
341
  'Authorization': `Bearer ${acc_token}`
@@ -512,13 +351,13 @@ export function BasicProvider({
512
351
  if (token?.refresh_token) {
513
352
  await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
514
353
  }
515
-
354
+
516
355
  await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user))
517
356
  log('Cached user info in storage')
518
-
519
- document.cookie = `basic_access_token=${token?.access_token}; Secure; SameSite=Strict; HttpOnly=false`;
520
- document.cookie = `basic_token=${JSON.stringify(token)}; Secure; SameSite=Strict`;
521
-
357
+
358
+ setCookie('basic_access_token', token?.access_token || '', { httpOnly: false });
359
+ setCookie('basic_token', JSON.stringify(token));
360
+
522
361
  setUser(user)
523
362
  setIsSignedIn(true)
524
363
 
@@ -540,11 +379,11 @@ export function BasicProvider({
540
379
  if (isExpired) {
541
380
  log('token is expired - refreshing ...')
542
381
  try {
543
- const newToken = await fetchToken(token?.refresh_token || '')
382
+ const newToken = await fetchToken(token?.refresh_token || '', true)
544
383
  fetchUser(newToken?.access_token || '')
545
384
  } catch (error) {
546
385
  log('Failed to refresh token in checkToken:', error)
547
-
386
+
548
387
  if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
549
388
  log('Network issue - continuing with expired token until online')
550
389
  fetchUser(token?.access_token || '')
@@ -559,7 +398,7 @@ export function BasicProvider({
559
398
 
560
399
  if (token) {
561
400
  checkToken()
562
- }
401
+ }
563
402
  }, [token])
564
403
 
565
404
  const getSignInLink = async (redirectUri?: string) => {
@@ -579,14 +418,18 @@ export function BasicProvider({
579
418
  throw new Error('Invalid redirect URI provided')
580
419
  }
581
420
 
582
- let baseUrl = "https://api.basic.tech/auth/authorize"
421
+ // Store redirect_uri for token exchange
422
+ await storageAdapter.set(STORAGE_KEYS.REDIRECT_URI, redirectUrl)
423
+ log('Stored redirect_uri for token exchange:', redirectUrl)
424
+
425
+ let baseUrl = `${authConfig.server_url}/auth/authorize`
583
426
  baseUrl += `?client_id=${project_id}`
584
427
  baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`
585
428
  baseUrl += `&response_type=code`
586
- baseUrl += `&scope=profile`
429
+ baseUrl += `&scope=${encodeURIComponent(scopesString)}`
587
430
  baseUrl += `&state=${randomState}`
588
431
 
589
- log('Generated sign-in link successfully')
432
+ log('Generated sign-in link successfully with scopes:', scopesString)
590
433
  return baseUrl;
591
434
 
592
435
  } catch (error) {
@@ -598,33 +441,33 @@ export function BasicProvider({
598
441
  const signin = async () => {
599
442
  try {
600
443
  log('signing in...')
601
-
444
+
602
445
  if (!project_id) {
603
446
  log('Error: project_id is required for sign-in')
604
447
  throw new Error('Project ID is required for authentication')
605
448
  }
606
-
449
+
607
450
  const signInLink = await getSignInLink()
608
451
  log('Generated sign-in link:', signInLink)
609
-
452
+
610
453
  if (!signInLink || !signInLink.startsWith('https://')) {
611
454
  log('Error: Invalid sign-in link generated')
612
455
  throw new Error('Failed to generate valid sign-in URL')
613
456
  }
614
-
457
+
615
458
  window.location.href = signInLink
616
-
459
+
617
460
  } catch (error) {
618
461
  log('Error during sign-in:', error)
619
-
620
- if (isDevelopment()) {
462
+
463
+ if (isDevMode()) {
621
464
  setError({
622
465
  code: 'signin_error',
623
466
  title: 'Sign-in Failed',
624
467
  message: (error as Error).message || 'An error occurred during sign-in. Please try again.'
625
468
  })
626
469
  }
627
-
470
+
628
471
  throw error
629
472
  }
630
473
  }
@@ -632,7 +475,7 @@ export function BasicProvider({
632
475
  const signinWithCode = async (code: string, state?: string): Promise<{ success: boolean, error?: string }> => {
633
476
  try {
634
477
  log('signinWithCode called with code:', code)
635
-
478
+
636
479
  if (!code || typeof code !== 'string') {
637
480
  return { success: false, error: 'Invalid authorization code' }
638
481
  }
@@ -646,10 +489,10 @@ export function BasicProvider({
646
489
  }
647
490
 
648
491
  await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
649
- cleanOAuthParamsFromUrl()
492
+ cleanOAuthParams()
493
+
494
+ const token = await fetchToken(code, false)
650
495
 
651
- const token = await fetchToken(code)
652
-
653
496
  if (token) {
654
497
  log('signinWithCode successful')
655
498
  return { success: true }
@@ -658,9 +501,9 @@ export function BasicProvider({
658
501
  }
659
502
  } catch (error) {
660
503
  log('signinWithCode error:', error)
661
- return {
662
- success: false,
663
- error: (error as Error).message || 'Authentication failed'
504
+ return {
505
+ success: false,
506
+ error: (error as Error).message || 'Authentication failed'
664
507
  }
665
508
  }
666
509
  }
@@ -670,17 +513,18 @@ export function BasicProvider({
670
513
  setUser({})
671
514
  setIsSignedIn(false)
672
515
  setToken(null)
673
-
674
- document.cookie = `basic_token=; Secure; SameSite=Strict`;
675
- document.cookie = `basic_access_token=; Secure; SameSite=Strict`;
516
+
517
+ clearCookie('basic_token');
518
+ clearCookie('basic_access_token');
676
519
  await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
677
520
  await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
678
521
  await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
522
+ await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
679
523
  if (syncRef.current) {
680
524
  (async () => {
681
525
  try {
682
526
  await syncRef.current?.close()
683
- await syncRef.current?.delete({disableAutoOpen: false})
527
+ await syncRef.current?.delete({ disableAutoOpen: false })
684
528
  syncRef.current = null
685
529
  window?.location?.reload()
686
530
  } catch (error) {
@@ -693,20 +537,19 @@ export function BasicProvider({
693
537
  const getToken = async (): Promise<string> => {
694
538
  log('getting token...')
695
539
 
696
-
697
540
  if (!token) {
698
541
  // Try to recover from storage refresh token
699
542
  const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
700
543
  if (refreshToken) {
701
544
  log('No token in memory, attempting to refresh from storage')
702
545
  try {
703
- const newToken = await fetchToken(refreshToken)
546
+ const newToken = await fetchToken(refreshToken, true)
704
547
  if (newToken?.access_token) {
705
548
  return newToken.access_token
706
549
  }
707
550
  } catch (error) {
708
551
  log('Failed to refresh token from storage:', error)
709
-
552
+
710
553
  if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
711
554
  log('Network issue - continuing with potentially expired token')
712
555
  const lastToken = localStorage.getItem('basic_access_token')
@@ -715,7 +558,7 @@ export function BasicProvider({
715
558
  }
716
559
  throw new Error('Network offline - authentication will be retried when online')
717
560
  }
718
-
561
+
719
562
  throw new Error('Authentication expired. Please sign in again.')
720
563
  }
721
564
  }
@@ -731,16 +574,16 @@ export function BasicProvider({
731
574
  const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
732
575
  if (refreshToken) {
733
576
  try {
734
- const newToken = await fetchToken(refreshToken)
577
+ const newToken = await fetchToken(refreshToken, true)
735
578
  return newToken?.access_token || ''
736
579
  } catch (error) {
737
580
  log('Failed to refresh expired token:', error)
738
-
581
+
739
582
  if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
740
583
  log('Network issue - using expired token until network is restored')
741
584
  return token.access_token
742
585
  }
743
-
586
+
744
587
  throw new Error('Authentication expired. Please sign in again.')
745
588
  }
746
589
  } else {
@@ -751,22 +594,7 @@ export function BasicProvider({
751
594
  return token?.access_token || ''
752
595
  }
753
596
 
754
- function getCookie(name: string) {
755
- let cookieValue = '';
756
- if (document.cookie && document.cookie !== '') {
757
- const cookies = document.cookie.split(';');
758
- for (let i = 0; i < cookies.length; i++) {
759
- const cookie = cookies[i]?.trim();
760
- if (cookie && cookie.substring(0, name.length + 1) === (name + '=')) {
761
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
762
- break;
763
- }
764
- }
765
- }
766
- return cookieValue;
767
- }
768
-
769
- const fetchToken = async (code: string) => {
597
+ const fetchToken = async (codeOrRefreshToken: string, isRefreshToken: boolean = false) => {
770
598
  try {
771
599
  if (!isOnline) {
772
600
  log('Network is offline, marking refresh as pending')
@@ -774,12 +602,48 @@ export function BasicProvider({
774
602
  throw new Error('Network offline - refresh will be retried when online')
775
603
  }
776
604
 
777
- const token = await fetch('https://api.basic.tech/auth/token', {
605
+ let requestBody: any
606
+
607
+ if (isRefreshToken) {
608
+ // Refresh token request
609
+ requestBody = {
610
+ grant_type: 'refresh_token',
611
+ refresh_token: codeOrRefreshToken
612
+ }
613
+ // Include client_id if available for validation
614
+ if (project_id) {
615
+ requestBody.client_id = project_id
616
+ }
617
+ } else {
618
+ // Authorization code exchange
619
+ requestBody = {
620
+ grant_type: 'authorization_code',
621
+ code: codeOrRefreshToken
622
+ }
623
+
624
+ // Retrieve stored redirect_uri (required by OAuth2 spec)
625
+ const storedRedirectUri = await storageAdapter.get(STORAGE_KEYS.REDIRECT_URI)
626
+ if (storedRedirectUri) {
627
+ requestBody.redirect_uri = storedRedirectUri
628
+ log('Including redirect_uri in token exchange:', storedRedirectUri)
629
+ } else {
630
+ log('Warning: No redirect_uri found in storage for token exchange')
631
+ }
632
+
633
+ // Include client_id for validation
634
+ if (project_id) {
635
+ requestBody.client_id = project_id
636
+ }
637
+ }
638
+
639
+ log('Token exchange request body:', { ...requestBody, refresh_token: isRefreshToken ? '[REDACTED]' : undefined, code: !isRefreshToken ? '[REDACTED]' : undefined })
640
+
641
+ const token = await fetch(`${authConfig.server_url}/auth/token`, {
778
642
  method: 'POST',
779
643
  headers: {
780
644
  'Content-Type': 'application/json'
781
645
  },
782
- body: JSON.stringify({ code: code })
646
+ body: JSON.stringify(requestBody)
783
647
  })
784
648
  .then(response => response.json())
785
649
  .catch(error => {
@@ -793,90 +657,64 @@ export function BasicProvider({
793
657
 
794
658
  if (token.error) {
795
659
  log('error fetching token', token.error)
796
-
660
+
797
661
  if (token.error.includes('network') || token.error.includes('timeout')) {
798
662
  setPendingRefresh(true)
799
663
  throw new Error('Network issue - refresh will be retried when online')
800
664
  }
801
-
665
+
802
666
  await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
803
667
  await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
804
- document.cookie = `basic_token=; Secure; SameSite=Strict`;
805
- document.cookie = `basic_access_token=; Secure; SameSite=Strict`;
806
-
668
+ await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
669
+ clearCookie('basic_token');
670
+ clearCookie('basic_access_token');
671
+
807
672
  setUser({})
808
673
  setIsSignedIn(false)
809
674
  setToken(null)
810
675
  setIsAuthReady(true)
811
-
676
+
812
677
  throw new Error(`Token refresh failed: ${token.error}`)
813
678
  } else {
814
679
  setToken(token)
815
680
  setPendingRefresh(false)
816
-
681
+
817
682
  if (token.refresh_token) {
818
683
  await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
819
684
  log('Updated refresh token in storage')
820
685
  }
821
-
822
- document.cookie = `basic_access_token=${token.access_token}; Secure; SameSite=Strict; HttpOnly=false`;
686
+
687
+ // Clean up redirect_uri after successful token exchange
688
+ if (!isRefreshToken) {
689
+ await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
690
+ log('Cleaned up redirect_uri from storage after successful exchange')
691
+ }
692
+
693
+ setCookie('basic_access_token', token.access_token, { httpOnly: false });
823
694
  log('Updated access token in cookie')
824
695
  }
825
696
  return token
826
697
  } catch (error) {
827
698
  log('Token refresh error:', error)
828
-
699
+
829
700
  if (!(error as Error).message.includes('offline') && !(error as Error).message.includes('Network')) {
830
701
  await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
831
702
  await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
832
- document.cookie = `basic_token=; Secure; SameSite=Strict`;
833
- document.cookie = `basic_access_token=; Secure; SameSite=Strict`;
834
-
703
+ await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI)
704
+ clearCookie('basic_token');
705
+ clearCookie('basic_access_token');
706
+
835
707
  setUser({})
836
708
  setIsSignedIn(false)
837
709
  setToken(null)
838
710
  setIsAuthReady(true)
839
711
  }
840
-
841
- throw error
842
- }
843
- }
844
-
845
-
846
- const db_ = (tableName: string) => {
847
- const checkSignIn = () => {
848
- if (!isSignedIn) {
849
- throw new Error('cannot use db. user not logged in.')
850
- }
851
- }
852
-
853
- return {
854
- get: async () => {
855
- checkSignIn()
856
- const tok = await getToken()
857
- return get({ projectId: project_id, accountId: user?.id, tableName: tableName, token: tok })
858
- },
859
- add: async (value: any) => {
860
- checkSignIn()
861
- const tok = await getToken()
862
- return add({ projectId: project_id, accountId: user?.id, tableName: tableName, value: value, token: tok })
863
- },
864
- update: async (id: string, value: any) => {
865
- checkSignIn()
866
- const tok = await getToken()
867
- return update({ projectId: project_id, accountId: user?.id, tableName: tableName, id: id, value: value, token: tok })
868
- },
869
- delete: async (id: string) => {
870
- checkSignIn()
871
- const tok = await getToken()
872
- return deleteRecord({ projectId: project_id, accountId: user?.id, tableName: tableName, id: id, token: tok })
873
- }
874
712
 
713
+ throw error
875
714
  }
876
-
877
715
  }
878
716
 
879
- const noDb = ({
717
+ const noDb = ({
880
718
  collection: () => {
881
719
  throw new Error('no basicdb found - initialization failed. double check your schema.')
882
720
  }
@@ -896,17 +734,17 @@ export function BasicProvider({
896
734
  db: syncRef.current ? syncRef.current : noDb,
897
735
  dbStatus
898
736
  }}>
899
-
900
- {error && isDevelopment() && <ErrorDisplay error={error} />}
737
+
738
+ {error && isDevMode() && <ErrorDisplay error={error} />}
901
739
  {isReady && children}
902
740
  </BasicContext.Provider>
903
741
  )
904
742
  }
905
743
 
906
744
  function ErrorDisplay({ error }: { error: ErrorObject }) {
907
- return <div style={{
745
+ return <div style={{
908
746
  position: 'absolute',
909
- top: 20,
747
+ top: 20,
910
748
  left: 20,
911
749
  color: 'black',
912
750
  backgroundColor: '#f8d7da',
@@ -916,10 +754,10 @@ function ErrorDisplay({ error }: { error: ErrorObject }) {
916
754
  maxWidth: '400px',
917
755
  margin: '20px auto',
918
756
  boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
919
- fontFamily: 'monospace',
920
- }}>
921
- <h3 style={{fontSize: '0.8rem', opacity: 0.8}}>code: {error.code}</h3>
922
- <h1 style={{fontSize: '1.2rem', lineHeight: '1.5'}}>{error.title}</h1>
757
+ fontFamily: 'monospace',
758
+ }}>
759
+ <h3 style={{ fontSize: '0.8rem', opacity: 0.8 }}>code: {error.code}</h3>
760
+ <h1 style={{ fontSize: '1.2rem', lineHeight: '1.5' }}>{error.title}</h1>
923
761
  <p>{error.message}</p>
924
762
  </div>
925
763
  }