@basictech/react 0.6.0 → 0.7.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.
@@ -1,5 +1,3 @@
1
- // @ts-nocheck
2
-
3
1
  import React, { createContext, useContext, useEffect, useState, useRef } from 'react'
4
2
  import { jwtDecode } from 'jwt-decode'
5
3
 
@@ -9,6 +7,25 @@ import { validateSchema, compareSchemas } from '@basictech/schema'
9
7
 
10
8
  import { log } from './config'
11
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>
14
+ }
15
+
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
+ }
28
+ }
12
29
 
13
30
  type BasicSyncType = {
14
31
  basic_schema: any;
@@ -20,7 +37,7 @@ type BasicSyncType = {
20
37
  count: () => Promise<number>;
21
38
  };
22
39
  };
23
- [key: string]: any; // For other potential methods and properties
40
+ [key: string]: any;
24
41
  };
25
42
 
26
43
 
@@ -46,7 +63,7 @@ type Token = {
46
63
  access_token: string,
47
64
  token_type: string,
48
65
  expires_in: number,
49
- refresh: string,
66
+ refresh_token: string,
50
67
  }
51
68
 
52
69
  export const BasicContext = createContext<{
@@ -54,10 +71,11 @@ export const BasicContext = createContext<{
54
71
  isAuthReady: boolean,
55
72
  isSignedIn: boolean,
56
73
  user: User | null,
57
- signout: () => void,
58
- signin: () => void,
74
+ signout: () => Promise<void>,
75
+ signin: () => Promise<void>,
76
+ signinWithCode: (code: string, state?: string) => Promise<{ success: boolean, error?: string }>,
59
77
  getToken: () => Promise<string>,
60
- getSignInLink: () => string,
78
+ getSignInLink: (redirectUri?: string) => Promise<string>,
61
79
  db: any,
62
80
  dbStatus: DBStatus
63
81
  }>({
@@ -65,21 +83,25 @@ export const BasicContext = createContext<{
65
83
  isAuthReady: false,
66
84
  isSignedIn: false,
67
85
  user: null,
68
- signout: () => { },
69
- signin: () => { },
86
+ signout: () => Promise.resolve(),
87
+ signin: () => Promise.resolve(),
88
+ signinWithCode: () => new Promise(() => { }),
70
89
  getToken: () => new Promise(() => { }),
71
- getSignInLink: () => "",
90
+ getSignInLink: () => Promise.resolve(""),
72
91
  db: {},
73
92
  dbStatus: DBStatus.LOADING
74
93
  });
75
94
 
76
95
  const EmptyDB: BasicSyncType = {
96
+ basic_schema: {},
97
+ connect: () => {},
98
+ debugeroo: () => {},
77
99
  isOpen: false,
78
100
  collection: () => {
79
101
  return {
80
102
  ref: {
81
- toArray: () => [],
82
- count: () => 0
103
+ toArray: () => Promise.resolve([]),
104
+ count: () => Promise.resolve(0)
83
105
  }
84
106
  }
85
107
  }
@@ -223,7 +245,19 @@ async function checkForNewVersion(): Promise<{ hasNewVersion: boolean, latestVer
223
245
  }
224
246
  }
225
247
 
226
- export function BasicProvider({ children, project_id, schema, debug = false }: { children: React.ReactNode, project_id?: string, schema?: any, debug?: boolean }) {
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,
258
+ debug?: boolean,
259
+ storage?: BasicStorage
260
+ }) {
227
261
  const [isAuthReady, setIsAuthReady] = useState(false)
228
262
  const [isSignedIn, setIsSignedIn] = useState<boolean>(false)
229
263
  const [token, setToken] = useState<Token | null>(null)
@@ -233,8 +267,71 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
233
267
 
234
268
  const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.OFFLINE)
235
269
  const [error, setError] = useState<ErrorObject | null>(null)
270
+ const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine)
271
+ const [pendingRefresh, setPendingRefresh] = useState<boolean>(false)
236
272
 
237
273
  const syncRef = useRef<BasicSync | null>(null);
274
+ 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'
280
+ }
281
+
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
+ }
293
+
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
+ }
303
+
304
+ useEffect(() => {
305
+ const handleOnline = () => {
306
+ log('Network came back online')
307
+ setIsOnline(true)
308
+ if (pendingRefresh) {
309
+ log('Retrying pending token refresh')
310
+ setPendingRefresh(false)
311
+ if (token) {
312
+ const refreshToken = token.refresh_token || localStorage.getItem('basic_refresh_token')
313
+ if (refreshToken) {
314
+ fetchToken(refreshToken).catch(error => {
315
+ log('Retry refresh failed:', error)
316
+ })
317
+ }
318
+ }
319
+ }
320
+ }
321
+
322
+ const handleOffline = () => {
323
+ log('Network went offline')
324
+ setIsOnline(false)
325
+ }
326
+
327
+ window.addEventListener('online', handleOnline)
328
+ window.addEventListener('offline', handleOffline)
329
+
330
+ return () => {
331
+ window.removeEventListener('online', handleOnline)
332
+ window.removeEventListener('offline', handleOffline)
333
+ }
334
+ }, [pendingRefresh, token])
238
335
 
239
336
  useEffect(() => {
240
337
  function initDb(options: { shouldConnect: boolean }) {
@@ -243,12 +340,12 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
243
340
  syncRef.current = new BasicSync('basicdb', { schema: schema });
244
341
 
245
342
  syncRef.current.syncable.on('statusChanged', (status: number, url: string) => {
246
- setDbStatus(getSyncStatus(status))
343
+ setDbStatus(getSyncStatus(status) as DBStatus)
247
344
  })
248
345
 
249
- syncRef.current.syncable.getStatus().then((status) => {
250
- setDbStatus(getSyncStatus(status))
251
- })
346
+ // syncRef.current.syncable.getStatus().then((status: number) => {
347
+ // setDbStatus(getSyncStatus(status) as DBStatus)
348
+ // })
252
349
 
253
350
  if (options.shouldConnect) {
254
351
  setShouldConnect(true)
@@ -257,12 +354,6 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
257
354
  }
258
355
 
259
356
  setIsReady(true)
260
-
261
- // log('db is open', syncRef.current.isOpen())
262
- // syncRef.current.open()
263
- // .then(() => {
264
- // log("is open now:", syncRef.current.isOpen())
265
- // })
266
357
  }
267
358
  }
268
359
 
@@ -276,7 +367,7 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
276
367
  log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`)
277
368
  errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
278
369
  })
279
- console.groupEnd('Schema Errors')
370
+ console.groupEnd()
280
371
  setError({
281
372
  code: 'schema_invalid',
282
373
  title: 'Basic Schema is invalid!',
@@ -319,39 +410,87 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
319
410
  }
320
411
  }, [isSignedIn, shouldConnect])
321
412
 
413
+ const connectToDb = async () => {
414
+ const tok = await getToken()
415
+ if (!tok) {
416
+ log('no token found')
417
+ return
418
+ }
419
+
420
+ log('connecting to db...')
421
+
422
+ syncRef.current?.connect({ access_token: tok })
423
+ .catch((e) => {
424
+ log('error connecting to db', e)
425
+ })
426
+ }
427
+
322
428
  useEffect(() => {
323
- localStorage.setItem('basic_debug', debug ? 'true' : 'false')
429
+ const initializeAuth = async () => {
430
+ await storageAdapter.set(STORAGE_KEYS.DEBUG, debug ? 'true' : 'false')
324
431
 
325
- try {
432
+ try {
326
433
  if (window.location.search.includes('code')) {
327
- let code = window.location?.search?.split('code=')[1].split('&')[0]
434
+ let code = window.location?.search?.split('code=')[1]?.split('&')[0]
435
+ if (!code) return
328
436
 
329
- const state = localStorage.getItem('basic_auth_state')
330
- if (!state || state !== window.location.search.split('state=')[1].split('&')[0]) {
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) {
331
440
  log('error: auth state does not match')
332
441
  setIsAuthReady(true)
333
442
 
334
- localStorage.removeItem('basic_auth_state')
335
- window.history.pushState({}, document.title, "/");
443
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
444
+ // Clean OAuth parameters from URL
445
+ cleanOAuthParamsFromUrl()
336
446
  return
337
447
  }
338
448
 
339
- localStorage.removeItem('basic_auth_state')
449
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
450
+ // Clean OAuth parameters from URL
451
+ cleanOAuthParamsFromUrl()
340
452
 
341
- fetchToken(code)
453
+ fetchToken(code).catch((error) => {
454
+ log('Error fetching token:', error)
455
+ })
342
456
  } else {
343
- let cookie_token = getCookie('basic_token')
344
- if (cookie_token !== '') {
345
- setToken(JSON.parse(cookie_token))
346
- } else {
347
- setIsAuthReady(true)
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)
462
+ })
463
+ } 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)
481
+ }
482
+ }
483
+ setIsAuthReady(true)
484
+ }
348
485
  }
349
486
  }
350
487
 
351
-
352
- } catch (e) {
353
- log('error getting cookie', e)
488
+ } catch (e) {
489
+ log('error getting token', e)
490
+ }
354
491
  }
492
+
493
+ initializeAuth()
355
494
  }, [])
356
495
 
357
496
  useEffect(() => {
@@ -368,16 +507,18 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
368
507
 
369
508
  if (user.error) {
370
509
  log('error fetching user', user.error)
371
- // refreshToken()
372
510
  return
373
511
  } else {
374
- // log('user', user)
375
- document.cookie = `basic_token=${JSON.stringify(token)}; Secure; SameSite=Strict`;
376
-
377
- if (window.location.search.includes('code')) {
378
- window.history.pushState({}, document.title, "/");
512
+ if (token?.refresh_token) {
513
+ await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
379
514
  }
380
515
 
516
+ await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user))
517
+ 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
+
381
522
  setUser(user)
382
523
  setIsSignedIn(true)
383
524
 
@@ -398,10 +539,21 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
398
539
 
399
540
  if (isExpired) {
400
541
  log('token is expired - refreshing ...')
401
- const newToken = await fetchToken(token?.refresh)
402
- fetchUser(newToken.access_token)
542
+ try {
543
+ const newToken = await fetchToken(token?.refresh_token || '')
544
+ fetchUser(newToken?.access_token || '')
545
+ } catch (error) {
546
+ log('Failed to refresh token in checkToken:', error)
547
+
548
+ if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
549
+ log('Network issue - continuing with expired token until online')
550
+ fetchUser(token?.access_token || '')
551
+ } else {
552
+ setIsAuthReady(true)
553
+ }
554
+ }
403
555
  } else {
404
- fetchUser(token.access_token)
556
+ fetchUser(token?.access_token || '')
405
557
  }
406
558
  }
407
559
 
@@ -410,65 +562,125 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
410
562
  }
411
563
  }, [token])
412
564
 
413
- const connectToDb = async () => {
414
- const tok = await getToken()
415
- if (!tok) {
416
- log('no token found')
417
- return
418
- }
565
+ const getSignInLink = async (redirectUri?: string) => {
566
+ try {
567
+ log('getting sign in link...')
419
568
 
420
- log('connecting to db...')
569
+ if (!project_id) {
570
+ throw new Error('Project ID is required to generate sign-in link')
571
+ }
421
572
 
422
- // TODO: handle if signed out after connect() is already called
573
+ const randomState = Math.random().toString(36).substring(6);
574
+ await storageAdapter.set(STORAGE_KEYS.AUTH_STATE, randomState)
423
575
 
424
- syncRef.current.connect({ access_token: tok })
425
- .catch((e) => {
426
- log('error connecting to db', e)
427
- })
428
- }
576
+ const redirectUrl = redirectUri || window.location.href
577
+
578
+ if (!redirectUrl || (!redirectUrl.startsWith('http://') && !redirectUrl.startsWith('https://'))) {
579
+ throw new Error('Invalid redirect URI provided')
580
+ }
429
581
 
430
- const getSignInLink = () => {
431
- log('getting sign in link...')
582
+ let baseUrl = "https://api.basic.tech/auth/authorize"
583
+ baseUrl += `?client_id=${project_id}`
584
+ baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`
585
+ baseUrl += `&response_type=code`
586
+ baseUrl += `&scope=profile`
587
+ baseUrl += `&state=${randomState}`
432
588
 
433
- const randomState = Math.random().toString(36).substring(6);
434
- localStorage.setItem('basic_auth_state', randomState)
589
+ log('Generated sign-in link successfully')
590
+ return baseUrl;
435
591
 
436
- let baseUrl = "https://api.basic.tech/auth/authorize"
437
- baseUrl += `?client_id=${project_id}`
438
- baseUrl += `&redirect_uri=${encodeURIComponent(window.location.href)}`
439
- baseUrl += `&response_type=code`
440
- baseUrl += `&scope=profile`
441
- baseUrl += `&state=${randomState}`
592
+ } catch (error) {
593
+ log('Error generating sign-in link:', error)
594
+ throw error
595
+ }
596
+ }
442
597
 
443
- return baseUrl;
598
+ const signin = async () => {
599
+ try {
600
+ log('signing in...')
601
+
602
+ if (!project_id) {
603
+ log('Error: project_id is required for sign-in')
604
+ throw new Error('Project ID is required for authentication')
605
+ }
606
+
607
+ const signInLink = await getSignInLink()
608
+ log('Generated sign-in link:', signInLink)
609
+
610
+ if (!signInLink || !signInLink.startsWith('https://')) {
611
+ log('Error: Invalid sign-in link generated')
612
+ throw new Error('Failed to generate valid sign-in URL')
613
+ }
614
+
615
+ window.location.href = signInLink
616
+
617
+ } catch (error) {
618
+ log('Error during sign-in:', error)
619
+
620
+ if (isDevelopment()) {
621
+ setError({
622
+ code: 'signin_error',
623
+ title: 'Sign-in Failed',
624
+ message: (error as Error).message || 'An error occurred during sign-in. Please try again.'
625
+ })
626
+ }
627
+
628
+ throw error
629
+ }
444
630
  }
445
631
 
446
- const signin = () => {
447
- log('signing in: ', getSignInLink())
448
- const signInLink = getSignInLink()
449
- //todo: change to the other thing?
450
- window.location.href = signInLink;
632
+ const signinWithCode = async (code: string, state?: string): Promise<{ success: boolean, error?: string }> => {
633
+ try {
634
+ log('signinWithCode called with code:', code)
635
+
636
+ if (!code || typeof code !== 'string') {
637
+ return { success: false, error: 'Invalid authorization code' }
638
+ }
639
+
640
+ if (state) {
641
+ const storedState = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
642
+ if (storedState && storedState !== state) {
643
+ log('State parameter mismatch:', { provided: state, stored: storedState })
644
+ return { success: false, error: 'State parameter mismatch' }
645
+ }
646
+ }
647
+
648
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
649
+ cleanOAuthParamsFromUrl()
650
+
651
+ const token = await fetchToken(code)
652
+
653
+ if (token) {
654
+ log('signinWithCode successful')
655
+ return { success: true }
656
+ } else {
657
+ return { success: false, error: 'Failed to exchange code for token' }
658
+ }
659
+ } catch (error) {
660
+ log('signinWithCode error:', error)
661
+ return {
662
+ success: false,
663
+ error: (error as Error).message || 'Authentication failed'
664
+ }
665
+ }
451
666
  }
452
667
 
453
- const signout = () => {
668
+ const signout = async () => {
454
669
  log('signing out!')
455
670
  setUser({})
456
671
  setIsSignedIn(false)
457
672
  setToken(null)
673
+
458
674
  document.cookie = `basic_token=; Secure; SameSite=Strict`;
459
- localStorage.removeItem('basic_auth_state')
460
-
461
- // if (syncRef.current) {
462
- // // WIP - BUG - sometimes connects even after signout
463
- // syncRef.current.disconnect()
464
-
465
-
466
- // }
675
+ document.cookie = `basic_access_token=; Secure; SameSite=Strict`;
676
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
677
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
678
+ await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
467
679
  if (syncRef.current) {
468
680
  (async () => {
469
681
  try {
470
- await syncRef.current.close()
471
- await syncRef.current.delete({disableAutoOpen: false})
682
+ await syncRef.current?.close()
683
+ await syncRef.current?.delete({disableAutoOpen: false})
472
684
  syncRef.current = null
473
685
  window?.location?.reload()
474
686
  } catch (error) {
@@ -481,7 +693,32 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
481
693
  const getToken = async (): Promise<string> => {
482
694
  log('getting token...')
483
695
 
696
+
484
697
  if (!token) {
698
+ // Try to recover from storage refresh token
699
+ const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
700
+ if (refreshToken) {
701
+ log('No token in memory, attempting to refresh from storage')
702
+ try {
703
+ const newToken = await fetchToken(refreshToken)
704
+ if (newToken?.access_token) {
705
+ return newToken.access_token
706
+ }
707
+ } catch (error) {
708
+ log('Failed to refresh token from storage:', error)
709
+
710
+ if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
711
+ log('Network issue - continuing with potentially expired token')
712
+ const lastToken = localStorage.getItem('basic_access_token')
713
+ if (lastToken) {
714
+ return lastToken
715
+ }
716
+ throw new Error('Network offline - authentication will be retried when online')
717
+ }
718
+
719
+ throw new Error('Authentication expired. Please sign in again.')
720
+ }
721
+ }
485
722
  log('no token found')
486
723
  throw new Error('no token found')
487
724
  }
@@ -491,8 +728,24 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
491
728
 
492
729
  if (isExpired) {
493
730
  log('token is expired - refreshing ...')
494
- const newToken = await fetchToken(token?.refresh)
495
- return newToken?.access_token || ''
731
+ const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
732
+ if (refreshToken) {
733
+ try {
734
+ const newToken = await fetchToken(refreshToken)
735
+ return newToken?.access_token || ''
736
+ } catch (error) {
737
+ log('Failed to refresh expired token:', error)
738
+
739
+ if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
740
+ log('Network issue - using expired token until network is restored')
741
+ return token.access_token
742
+ }
743
+
744
+ throw new Error('Authentication expired. Please sign in again.')
745
+ }
746
+ } else {
747
+ throw new Error('no refresh token available')
748
+ }
496
749
  }
497
750
 
498
751
  return token?.access_token || ''
@@ -503,8 +756,8 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
503
756
  if (document.cookie && document.cookie !== '') {
504
757
  const cookies = document.cookie.split(';');
505
758
  for (let i = 0; i < cookies.length; i++) {
506
- const cookie = cookies[i].trim();
507
- if (cookie.substring(0, name.length + 1) === (name + '=')) {
759
+ const cookie = cookies[i]?.trim();
760
+ if (cookie && cookie.substring(0, name.length + 1) === (name + '=')) {
508
761
  cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
509
762
  break;
510
763
  }
@@ -514,24 +767,79 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
514
767
  }
515
768
 
516
769
  const fetchToken = async (code: string) => {
517
- const token = await fetch('https://api.basic.tech/auth/token', {
518
- method: 'POST',
519
- headers: {
520
- 'Content-Type': 'application/json'
521
- },
522
- body: JSON.stringify({ code: code })
523
- })
524
- .then(response => response.json())
525
- .catch(error => log('Error:', error))
770
+ try {
771
+ if (!isOnline) {
772
+ log('Network is offline, marking refresh as pending')
773
+ setPendingRefresh(true)
774
+ throw new Error('Network offline - refresh will be retried when online')
775
+ }
526
776
 
527
- if (token.error) {
528
- log('error fetching token', token.error)
529
- return
530
- } else {
531
- // log('token', token)
532
- setToken(token)
777
+ const token = await fetch('https://api.basic.tech/auth/token', {
778
+ method: 'POST',
779
+ headers: {
780
+ 'Content-Type': 'application/json'
781
+ },
782
+ body: JSON.stringify({ code: code })
783
+ })
784
+ .then(response => response.json())
785
+ .catch(error => {
786
+ log('Network error fetching token:', error)
787
+ if (!isOnline) {
788
+ setPendingRefresh(true)
789
+ throw new Error('Network offline - refresh will be retried when online')
790
+ }
791
+ throw new Error('Network error during token refresh')
792
+ })
793
+
794
+ if (token.error) {
795
+ log('error fetching token', token.error)
796
+
797
+ if (token.error.includes('network') || token.error.includes('timeout')) {
798
+ setPendingRefresh(true)
799
+ throw new Error('Network issue - refresh will be retried when online')
800
+ }
801
+
802
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
803
+ 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
+
807
+ setUser({})
808
+ setIsSignedIn(false)
809
+ setToken(null)
810
+ setIsAuthReady(true)
811
+
812
+ throw new Error(`Token refresh failed: ${token.error}`)
813
+ } else {
814
+ setToken(token)
815
+ setPendingRefresh(false)
816
+
817
+ if (token.refresh_token) {
818
+ await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
819
+ log('Updated refresh token in storage')
820
+ }
821
+
822
+ document.cookie = `basic_access_token=${token.access_token}; Secure; SameSite=Strict; HttpOnly=false`;
823
+ log('Updated access token in cookie')
824
+ }
825
+ return token
826
+ } catch (error) {
827
+ log('Token refresh error:', error)
828
+
829
+ if (!(error as Error).message.includes('offline') && !(error as Error).message.includes('Network')) {
830
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
831
+ 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
+
835
+ setUser({})
836
+ setIsSignedIn(false)
837
+ setToken(null)
838
+ setIsAuthReady(true)
839
+ }
840
+
841
+ throw error
533
842
  }
534
- return token
535
843
  }
536
844
 
537
845
 
@@ -546,22 +854,22 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
546
854
  get: async () => {
547
855
  checkSignIn()
548
856
  const tok = await getToken()
549
- return get({ projectId: project_id, accountId: user.id, tableName: tableName, token: tok })
857
+ return get({ projectId: project_id, accountId: user?.id, tableName: tableName, token: tok })
550
858
  },
551
859
  add: async (value: any) => {
552
860
  checkSignIn()
553
861
  const tok = await getToken()
554
- return add({ projectId: project_id, accountId: user.id, tableName: tableName, value: value, token: tok })
862
+ return add({ projectId: project_id, accountId: user?.id, tableName: tableName, value: value, token: tok })
555
863
  },
556
864
  update: async (id: string, value: any) => {
557
865
  checkSignIn()
558
866
  const tok = await getToken()
559
- return update({ projectId: project_id, accountId: user.id, tableName: tableName, id: id, value: value, token: tok })
867
+ return update({ projectId: project_id, accountId: user?.id, tableName: tableName, id: id, value: value, token: tok })
560
868
  },
561
869
  delete: async (id: string) => {
562
870
  checkSignIn()
563
871
  const tok = await getToken()
564
- return deleteRecord({ projectId: project_id, accountId: user.id, tableName: tableName, id: id, token: tok })
872
+ return deleteRecord({ projectId: project_id, accountId: user?.id, tableName: tableName, id: id, token: tok })
565
873
  }
566
874
 
567
875
  }
@@ -582,13 +890,14 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
582
890
  user,
583
891
  signout,
584
892
  signin,
893
+ signinWithCode,
585
894
  getToken,
586
895
  getSignInLink,
587
896
  db: syncRef.current ? syncRef.current : noDb,
588
897
  dbStatus
589
898
  }}>
590
899
 
591
- {error && <ErrorDisplay error={error} />}
900
+ {error && isDevelopment() && <ErrorDisplay error={error} />}
592
901
  {isReady && children}
593
902
  </BasicContext.Provider>
594
903
  )
@@ -615,11 +924,6 @@ function ErrorDisplay({ error }: { error: ErrorObject }) {
615
924
  </div>
616
925
  }
617
926
 
618
- /*
619
- possible errors:
620
- - projectid missing / invalid
621
- - schema missing / invalid
622
- */
623
927
 
624
928
  export function useBasic() {
625
929
  return useContext(BasicContext);